aboutsummaryrefslogtreecommitdiff
path: root/src/main/kotlin
diff options
context:
space:
mode:
Diffstat (limited to 'src/main/kotlin')
-rw-r--r--src/main/kotlin/Compat.kt11
-rw-r--r--src/main/kotlin/Firmament.kt36
-rw-r--r--src/main/kotlin/apis/Profiles.kt16
-rw-r--r--src/main/kotlin/apis/Routes.kt30
-rw-r--r--src/main/kotlin/apis/UrsaManager.kt16
-rw-r--r--src/main/kotlin/commands/Duration.kt74
-rw-r--r--src/main/kotlin/commands/dsl.kt8
-rw-r--r--src/main/kotlin/commands/rome.kt54
-rw-r--r--src/main/kotlin/events/AllowChatEvent.kt2
-rw-r--r--src/main/kotlin/events/BakeExtraModelsEvent.kt24
-rw-r--r--src/main/kotlin/events/CustomItemModelEvent.kt73
-rw-r--r--src/main/kotlin/events/EntityRenderTintEvent.kt67
-rw-r--r--src/main/kotlin/events/EntityUpdateEvent.kt53
-rw-r--r--src/main/kotlin/events/FeaturesInitializedEvent.kt8
-rw-r--r--src/main/kotlin/events/FirmamentEventBus.kt3
-rw-r--r--src/main/kotlin/events/HandledScreenKeyPressedEvent.kt45
-rw-r--r--src/main/kotlin/events/IsSlotProtectedEvent.kt82
-rw-r--r--src/main/kotlin/events/JoinServerEvent.kt11
-rw-r--r--src/main/kotlin/events/ModifyChatEvent.kt2
-rw-r--r--src/main/kotlin/events/PartyMessageReceivedEvent.kt9
-rw-r--r--src/main/kotlin/events/PlayerInventoryUpdate.kt19
-rw-r--r--src/main/kotlin/events/SlotRenderEvents.kt3
-rw-r--r--src/main/kotlin/events/UseItemEvent.kt11
-rw-r--r--src/main/kotlin/events/WorldKeyboardEvent.kt21
-rw-r--r--src/main/kotlin/events/WorldMouseMoveEvent.kt5
-rw-r--r--src/main/kotlin/events/WorldRenderLastEvent.kt4
-rw-r--r--src/main/kotlin/events/registration/ChatEvents.kt86
-rw-r--r--src/main/kotlin/events/subscription/Subscription.kt4
-rw-r--r--src/main/kotlin/features/FeatureManager.kt134
-rw-r--r--src/main/kotlin/features/FirmamentFeature.kt23
-rw-r--r--src/main/kotlin/features/chat/AutoCompletions.kt15
-rw-r--r--src/main/kotlin/features/chat/ChatLinks.kt250
-rw-r--r--src/main/kotlin/features/chat/CopyChat.kt21
-rw-r--r--src/main/kotlin/features/chat/PartyCommands.kt136
-rw-r--r--src/main/kotlin/features/chat/QuickCommands.kt233
-rw-r--r--src/main/kotlin/features/debug/AnimatedClothingScanner.kt193
-rw-r--r--src/main/kotlin/features/debug/DebugLogger.kt6
-rw-r--r--src/main/kotlin/features/debug/DebugView.kt29
-rw-r--r--src/main/kotlin/features/debug/DeveloperFeatures.kt68
-rw-r--r--src/main/kotlin/features/debug/ExportedTestConstantMeta.kt27
-rw-r--r--src/main/kotlin/features/debug/MinorTrolling.kt6
-rw-r--r--src/main/kotlin/features/debug/PowerUserTools.kt79
-rw-r--r--src/main/kotlin/features/debug/SoundVisualizer.kt65
-rw-r--r--src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt255
-rw-r--r--src/main/kotlin/features/debug/itemeditor/ItemExporter.kt250
-rw-r--r--src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt87
-rw-r--r--src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt317
-rw-r--r--src/main/kotlin/features/diana/AncestralSpadeSolver.kt15
-rw-r--r--src/main/kotlin/features/diana/DianaWaypoints.kt36
-rw-r--r--src/main/kotlin/features/diana/NearbyBurrowsSolver.kt7
-rw-r--r--src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt399
-rw-r--r--src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt65
-rw-r--r--src/main/kotlin/features/events/carnival/CarnivalFeatures.kt13
-rw-r--r--src/main/kotlin/features/events/carnival/MinesweeperHelper.kt10
-rw-r--r--src/main/kotlin/features/fixes/CompatibliltyFeatures.kt12
-rw-r--r--src/main/kotlin/features/fixes/Fixes.kt117
-rw-r--r--src/main/kotlin/features/garden/HideComposterNoises.kt34
-rw-r--r--src/main/kotlin/features/inventory/CraftingOverlay.kt7
-rw-r--r--src/main/kotlin/features/inventory/ItemHotkeys.kt10
-rw-r--r--src/main/kotlin/features/inventory/ItemRarityCosmetics.kt34
-rw-r--r--src/main/kotlin/features/inventory/JunkHighlighter.kt30
-rw-r--r--src/main/kotlin/features/inventory/PetFeatures.kt90
-rw-r--r--src/main/kotlin/features/inventory/PriceData.kt147
-rw-r--r--src/main/kotlin/features/inventory/REIDependencyWarner.kt10
-rw-r--r--src/main/kotlin/features/inventory/SaveCursorPosition.kt107
-rw-r--r--src/main/kotlin/features/inventory/SlotLocking.kt273
-rw-r--r--src/main/kotlin/features/inventory/TimerInLore.kt152
-rw-r--r--src/main/kotlin/features/inventory/WardrobeKeybinds.kt78
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButton.kt129
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt146
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt1
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtons.kt155
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt4
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt62
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt4
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt238
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt32
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt82
-rw-r--r--src/main/kotlin/features/items/BlockZapperOverlay.kt139
-rw-r--r--src/main/kotlin/features/items/BonemerangOverlay.kt94
-rw-r--r--src/main/kotlin/features/items/EtherwarpOverlay.kt230
-rw-r--r--src/main/kotlin/features/macros/ComboProcessor.kt103
-rw-r--r--src/main/kotlin/features/macros/HotkeyAction.kt40
-rw-r--r--src/main/kotlin/features/macros/KeyComboTrie.kt73
-rw-r--r--src/main/kotlin/features/macros/MacroData.kt19
-rw-r--r--src/main/kotlin/features/macros/MacroUI.kt293
-rw-r--r--src/main/kotlin/features/macros/RadialMenu.kt153
-rw-r--r--src/main/kotlin/features/mining/CommissionFeatures.kt8
-rw-r--r--src/main/kotlin/features/mining/Histogram.kt3
-rw-r--r--src/main/kotlin/features/mining/HotmPresets.kt1
-rw-r--r--src/main/kotlin/features/mining/MiningBlockInfoUi.kt53
-rw-r--r--src/main/kotlin/features/mining/PickaxeAbility.kt128
-rw-r--r--src/main/kotlin/features/mining/PristineProfitTracker.kt41
-rw-r--r--src/main/kotlin/features/misc/CustomCapes.kt188
-rw-r--r--src/main/kotlin/features/misc/Devs.kt41
-rw-r--r--src/main/kotlin/features/misc/Hud.kt75
-rw-r--r--src/main/kotlin/features/misc/LicenseViewer.kt128
-rw-r--r--src/main/kotlin/features/misc/ModAnnouncer.kt80
-rw-r--r--src/main/kotlin/features/misc/TimerFeature.kt124
-rw-r--r--src/main/kotlin/features/notifications/Notifications.kt7
-rw-r--r--src/main/kotlin/features/texturepack/BakedModelExtra.kt30
-rw-r--r--src/main/kotlin/features/texturepack/BakedOverrideData.kt14
-rw-r--r--src/main/kotlin/features/texturepack/CustomBlockTextures.kt301
-rw-r--r--src/main/kotlin/features/texturepack/CustomGlobalArmorOverrides.kt155
-rw-r--r--src/main/kotlin/features/texturepack/CustomGlobalTextures.kt164
-rw-r--r--src/main/kotlin/features/texturepack/CustomModelOverrideParser.kt82
-rw-r--r--src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt135
-rw-r--r--src/main/kotlin/features/texturepack/CustomTextColors.kt66
-rw-r--r--src/main/kotlin/features/texturepack/FirmamentModelPredicate.kt8
-rw-r--r--src/main/kotlin/features/texturepack/FirmamentModelPredicateParser.kt8
-rw-r--r--src/main/kotlin/features/texturepack/JsonUnbakedModelFirmExtra.kt16
-rw-r--r--src/main/kotlin/features/texturepack/ModelOverrideData.kt15
-rw-r--r--src/main/kotlin/features/texturepack/RarityMatcher.kt69
-rw-r--r--src/main/kotlin/features/texturepack/StringMatcher.kt159
-rw-r--r--src/main/kotlin/features/texturepack/TintOverrides.kt75
-rw-r--r--src/main/kotlin/features/texturepack/predicates/AlwaysPredicate.kt19
-rw-r--r--src/main/kotlin/features/texturepack/predicates/AndPredicate.kt28
-rw-r--r--src/main/kotlin/features/texturepack/predicates/DisplayNamePredicate.kt22
-rw-r--r--src/main/kotlin/features/texturepack/predicates/ExtraAttributesPredicate.kt271
-rw-r--r--src/main/kotlin/features/texturepack/predicates/ItemPredicate.kt34
-rw-r--r--src/main/kotlin/features/texturepack/predicates/LorePredicate.kt22
-rw-r--r--src/main/kotlin/features/texturepack/predicates/NotPredicate.kt21
-rw-r--r--src/main/kotlin/features/texturepack/predicates/NumberMatcher.kt124
-rw-r--r--src/main/kotlin/features/texturepack/predicates/OrPredicate.kt29
-rw-r--r--src/main/kotlin/features/texturepack/predicates/PetPredicate.kt66
-rw-r--r--src/main/kotlin/features/world/ColeWeightCompat.kt125
-rw-r--r--src/main/kotlin/features/world/FairySouls.kt214
-rw-r--r--src/main/kotlin/features/world/FirmWaypointManager.kt168
-rw-r--r--src/main/kotlin/features/world/FirmWaypoints.kt37
-rw-r--r--src/main/kotlin/features/world/TemporaryWaypoints.kt72
-rw-r--r--src/main/kotlin/features/world/Waypoints.kt321
-rw-r--r--src/main/kotlin/gui/BarComponent.kt19
-rw-r--r--src/main/kotlin/gui/CheckboxComponent.kt57
-rw-r--r--src/main/kotlin/gui/FirmButtonComponent.kt134
-rw-r--r--src/main/kotlin/gui/FirmHoverComponent.kt92
-rw-r--r--src/main/kotlin/gui/ImageComponent.kt44
-rw-r--r--src/main/kotlin/gui/config/AllConfigsGui.kt27
-rw-r--r--src/main/kotlin/gui/config/BooleanHandler.kt3
-rw-r--r--src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt2
-rw-r--r--src/main/kotlin/gui/config/ChoiceHandler.kt49
-rw-r--r--src/main/kotlin/gui/config/ClickHandler.kt1
-rw-r--r--src/main/kotlin/gui/config/ColourHandler.kt83
-rw-r--r--src/main/kotlin/gui/config/DurationHandler.kt7
-rw-r--r--src/main/kotlin/gui/config/EnumRenderer.kt15
-rw-r--r--src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt2
-rw-r--r--src/main/kotlin/gui/config/HudMetaHandler.kt15
-rw-r--r--src/main/kotlin/gui/config/IntegerHandler.kt6
-rw-r--r--src/main/kotlin/gui/config/JAnyHud.kt71
-rw-r--r--src/main/kotlin/gui/config/KeyBindingHandler.kt38
-rw-r--r--src/main/kotlin/gui/config/KeyBindingStateManager.kt109
-rw-r--r--src/main/kotlin/gui/config/ManagedConfig.kt215
-rw-r--r--src/main/kotlin/gui/config/ManagedConfigElement.kt8
-rw-r--r--src/main/kotlin/gui/config/ManagedOption.kt12
-rw-r--r--src/main/kotlin/gui/config/StringHandler.kt3
-rw-r--r--src/main/kotlin/gui/config/storage/ConfigLoadContext.kt77
-rw-r--r--src/main/kotlin/gui/config/storage/ConfigStorageClass.kt8
-rw-r--r--src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt204
-rw-r--r--src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt83
-rw-r--r--src/main/kotlin/gui/config/storage/LegacyImporter.kt66
-rw-r--r--src/main/kotlin/gui/config/storage/README.md68
-rw-r--r--src/main/kotlin/gui/entity/EntityRenderer.kt139
-rw-r--r--src/main/kotlin/gui/entity/FakeWorld.kt338
-rw-r--r--src/main/kotlin/gui/entity/GuiPlayer.kt10
-rw-r--r--src/main/kotlin/gui/entity/ModifyEquipment.kt8
-rw-r--r--src/main/kotlin/gui/entity/ModifyHorse.kt76
-rw-r--r--src/main/kotlin/gui/hud/MoulConfigHud.kt92
-rw-r--r--src/main/kotlin/jarvis/JarvisIntegration.kt10
-rw-r--r--src/main/kotlin/keybindings/FirmamentKeyBindings.kt29
-rw-r--r--src/main/kotlin/keybindings/FirmamentKeyboardState.kt23
-rw-r--r--src/main/kotlin/keybindings/GenericInputButton.kt289
-rw-r--r--src/main/kotlin/keybindings/IKeyBinding.kt29
-rw-r--r--src/main/kotlin/keybindings/SavedKeyBinding.kt136
-rw-r--r--src/main/kotlin/repo/BetterRepoRecipeCache.kt41
-rw-r--r--src/main/kotlin/repo/EssenceRecipeProvider.kt75
-rw-r--r--src/main/kotlin/repo/ExpLadder.kt3
-rw-r--r--src/main/kotlin/repo/ExpensiveItemCacheApi.kt8
-rw-r--r--src/main/kotlin/repo/ExtraRecipeProvider.kt7
-rw-r--r--src/main/kotlin/repo/HypixelStaticData.kt25
-rw-r--r--src/main/kotlin/repo/ItemCache.kt208
-rw-r--r--src/main/kotlin/repo/MiningRepoData.kt131
-rw-r--r--src/main/kotlin/repo/ModernOverlaysData.kt41
-rw-r--r--src/main/kotlin/repo/Reforge.kt160
-rw-r--r--src/main/kotlin/repo/ReforgeStore.kt125
-rw-r--r--src/main/kotlin/repo/RepoDownloadManager.kt194
-rw-r--r--src/main/kotlin/repo/RepoItemTypeCache.kt15
-rw-r--r--src/main/kotlin/repo/RepoManager.kt98
-rw-r--r--src/main/kotlin/repo/RepoModResourcePack.kt161
-rw-r--r--src/main/kotlin/repo/SBItemStack.kt285
-rw-r--r--src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt4
-rw-r--r--src/main/kotlin/repo/recipes/RecipeLayouter.kt7
-rw-r--r--src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt34
-rw-r--r--src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt74
-rw-r--r--src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt88
-rw-r--r--src/main/kotlin/util/AprilFoolsUtil.kt10
-rw-r--r--src/main/kotlin/util/Base64Util.kt7
-rw-r--r--src/main/kotlin/util/BazaarPriceStrategy.kt2
-rw-r--r--src/main/kotlin/util/ChromaColourUtil.kt10
-rw-r--r--src/main/kotlin/util/ErrorUtil.kt25
-rw-r--r--src/main/kotlin/util/FirmFormatters.kt50
-rw-r--r--src/main/kotlin/util/FragmentGuiScreen.kt12
-rw-r--r--src/main/kotlin/util/HoveredItemStack.kt19
-rw-r--r--src/main/kotlin/util/IntUtil.kt12
-rw-r--r--src/main/kotlin/util/LegacyFormattingCode.kt54
-rw-r--r--src/main/kotlin/util/LegacyTagParser.kt2
-rw-r--r--src/main/kotlin/util/LegacyTagWriter.kt103
-rw-r--r--src/main/kotlin/util/MC.kt44
-rw-r--r--src/main/kotlin/util/MoulConfigFragment.kt69
-rw-r--r--src/main/kotlin/util/MoulConfigUtils.kt104
-rw-r--r--src/main/kotlin/util/SBData.kt133
-rw-r--r--src/main/kotlin/util/ScoreboardUtil.kt72
-rw-r--r--src/main/kotlin/util/SkyBlockIsland.kt64
-rw-r--r--src/main/kotlin/util/SkyblockId.kt185
-rw-r--r--src/main/kotlin/util/StringUtil.kt8
-rw-r--r--src/main/kotlin/util/TemplateUtil.kt3
-rw-r--r--src/main/kotlin/util/TestUtil.kt2
-rw-r--r--src/main/kotlin/util/WarpUtil.kt141
-rw-r--r--src/main/kotlin/util/accessors/GetRectangle.kt2
-rw-r--r--src/main/kotlin/util/asm/AsmAnnotationUtil.kt89
-rw-r--r--src/main/kotlin/util/async/input.kt112
-rw-r--r--src/main/kotlin/util/collections/RangeUtil.kt40
-rw-r--r--src/main/kotlin/util/collections/WeakCache.kt202
-rw-r--r--src/main/kotlin/util/compatloader/CompatLoader.kt2
-rw-r--r--src/main/kotlin/util/compatloader/CompatMeta.kt48
-rw-r--r--src/main/kotlin/util/data/Config.kt15
-rw-r--r--src/main/kotlin/util/data/DataHolder.kt63
-rw-r--r--src/main/kotlin/util/data/IDataHolder.kt140
-rw-r--r--src/main/kotlin/util/data/MultiFileDataHolder.kt62
-rw-r--r--src/main/kotlin/util/data/ProfileSpecificDataHolder.kt83
-rw-r--r--src/main/kotlin/util/json/CodecSerializer.kt26
-rw-r--r--src/main/kotlin/util/json/DashlessUUIDSerializer.kt7
-rw-r--r--src/main/kotlin/util/json/InstantAsLongSerializer.kt6
-rw-r--r--src/main/kotlin/util/json/KJsonOps.kt131
-rw-r--r--src/main/kotlin/util/json/KJsonUtils.kt11
-rw-r--r--src/main/kotlin/util/json/jsonConversion.kt65
-rw-r--r--src/main/kotlin/util/math/GChainReconciliation.kt102
-rw-r--r--src/main/kotlin/util/math/Projections.kt46
-rw-r--r--src/main/kotlin/util/mc/ArmorUtil.kt8
-rw-r--r--src/main/kotlin/util/mc/CustomRenderPassHelper.kt160
-rw-r--r--src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt24
-rw-r--r--src/main/kotlin/util/mc/InitLevel.kt25
-rw-r--r--src/main/kotlin/util/mc/IntrospectableItemModelManager.kt7
-rw-r--r--src/main/kotlin/util/mc/ItemUtil.kt30
-rw-r--r--src/main/kotlin/util/mc/MCTabListAPI.kt96
-rw-r--r--src/main/kotlin/util/mc/NbtItemData.kt4
-rw-r--r--src/main/kotlin/util/mc/NbtPrism.kt85
-rw-r--r--src/main/kotlin/util/mc/NbtUtil.kt10
-rw-r--r--src/main/kotlin/util/mc/PlayerUtil.kt7
-rw-r--r--src/main/kotlin/util/mc/SNbtFormatter.kt16
-rw-r--r--src/main/kotlin/util/mc/SkullItemData.kt8
-rw-r--r--src/main/kotlin/util/mc/SlotUtils.kt18
-rw-r--r--src/main/kotlin/util/mc/TolerantRegistriesOps.kt29
-rw-r--r--src/main/kotlin/util/mc/asFakeServer.kt37
-rw-r--r--src/main/kotlin/util/regex.kt14
-rw-r--r--src/main/kotlin/util/render/CustomRenderLayers.kt105
-rw-r--r--src/main/kotlin/util/render/DrawContextExt.kt192
-rw-r--r--src/main/kotlin/util/render/DumpTexture.kt34
-rw-r--r--src/main/kotlin/util/render/FacingThePlayerContext.kt26
-rw-r--r--src/main/kotlin/util/render/FirmamentShaders.kt20
-rw-r--r--src/main/kotlin/util/render/LerpUtils.kt37
-rw-r--r--src/main/kotlin/util/render/MultiSpecialGuiRenderState.kt47
-rw-r--r--src/main/kotlin/util/render/RenderCircleProgress.kt219
-rw-r--r--src/main/kotlin/util/render/RenderInWorldContext.kt222
-rw-r--r--src/main/kotlin/util/render/TintedOverlayTexture.kt35
-rw-r--r--src/main/kotlin/util/render/TranslatedScissors.kt30
-rw-r--r--src/main/kotlin/util/skyblock/DungeonUtil.kt33
-rw-r--r--src/main/kotlin/util/skyblock/ItemType.kt42
-rw-r--r--src/main/kotlin/util/skyblock/PartyUtil.kt210
-rw-r--r--src/main/kotlin/util/skyblock/Rarity.kt40
-rw-r--r--src/main/kotlin/util/skyblock/SackUtil.kt15
-rw-r--r--src/main/kotlin/util/skyblock/SkyBlockItems.kt15
-rw-r--r--src/main/kotlin/util/skyblock/TabListAPI.kt41
-rw-r--r--src/main/kotlin/util/textutil.kt174
-rw-r--r--src/main/kotlin/util/uuid.kt6
273 files changed, 13047 insertions, 6299 deletions
diff --git a/src/main/kotlin/Compat.kt b/src/main/kotlin/Compat.kt
new file mode 100644
index 0000000..ba3c88d
--- /dev/null
+++ b/src/main/kotlin/Compat.kt
@@ -0,0 +1,11 @@
+package moe.nea.firmament
+
+import moe.nea.firmament.util.compatloader.CompatMeta
+import moe.nea.firmament.util.compatloader.ICompatMeta
+
+@CompatMeta
+object Compat : ICompatMeta {
+ override fun shouldLoad(): Boolean {
+ return true
+ }
+}
diff --git a/src/main/kotlin/Firmament.kt b/src/main/kotlin/Firmament.kt
index 2c2a6b7..04af5bc 100644
--- a/src/main/kotlin/Firmament.kt
+++ b/src/main/kotlin/Firmament.kt
@@ -1,5 +1,6 @@
package moe.nea.firmament
+import com.google.gson.Gson
import com.mojang.brigadier.CommandDispatcher
import io.ktor.client.HttpClient
import io.ktor.client.plugins.UserAgent
@@ -18,6 +19,8 @@ import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback
import net.fabricmc.fabric.api.client.screen.v1.ScreenEvents
+import net.fabricmc.fabric.api.resource.ResourceManagerHelper
+import net.fabricmc.fabric.api.resource.ResourcePackActivationType
import net.fabricmc.loader.api.FabricLoader
import net.fabricmc.loader.api.Version
import net.fabricmc.loader.api.metadata.ModMetadata
@@ -43,11 +46,13 @@ import moe.nea.firmament.events.ScreenRenderPostEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.events.registration.registerFirmamentEvents
import moe.nea.firmament.features.FeatureManager
+import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader
import moe.nea.firmament.repo.HypixelStaticData
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
-import moe.nea.firmament.util.data.IDataHolder
+import moe.nea.firmament.util.mc.InitLevel
+import moe.nea.firmament.util.tr
object Firmament {
val modContainer by lazy { FabricLoader.getInstance().getModContainer(MOD_ID).get() }
@@ -62,16 +67,32 @@ object Firmament {
}
val version: Version by lazy { metadata.version }
+ private val DEFAULT_JSON_INDENT = " "
+
@OptIn(ExperimentalSerializationApi::class)
val json = Json {
prettyPrint = DEBUG
isLenient = true
allowTrailingComma = true
+ allowComments = true
ignoreUnknownKeys = true
encodeDefaults = true
+ prettyPrintIndent = if (prettyPrint) "\t" else DEFAULT_JSON_INDENT
+ }
+
+ /**
+ * FUCK two space indentation
+ */
+ val twoSpaceJson = Json(from = json) {
+ prettyPrint = true
+ prettyPrintIndent = " "
}
+ val gson = Gson()
val tightJson = Json(from = json) {
prettyPrint = false
+ // Reset pretty print indent back to default to prevent getting yelled at by json
+ prettyPrintIndent = DEFAULT_JSON_INDENT
+ explicitNulls = false
}
@@ -114,15 +135,14 @@ object Firmament {
@JvmStatic
fun onClientInitialize() {
+ InitLevel.bump(InitLevel.MC_INIT)
FeatureManager.subscribeEvents()
- var tick = 0
+ FirmamentConfigLoader.loadConfig()
ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { instance ->
- TickEvent.publish(TickEvent(tick++))
+ TickEvent.publish(TickEvent(MC.currentTick++))
})
- IDataHolder.registerEvents()
RepoManager.initialize()
SBData.init()
- FeatureManager.autoload()
HypixelStaticData.spawnDataCollectionLoop()
ClientCommandRegistrationCallback.EVENT.register(this::registerCommands)
ClientLifecycleEvents.CLIENT_STARTED.register(ClientLifecycleEvents.ClientStarted {
@@ -143,6 +163,12 @@ object Firmament {
})
})
ClientInitEvent.publish(ClientInitEvent())
+ ResourceManagerHelper.registerBuiltinResourcePack(
+ identifier("transparent_overlay"),
+ modContainer,
+ tr("firmament.resourcepack.transparentoverlay", "Transparent Firmament Overlay"),
+ ResourcePackActivationType.NORMAL
+ )
}
diff --git a/src/main/kotlin/apis/Profiles.kt b/src/main/kotlin/apis/Profiles.kt
index 789364a..ec4a6e4 100644
--- a/src/main/kotlin/apis/Profiles.kt
+++ b/src/main/kotlin/apis/Profiles.kt
@@ -6,20 +6,20 @@ package moe.nea.firmament.apis
import io.github.moulberry.repo.constants.Leveling
import io.github.moulberry.repo.data.Rarity
-import kotlinx.datetime.Instant
+import java.time.Instant
+import java.util.UUID
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
+import kotlin.reflect.KProperty1
+import net.minecraft.util.DyeColor
+import net.minecraft.util.Formatting
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.LegacyFormattingCode
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.assertNotNullOr
import moe.nea.firmament.util.json.DashlessUUIDSerializer
import moe.nea.firmament.util.json.InstantAsLongSerializer
-import net.minecraft.util.DyeColor
-import net.minecraft.util.Formatting
-import java.util.*
-import kotlin.reflect.KProperty1
@Serializable
@@ -188,7 +188,7 @@ data class PlayerData(
}
@Serializable
-data class AshconNameLookup(
- val username: String,
- val uuid: UUID,
+data class MowojangNameLookup(
+ val name: String,
+ val id: UUID,
)
diff --git a/src/main/kotlin/apis/Routes.kt b/src/main/kotlin/apis/Routes.kt
index bf55a2d..737763d 100644
--- a/src/main/kotlin/apis/Routes.kt
+++ b/src/main/kotlin/apis/Routes.kt
@@ -2,19 +2,15 @@
package moe.nea.firmament.apis
-import io.ktor.client.call.*
-import io.ktor.client.request.*
-import io.ktor.http.*
-import io.ktor.util.*
-import java.util.*
+import io.ktor.client.call.body
+import io.ktor.client.request.get
+import io.ktor.http.isSuccess
+import io.ktor.util.CaseInsensitiveMap
+import java.util.UUID
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.async
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
-import kotlin.collections.MutableMap
-import kotlin.collections.listOf
-import kotlin.collections.mutableMapOf
-import kotlin.collections.set
import moe.nea.firmament.Firmament
import moe.nea.firmament.util.MinecraftDispatcher
@@ -28,13 +24,13 @@ object Routes {
return withContext(MinecraftDispatcher) {
UUIDToName.computeIfAbsent(uuid) {
async(Firmament.coroutineScope.coroutineContext) {
- val response = Firmament.httpClient.get("https://api.ashcon.app/mojang/v2/user/$uuid")
+ val response = Firmament.httpClient.get("https://mowojang.matdoes.dev/$uuid")
if (!response.status.isSuccess()) return@async null
- val data = response.body<AshconNameLookup>()
+ val data = response.body<MowojangNameLookup>()
launch(MinecraftDispatcher) {
- nameToUUID[data.username] = async { data.uuid }
+ nameToUUID[data.name] = async { data.id }
}
- data.username
+ data.name
}
}
}.await()
@@ -44,13 +40,13 @@ object Routes {
return withContext(MinecraftDispatcher) {
nameToUUID.computeIfAbsent(name) {
async(Firmament.coroutineScope.coroutineContext) {
- val response = Firmament.httpClient.get("https://api.ashcon.app/mojang/v2/user/$name")
+ val response = Firmament.httpClient.get("https://mowojang.matdoes.dev/$name")
if (!response.status.isSuccess()) return@async null
- val data = response.body<AshconNameLookup>()
+ val data = response.body<MowojangNameLookup>()
launch(MinecraftDispatcher) {
- UUIDToName[data.uuid] = async { data.username }
+ UUIDToName[data.id] = async { data.name }
}
- data.uuid
+ data.id
}
}
}.await()
diff --git a/src/main/kotlin/apis/UrsaManager.kt b/src/main/kotlin/apis/UrsaManager.kt
index 13f7aef..19e030c 100644
--- a/src/main/kotlin/apis/UrsaManager.kt
+++ b/src/main/kotlin/apis/UrsaManager.kt
@@ -2,17 +2,19 @@
package moe.nea.firmament.apis
-import io.ktor.client.request.*
-import io.ktor.client.statement.*
-import io.ktor.http.*
+import io.ktor.client.request.get
+import io.ktor.client.request.header
+import io.ktor.client.statement.HttpResponse
+import io.ktor.client.statement.bodyAsText
+import io.ktor.http.appendPathSegments
+import java.time.Duration
+import java.time.Instant
+import java.util.UUID
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.sync.Mutex
import kotlinx.coroutines.withContext
-import moe.nea.firmament.Firmament
import net.minecraft.client.MinecraftClient
-import java.time.Duration
-import java.time.Instant
-import java.util.*
+import moe.nea.firmament.Firmament
object UrsaManager {
private data class Token(
diff --git a/src/main/kotlin/commands/Duration.kt b/src/main/kotlin/commands/Duration.kt
new file mode 100644
index 0000000..58ce5d8
--- /dev/null
+++ b/src/main/kotlin/commands/Duration.kt
@@ -0,0 +1,74 @@
+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 kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import kotlin.time.DurationUnit
+import kotlin.time.toDuration
+import moe.nea.firmament.util.tr
+
+object DurationArgumentType : ArgumentType<Duration> {
+ val unknownTimeCode = DynamicCommandExceptionType { timeCode ->
+ tr("firmament.command-argument.duration.error",
+ "Unknown time code '$timeCode'")
+ }
+
+ override fun parse(reader: StringReader): Duration {
+ val start = reader.cursor
+ val string = reader.readUnquotedString()
+ val matcher = regex.matcher(string)
+ var s = 0
+ var time = 0.seconds
+ fun createError(till: Int) {
+ throw unknownTimeCode.createWithContext(
+ reader.also { it.cursor = start + s },
+ string.substring(s, till))
+ }
+
+ while (matcher.find()) {
+ if (matcher.start() != s) {
+ createError(matcher.start())
+ }
+ s = matcher.end()
+ val amount = matcher.group("count").toDouble()
+ val what = timeSuffixes[matcher.group("what").single()]!!
+ time += amount.toDuration(what)
+ }
+ if (string.length != s) {
+ createError(string.length)
+ }
+ return time
+ }
+
+
+ override fun <S : Any?> listSuggestions(
+ context: CommandContext<S>,
+ builder: SuggestionsBuilder
+ ): CompletableFuture<Suggestions> {
+ val remaining = builder.remainingLowerCase.substringBefore(' ')
+ if (remaining.isEmpty()) return super.listSuggestions(context, builder)
+ if (remaining.last().isDigit()) {
+ for (timeSuffix in timeSuffixes.keys) {
+ builder.suggest(remaining + timeSuffix)
+ }
+ }
+ return builder.buildFuture()
+ }
+
+ val timeSuffixes = mapOf(
+ 'm' to DurationUnit.MINUTES,
+ 's' to DurationUnit.SECONDS,
+ 'h' to DurationUnit.HOURS,
+ )
+ val regex = "(?<count>[0-9]+)(?<what>[${timeSuffixes.keys.joinToString("")}])".toPattern()
+
+ override fun getExamples(): Collection<String> {
+ return listOf("3m", "20s", "1h45m")
+ }
+}
diff --git a/src/main/kotlin/commands/dsl.kt b/src/main/kotlin/commands/dsl.kt
index d1f0d8c..c5955d7 100644
--- a/src/main/kotlin/commands/dsl.kt
+++ b/src/main/kotlin/commands/dsl.kt
@@ -7,14 +7,14 @@ import com.mojang.brigadier.builder.ArgumentBuilder
import com.mojang.brigadier.builder.RequiredArgumentBuilder
import com.mojang.brigadier.context.CommandContext
import com.mojang.brigadier.suggestion.SuggestionProvider
+import java.lang.reflect.ParameterizedType
+import java.lang.reflect.Type
+import java.lang.reflect.TypeVariable
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
import kotlinx.coroutines.launch
import moe.nea.firmament.Firmament
import moe.nea.firmament.util.MinecraftDispatcher
import moe.nea.firmament.util.iterate
-import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
-import java.lang.reflect.ParameterizedType
-import java.lang.reflect.Type
-import java.lang.reflect.TypeVariable
typealias DefaultSource = FabricClientCommandSource
diff --git a/src/main/kotlin/commands/rome.kt b/src/main/kotlin/commands/rome.kt
index 13acb3c..b1b2aa2 100644
--- a/src/main/kotlin/commands/rome.kt
+++ b/src/main/kotlin/commands/rome.kt
@@ -1,6 +1,7 @@
package moe.nea.firmament.commands
import com.mojang.brigadier.CommandDispatcher
+import com.mojang.brigadier.arguments.IntegerArgumentType
import com.mojang.brigadier.arguments.StringArgumentType.string
import io.ktor.client.statement.bodyAsText
import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
@@ -11,13 +12,14 @@ import moe.nea.firmament.apis.UrsaManager
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.FirmamentEventBus
import moe.nea.firmament.features.debug.DebugLogger
+import moe.nea.firmament.features.debug.DeveloperFeatures
import moe.nea.firmament.features.debug.PowerUserTools
import moe.nea.firmament.features.inventory.buttons.InventoryButtons
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverviewScreen
+import moe.nea.firmament.features.mining.MiningBlockInfoUi
import moe.nea.firmament.gui.config.AllConfigsGui
import moe.nea.firmament.gui.config.BooleanHandler
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.gui.config.ManagedOption
import moe.nea.firmament.init.MixinPlugin
import moe.nea.firmament.repo.HypixelStaticData
@@ -32,8 +34,10 @@ import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.ScreenUtil
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.accessors.messages
+import moe.nea.firmament.util.asBazaarStock
import moe.nea.firmament.util.collections.InstanceList
import moe.nea.firmament.util.collections.WeakCache
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.mc.SNbtFormatter
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.unformattedString
@@ -86,7 +90,7 @@ fun firmamentCommand() = literal("firmament") {
}
propertyObj as ManagedOption<Boolean>
propertyObj.value = !propertyObj.value
- configObj.save()
+ configObj.markDirty()
source.sendFeedback(
Text.stringifiedTranslatable(
"firmament.command.toggle.toggled", configObj.labelText,
@@ -129,6 +133,15 @@ fun firmamentCommand() = literal("firmament") {
}
}
thenLiteral("repo") {
+ thenLiteral("checkpr") {
+ thenArgument("prnum", IntegerArgumentType.integer(1)) { prnum ->
+ thenExecute {
+ val prnum = this[prnum]
+ source.sendFeedback(tr("firmament.repo.reload.pr", "Temporarily reloading repo from PR #${prnum}."))
+ RepoManager.downloadOverridenBranch("refs/pull/$prnum/head")
+ }
+ }
+ }
thenLiteral("reload") {
thenLiteral("fetch") {
thenExecute {
@@ -148,7 +161,7 @@ fun firmamentCommand() = literal("firmament") {
thenExecute {
val itemName = SkyblockId(get(item))
source.sendFeedback(Text.stringifiedTranslatable("firmament.price", itemName.neuItem))
- val bazaarData = HypixelStaticData.bazaarData[itemName]
+ val bazaarData = HypixelStaticData.bazaarData[itemName.asBazaarStock]
if (bazaarData != null) {
source.sendFeedback(Text.translatable("firmament.price.bazaar"))
source.sendFeedback(
@@ -191,7 +204,7 @@ fun firmamentCommand() = literal("firmament") {
}
}
}
- thenLiteral("dev") {
+ thenLiteral(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
thenLiteral("simulate") {
thenArgument("message", RestArgumentType) { message ->
thenExecute {
@@ -217,6 +230,20 @@ fun firmamentCommand() = literal("firmament") {
}
}
}
+ thenLiteral("screens") {
+ thenExecute {
+ MC.sendChat(Text.literal("""
+ |Screen: ${MC.screen} (${MC.screen?.title})
+ |Screen Handler: ${MC.handledScreen?.screenHandler} ${MC.handledScreen?.screenHandler?.syncId}
+ |Player Screen Handler: ${MC.player?.currentScreenHandler} ${MC.player?.currentScreenHandler?.syncId}
+ """.trimMargin()))
+ }
+ }
+ thenLiteral("blocks") {
+ thenExecute {
+ ScreenUtil.setScreenLater(MiningBlockInfoUi.makeScreen())
+ }
+ }
thenLiteral("dumpchat") {
thenExecute {
MC.inGameHud.chatHud.messages.forEach {
@@ -246,6 +273,8 @@ fun firmamentCommand() = literal("firmament") {
source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.gametype", locrawInfo.gametype))
source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.mode", locrawInfo.mode))
source.sendFeedback(Text.stringifiedTranslatable("firmament.sbinfo.map", locrawInfo.map))
+ source.sendFeedback(tr("firmament.sbinfo.custommining",
+ "Custom Mining: ${formatBool(locrawInfo.skyblockLocation?.hasCustomMining ?: false)}"))
}
}
}
@@ -254,6 +283,7 @@ fun firmamentCommand() = literal("firmament") {
val player = MC.player ?: return@thenExecute
player.world.getOtherEntities(player, player.boundingBox.expand(12.0))
.forEach(PowerUserTools::showEntity)
+ PowerUserTools.showEntity(player)
}
}
thenLiteral("callUrsa") {
@@ -295,13 +325,15 @@ fun firmamentCommand() = literal("firmament") {
}
thenLiteral("mixins") {
thenExecute {
- source.sendFeedback(Text.translatable("firmament.mixins.start"))
- MixinPlugin.appliedMixins
- .map { it.removePrefix(MixinPlugin.mixinPackage) }
- .forEach {
- source.sendFeedback(Text.literal(" - ").withColor(0xD020F0)
- .append(Text.literal(it).withColor(0xF6BA20)))
- }
+ MixinPlugin.instances.forEach { plugin ->
+ source.sendFeedback(tr("firmament.mixins.start.package", "Mixins (base ${plugin.mixinPackage}):"))
+ plugin.appliedMixins
+ .map { it.removePrefix(plugin.mixinPackage) }
+ .forEach {
+ source.sendFeedback(Text.literal(" - ").withColor(0xD020F0)
+ .append(Text.literal(it).withColor(0xF6BA20)))
+ }
+ }
}
}
thenLiteral("repo") {
diff --git a/src/main/kotlin/events/AllowChatEvent.kt b/src/main/kotlin/events/AllowChatEvent.kt
index 3069843..a1b4828 100644
--- a/src/main/kotlin/events/AllowChatEvent.kt
+++ b/src/main/kotlin/events/AllowChatEvent.kt
@@ -2,8 +2,8 @@
package moe.nea.firmament.events
-import moe.nea.firmament.util.unformattedString
import net.minecraft.text.Text
+import moe.nea.firmament.util.unformattedString
/**
* Filter whether the user should see a chat message altogether. May or may not be called for every chat packet sent by
diff --git a/src/main/kotlin/events/BakeExtraModelsEvent.kt b/src/main/kotlin/events/BakeExtraModelsEvent.kt
deleted file mode 100644
index adaa495..0000000
--- a/src/main/kotlin/events/BakeExtraModelsEvent.kt
+++ /dev/null
@@ -1,24 +0,0 @@
-package moe.nea.firmament.events
-
-import java.util.function.BiConsumer
-import net.minecraft.client.render.model.ReferencedModelsCollector
-import net.minecraft.client.util.ModelIdentifier
-import net.minecraft.util.Identifier
-
-// TODO: Rename this event, since it is not really directly baking models anymore
-class BakeExtraModelsEvent(
- private val addAnyModel: BiConsumer<ModelIdentifier, Identifier>,
-) : FirmamentEvent() {
-
- fun addNonItemModel(modelIdentifier: ModelIdentifier, identifier: Identifier) {
- this.addAnyModel.accept(modelIdentifier, identifier)
- }
-
- fun addItemModel(modelIdentifier: ModelIdentifier) {
- addNonItemModel(
- modelIdentifier,
- modelIdentifier.id.withPrefixedPath(ReferencedModelsCollector.ITEM_DIRECTORY))
- }
-
- companion object : FirmamentEventBus<BakeExtraModelsEvent>()
-}
diff --git a/src/main/kotlin/events/CustomItemModelEvent.kt b/src/main/kotlin/events/CustomItemModelEvent.kt
index 4328d77..7b86980 100644
--- a/src/main/kotlin/events/CustomItemModelEvent.kt
+++ b/src/main/kotlin/events/CustomItemModelEvent.kt
@@ -1,38 +1,75 @@
package moe.nea.firmament.events
+import java.util.Objects
import java.util.Optional
import kotlin.jvm.optionals.getOrNull
-import net.minecraft.client.render.item.ItemModels
-import net.minecraft.client.render.model.BakedModel
-import net.minecraft.client.util.ModelIdentifier
+import net.minecraft.component.DataComponentTypes
import net.minecraft.item.ItemStack
-import moe.nea.firmament.util.ErrorUtil
+import net.minecraft.util.Identifier
import moe.nea.firmament.util.collections.WeakCache
+import moe.nea.firmament.util.collections.WeakCache.CacheFunction
+import moe.nea.firmament.util.mc.IntrospectableItemModelManager
+// TODO: assert an order on these events
data class CustomItemModelEvent(
val itemStack: ItemStack,
- var overrideModel: ModelIdentifier? = null,
+ val itemModelManager: IntrospectableItemModelManager,
+ var overrideModel: Identifier? = null,
) : FirmamentEvent() {
companion object : FirmamentEventBus<CustomItemModelEvent>() {
- val cache =
- WeakCache.memoize<ItemStack, ItemModels, Optional<BakedModel>>("CustomItemModels") { stack, models ->
- val modelId = getModelIdentifier(stack) ?: return@memoize Optional.empty()
- ErrorUtil.softCheck("Model Id needs to have an inventory variant", modelId.variant() == "inventory")
- val bakedModel = models.getModel(modelId.id)
- if (bakedModel == null || bakedModel === models.missingModelSupplier.get()) return@memoize Optional.empty()
- Optional.of(bakedModel)
+ val weakCache =
+ object : WeakCache<ItemStack, IntrospectableItemModelManager, Optional<Identifier>>("ItemModelIdentifier") {
+ override fun mkRef(
+ key: ItemStack,
+ extraData: IntrospectableItemModelManager
+ ): WeakCache<ItemStack, IntrospectableItemModelManager, Optional<Identifier>>.Ref {
+ return IRef(key, extraData)
+ }
+
+ inner class IRef(weakInstance: ItemStack, data: IntrospectableItemModelManager) :
+ Ref(weakInstance, data) {
+ override fun shouldBeEvicted(): Boolean = false
+ val isSimpleStack = weakInstance.componentChanges.isEmpty || (weakInstance.componentChanges.size() == 1 && weakInstance.get(
+ DataComponentTypes.CUSTOM_DATA)?.isEmpty == true)
+ val item = weakInstance.item
+ override fun hashCode(): Int {
+ if (isSimpleStack)
+ return Objects.hash(item, extraData)
+ return super.hashCode()
+ }
+
+ override fun equals(other: Any?): Boolean {
+ if (other is IRef && isSimpleStack) {
+ return other.isSimpleStack && item == other.item
+ }
+ return super.equals(other)
+ }
+ }
}
+ val cache = CacheFunction.WithExtraData(weakCache, ::getModelIdentifier0)
@JvmStatic
- fun getModelIdentifier(itemStack: ItemStack?): ModelIdentifier? {
+ fun getModelIdentifier(itemStack: ItemStack?, itemModelManager: IntrospectableItemModelManager): Identifier? {
if (itemStack == null) return null
- return publish(CustomItemModelEvent(itemStack)).overrideModel
+ return cache.invoke(itemStack, itemModelManager).getOrNull()
}
- @JvmStatic
- fun getModel(itemStack: ItemStack?, thing: ItemModels): BakedModel? {
- if (itemStack == null) return null
- return cache.invoke(itemStack, thing).getOrNull()
+ fun getModelIdentifier0(
+ itemStack: ItemStack,
+ itemModelManager: IntrospectableItemModelManager
+ ): Optional<Identifier> {
+ // TODO: add an error / warning if the model does not exist
+ return Optional.ofNullable(publish(CustomItemModelEvent(itemStack, itemModelManager)).overrideModel)
}
}
+
+ fun overrideIfExists(overrideModel: Identifier) {
+ if (itemModelManager.hasModel_firmament(overrideModel))
+ this.overrideModel = overrideModel
+ }
+
+ fun overrideIfEmpty(identifier: Identifier) {
+ if (overrideModel == null)
+ overrideModel = identifier
+ }
}
diff --git a/src/main/kotlin/events/EntityRenderTintEvent.kt b/src/main/kotlin/events/EntityRenderTintEvent.kt
new file mode 100644
index 0000000..54cbff2
--- /dev/null
+++ b/src/main/kotlin/events/EntityRenderTintEvent.kt
@@ -0,0 +1,67 @@
+package moe.nea.firmament.events
+
+import net.minecraft.client.render.GameRenderer
+import net.minecraft.client.render.OverlayTexture
+import net.minecraft.client.render.entity.state.EntityRenderState
+import net.minecraft.entity.Entity
+import net.minecraft.entity.LivingEntity
+import moe.nea.firmament.events.EntityRenderTintEvent.Companion.overlayOverride
+import moe.nea.firmament.util.render.TintedOverlayTexture
+
+/**
+ * Change the tint color of a [LivingEntity]
+ */
+class EntityRenderTintEvent(
+ val entity: Entity,
+ val renderState: HasTintRenderState
+) : FirmamentEvent.Cancellable() {
+ init {
+ if (entity !is LivingEntity) {
+ cancel()
+ }
+ }
+
+ companion object : FirmamentEventBus<EntityRenderTintEvent>() {
+ /**
+ * Static variable containing an override for [GameRenderer.getOverlayTexture]. Should be only set briefly.
+ *
+ * This variable only affects render layers that naturally make use of the overlay texture, have proper overlay UVs set (`overlay u != 0`), and have a shader that makes use of the overlay (does not have the `NO_OVERLAY` flag set in its json definition).
+ *
+ * Currently supported layers: [net.minecraft.client.render.entity.equipment.EquipmentRenderer], [net.minecraft.client.render.entity.model.PlayerEntityModel], as well as some others naturally.
+ *
+ * @see moe.nea.firmament.mixins.render.entitytints.ReplaceOverlayTexture
+ * @see TintedOverlayTexture
+ */
+ @JvmField
+ var overlayOverride: OverlayTexture? = null
+ }
+
+ @Suppress("PropertyName", "FunctionName")
+ interface HasTintRenderState {
+ /**
+ * Multiplicative tint applied before the overlay.
+ */
+ var tint_firmament: Int
+
+ /**
+ * Must be set for [tint_firmament] to have any effect.
+ */
+ var hasTintOverride_firmament: Boolean
+
+ // TODO: allow for more specific selection of which layers get tinted
+ /**
+ * Specify a [TintedOverlayTexture] to be used. This does not apply to render layers not using the overlay texture.
+ * @see overlayOverride
+ */
+ var overlayTexture_firmament: TintedOverlayTexture?
+ fun reset_firmament()
+
+ companion object {
+ @JvmStatic
+ fun cast(state: EntityRenderState): HasTintRenderState {
+ return state as HasTintRenderState
+ }
+ }
+ }
+
+}
diff --git a/src/main/kotlin/events/EntityUpdateEvent.kt b/src/main/kotlin/events/EntityUpdateEvent.kt
index d091984..fec2fa5 100644
--- a/src/main/kotlin/events/EntityUpdateEvent.kt
+++ b/src/main/kotlin/events/EntityUpdateEvent.kt
@@ -1,10 +1,14 @@
-
package moe.nea.firmament.events
+import com.mojang.datafixers.util.Pair
import net.minecraft.entity.Entity
+import net.minecraft.entity.EquipmentSlot
import net.minecraft.entity.LivingEntity
import net.minecraft.entity.data.DataTracker
+import net.minecraft.item.ItemStack
import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.util.MC
/**
* This event is fired when some entity properties are updated.
@@ -13,19 +17,44 @@ import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket
* *after* the values have been applied to the entity.
*/
sealed class EntityUpdateEvent : FirmamentEvent() {
- companion object : FirmamentEventBus<EntityUpdateEvent>()
+ companion object : FirmamentEventBus<EntityUpdateEvent>() {
+ @Subscribe
+ fun onPlayerInventoryUpdate(event: PlayerInventoryUpdate) {
+ val p = MC.player ?: return
+ val updatedSlots = listOf(
+ EquipmentSlot.HEAD to 39,
+ EquipmentSlot.CHEST to 38,
+ EquipmentSlot.LEGS to 37,
+ EquipmentSlot.FEET to 36,
+ EquipmentSlot.OFFHAND to 40,
+ EquipmentSlot.MAINHAND to p.inventory.selectedSlot, // TODO: also equipment update when you swap your selected slot perhaps
+ ).mapNotNull { (slot, stackIndex) ->
+ val slotIndex = p.playerScreenHandler.getSlotIndex(p.inventory, stackIndex).asInt
+ event.getOrNull(slotIndex)?.let {
+ Pair.of(slot, it)
+ }
+ }
+ if (updatedSlots.isNotEmpty())
+ publish(EquipmentUpdate(p, updatedSlots))
+ }
+ }
+
+ abstract val entity: Entity
- abstract val entity: Entity
+ data class AttributeUpdate(
+ override val entity: LivingEntity,
+ val attributes: List<EntityAttributesS2CPacket.Entry>,
+ ) : EntityUpdateEvent()
- data class AttributeUpdate(
- override val entity: LivingEntity,
- val attributes: List<EntityAttributesS2CPacket.Entry>,
- ) : EntityUpdateEvent()
+ data class TrackedDataUpdate(
+ override val entity: Entity,
+ val trackedValues: List<DataTracker.SerializedEntry<*>>,
+ ) : EntityUpdateEvent()
- data class TrackedDataUpdate(
- override val entity: Entity,
- val trackedValues: List<DataTracker.SerializedEntry<*>>,
- ) : EntityUpdateEvent()
+ data class EquipmentUpdate(
+ override val entity: Entity,
+ val newEquipment: List<Pair<EquipmentSlot, ItemStack>>,
+ ) : EntityUpdateEvent()
-// TODO: onEntityPassengersSet, onEntityAttach?, onEntityEquipmentUpdate, onEntityStatusEffect
+// TODO: onEntityPassengersSet, onEntityAttach?, onEntityStatusEffect
}
diff --git a/src/main/kotlin/events/FeaturesInitializedEvent.kt b/src/main/kotlin/events/FeaturesInitializedEvent.kt
deleted file mode 100644
index ad2ad8a..0000000
--- a/src/main/kotlin/events/FeaturesInitializedEvent.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-
-package moe.nea.firmament.events
-
-import moe.nea.firmament.features.FirmamentFeature
-
-data class FeaturesInitializedEvent(val features: List<FirmamentFeature>) : FirmamentEvent() {
- companion object : FirmamentEventBus<FeaturesInitializedEvent>()
-}
diff --git a/src/main/kotlin/events/FirmamentEventBus.kt b/src/main/kotlin/events/FirmamentEventBus.kt
index 71331d1..af4e16a 100644
--- a/src/main/kotlin/events/FirmamentEventBus.kt
+++ b/src/main/kotlin/events/FirmamentEventBus.kt
@@ -3,6 +3,7 @@ package moe.nea.firmament.events
import java.util.concurrent.CopyOnWriteArrayList
import org.apache.commons.lang3.reflect.TypeUtils
import moe.nea.firmament.Firmament
+import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.MC
/**
@@ -48,7 +49,7 @@ open class FirmamentEventBus<T : FirmamentEvent> {
val klass = e.javaClass
if (!function.knownErrors.contains(klass) || Firmament.DEBUG) {
function.knownErrors.add(klass)
- Firmament.logger.error("Caught exception during processing event $event by $function", e)
+ ErrorUtil.softError("Caught exception during processing event $event by $function", e)
}
}
}
diff --git a/src/main/kotlin/events/HandledScreenKeyPressedEvent.kt b/src/main/kotlin/events/HandledScreenKeyPressedEvent.kt
index 183ec71..34aebe6 100644
--- a/src/main/kotlin/events/HandledScreenKeyPressedEvent.kt
+++ b/src/main/kotlin/events/HandledScreenKeyPressedEvent.kt
@@ -1,38 +1,39 @@
package moe.nea.firmament.events
+import org.lwjgl.glfw.GLFW
import net.minecraft.client.gui.screen.ingame.HandledScreen
-import net.minecraft.client.option.KeyBinding
-import moe.nea.firmament.keybindings.IKeyBinding
+import moe.nea.firmament.keybindings.GenericInputAction
+import moe.nea.firmament.keybindings.InputModifiers
+import moe.nea.firmament.keybindings.SavedKeyBinding
-sealed interface HandledScreenKeyEvent {
+sealed interface HandledScreenInputEvent {
val screen: HandledScreen<*>
- val keyCode: Int
- val scanCode: Int
- val modifiers: Int
-
- fun matches(keyBinding: KeyBinding): Boolean {
- return matches(IKeyBinding.minecraft(keyBinding))
- }
-
- fun matches(keyBinding: IKeyBinding): Boolean {
- return keyBinding.matches(keyCode, scanCode, modifiers)
- }
+ val input: GenericInputAction
+ val modifiers: InputModifiers
}
data class HandledScreenKeyPressedEvent(
override val screen: HandledScreen<*>,
- override val keyCode: Int,
- override val scanCode: Int,
- override val modifiers: Int
-) : FirmamentEvent.Cancellable(), HandledScreenKeyEvent {
+ override val input: GenericInputAction,
+ override val modifiers: InputModifiers,
+ // TODO: val isRepeat: Boolean,
+) : FirmamentEvent.Cancellable(), HandledScreenInputEvent {
+ fun matches(keyBinding: SavedKeyBinding, atLeast: Boolean = false): Boolean {
+ return keyBinding.matches(input, modifiers, atLeast)
+ }
+
+ fun isLeftClick() = input == GenericInputAction.mouse(GLFW.GLFW_MOUSE_BUTTON_LEFT)
companion object : FirmamentEventBus<HandledScreenKeyPressedEvent>()
}
data class HandledScreenKeyReleasedEvent(
override val screen: HandledScreen<*>,
- override val keyCode: Int,
- override val scanCode: Int,
- override val modifiers: Int
-) : FirmamentEvent.Cancellable(), HandledScreenKeyEvent {
+ override val input: GenericInputAction,
+ override val modifiers: InputModifiers,
+) : FirmamentEvent.Cancellable(), HandledScreenInputEvent {
+ fun matches(keyBinding: SavedKeyBinding, atLeast: Boolean = false): Boolean {
+ return keyBinding.matches(input, modifiers, atLeast)
+ }
+
companion object : FirmamentEventBus<HandledScreenKeyReleasedEvent>()
}
diff --git a/src/main/kotlin/events/IsSlotProtectedEvent.kt b/src/main/kotlin/events/IsSlotProtectedEvent.kt
index cd2b676..6ab0174 100644
--- a/src/main/kotlin/events/IsSlotProtectedEvent.kt
+++ b/src/main/kotlin/events/IsSlotProtectedEvent.kt
@@ -1,46 +1,64 @@
-
-
package moe.nea.firmament.events
import net.minecraft.item.ItemStack
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
-import net.minecraft.text.Text
import moe.nea.firmament.util.CommonSoundEffects
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.grey
+import moe.nea.firmament.util.hover
+import moe.nea.firmament.util.red
+import moe.nea.firmament.util.tr
data class IsSlotProtectedEvent(
- val slot: Slot?,
- val actionType: SlotActionType,
- var isProtected: Boolean,
- val itemStackOverride: ItemStack?,
- var silent: Boolean = false,
+ val slot: Slot?,
+ val actionType: SlotActionType,
+ var isProtected: Boolean,
+ val itemStackOverride: ItemStack?,
+ val origin: MoveOrigin,
+ var silent: Boolean = false,
) : FirmamentEvent() {
- val itemStack get() = itemStackOverride ?: slot!!.stack
+ val itemStack get() = itemStackOverride ?: slot!!.stack
+
+ fun protect() {
+ isProtected = true
+ silent = false
+ }
- fun protect() {
- isProtected = true
- }
+ fun protectSilent() {
+ if (!isProtected) {
+ silent = true
+ }
+ isProtected = true
+ }
- fun protectSilent() {
- if (!isProtected) {
- silent = true
- }
- isProtected = true
- }
+ enum class MoveOrigin {
+ DROP_FROM_HOTBAR,
+ SALVAGE,
+ INVENTORY_MOVE
+ ;
+ }
- companion object : FirmamentEventBus<IsSlotProtectedEvent>() {
- @JvmStatic
- @JvmOverloads
- fun shouldBlockInteraction(slot: Slot?, action: SlotActionType, itemStackOverride: ItemStack? = null): Boolean {
- if (slot == null && itemStackOverride == null) return false
- val event = IsSlotProtectedEvent(slot, action, false, itemStackOverride)
- publish(event)
- if (event.isProtected && !event.silent) {
- MC.sendChat(Text.translatable("firmament.protectitem").append(event.itemStack.name))
- CommonSoundEffects.playFailure()
- }
- return event.isProtected
- }
- }
+ companion object : FirmamentEventBus<IsSlotProtectedEvent>() {
+ @JvmStatic
+ @JvmOverloads
+ fun shouldBlockInteraction(
+ slot: Slot?, action: SlotActionType,
+ origin: MoveOrigin,
+ itemStackOverride: ItemStack? = null,
+ ): Boolean {
+ if (slot == null && itemStackOverride == null) return false
+ val event = IsSlotProtectedEvent(slot, action, false, itemStackOverride, origin)
+ publish(event)
+ if (event.isProtected && !event.silent) {
+ MC.sendChat(tr("firmament.protectitem", "Firmament protected your item: ${event.itemStack.name}.\n")
+ .red()
+ .append(tr("firmament.protectitem.hoverhint", "Hover for more info.").grey())
+ .hover(tr("firmament.protectitem.hint",
+ "To unlock this item use the Lock Slot or Lock Item keybind from Firmament while hovering over this item.")))
+ CommonSoundEffects.playFailure()
+ }
+ return event.isProtected
+ }
+ }
}
diff --git a/src/main/kotlin/events/JoinServerEvent.kt b/src/main/kotlin/events/JoinServerEvent.kt
new file mode 100644
index 0000000..225a2c1
--- /dev/null
+++ b/src/main/kotlin/events/JoinServerEvent.kt
@@ -0,0 +1,11 @@
+package moe.nea.firmament.events
+
+import net.fabricmc.fabric.api.networking.v1.PacketSender
+import net.minecraft.client.network.ClientPlayNetworkHandler
+
+data class JoinServerEvent(
+ val networkHandler: ClientPlayNetworkHandler,
+ val packetSender: PacketSender,
+) : FirmamentEvent() {
+ companion object : FirmamentEventBus<JoinServerEvent>()
+}
diff --git a/src/main/kotlin/events/ModifyChatEvent.kt b/src/main/kotlin/events/ModifyChatEvent.kt
index a5868e8..4a7025c 100644
--- a/src/main/kotlin/events/ModifyChatEvent.kt
+++ b/src/main/kotlin/events/ModifyChatEvent.kt
@@ -2,8 +2,8 @@
package moe.nea.firmament.events
-import moe.nea.firmament.util.unformattedString
import net.minecraft.text.Text
+import moe.nea.firmament.util.unformattedString
/**
* Allow modification of a chat message before it is sent off to the user. Intended for display purposes.
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<PartyMessageReceivedEvent>()
+}
diff --git a/src/main/kotlin/events/PlayerInventoryUpdate.kt b/src/main/kotlin/events/PlayerInventoryUpdate.kt
index 6e8203a..88439a9 100644
--- a/src/main/kotlin/events/PlayerInventoryUpdate.kt
+++ b/src/main/kotlin/events/PlayerInventoryUpdate.kt
@@ -1,11 +1,22 @@
-
package moe.nea.firmament.events
import net.minecraft.item.ItemStack
sealed class PlayerInventoryUpdate : FirmamentEvent() {
- companion object : FirmamentEventBus<PlayerInventoryUpdate>()
- data class Single(val slot: Int, val stack: ItemStack) : PlayerInventoryUpdate()
- data class Multi(val contents: List<ItemStack>) : PlayerInventoryUpdate()
+ companion object : FirmamentEventBus<PlayerInventoryUpdate>()
+ data class Single(val slot: Int, val stack: ItemStack) : PlayerInventoryUpdate() {
+ override fun getOrNull(slot: Int): ItemStack? {
+ if (slot == this.slot) return stack
+ return null
+ }
+
+ }
+
+ data class Multi(val contents: List<ItemStack>) : PlayerInventoryUpdate() {
+ override fun getOrNull(slot: Int): ItemStack? {
+ return contents.getOrNull(slot)
+ }
+ }
+ abstract fun getOrNull(slot: Int): ItemStack?
}
diff --git a/src/main/kotlin/events/SlotRenderEvents.kt b/src/main/kotlin/events/SlotRenderEvents.kt
index 5234176..9076d53 100644
--- a/src/main/kotlin/events/SlotRenderEvents.kt
+++ b/src/main/kotlin/events/SlotRenderEvents.kt
@@ -3,11 +3,8 @@
package moe.nea.firmament.events
import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.render.RenderLayer
-import net.minecraft.client.texture.Sprite
import net.minecraft.screen.slot.Slot
import net.minecraft.util.Identifier
-import moe.nea.firmament.util.MC
import moe.nea.firmament.util.render.drawGuiTexture
interface SlotRenderEvents {
diff --git a/src/main/kotlin/events/UseItemEvent.kt b/src/main/kotlin/events/UseItemEvent.kt
new file mode 100644
index 0000000..e294bb1
--- /dev/null
+++ b/src/main/kotlin/events/UseItemEvent.kt
@@ -0,0 +1,11 @@
+package moe.nea.firmament.events
+
+import net.minecraft.entity.player.PlayerEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.util.Hand
+import net.minecraft.world.World
+
+data class UseItemEvent(val playerEntity: PlayerEntity, val world: World, val hand: Hand) : FirmamentEvent.Cancellable() {
+ companion object : FirmamentEventBus<UseItemEvent>()
+ val item: ItemStack = playerEntity.getStackInHand(hand)
+}
diff --git a/src/main/kotlin/events/WorldKeyboardEvent.kt b/src/main/kotlin/events/WorldKeyboardEvent.kt
index e8566fd..860db5c 100644
--- a/src/main/kotlin/events/WorldKeyboardEvent.kt
+++ b/src/main/kotlin/events/WorldKeyboardEvent.kt
@@ -1,18 +1,13 @@
-
-
package moe.nea.firmament.events
-import net.minecraft.client.option.KeyBinding
-import moe.nea.firmament.keybindings.IKeyBinding
-
-data class WorldKeyboardEvent(val keyCode: Int, val scanCode: Int, val modifiers: Int) : FirmamentEvent.Cancellable() {
- companion object : FirmamentEventBus<WorldKeyboardEvent>()
+import moe.nea.firmament.keybindings.GenericInputAction
+import moe.nea.firmament.keybindings.InputModifiers
+import moe.nea.firmament.keybindings.SavedKeyBinding
- fun matches(keyBinding: KeyBinding): Boolean {
- return matches(IKeyBinding.minecraft(keyBinding))
- }
+data class WorldKeyboardEvent(val action: GenericInputAction, val modifiers: InputModifiers) : FirmamentEvent.Cancellable() {
+ fun matches(keyBinding: SavedKeyBinding, atLeast: Boolean = false): Boolean {
+ return keyBinding.matches(action, modifiers, atLeast)
+ }
- fun matches(keyBinding: IKeyBinding): Boolean {
- return keyBinding.matches(keyCode, scanCode, modifiers)
- }
+ companion object : FirmamentEventBus<WorldKeyboardEvent>()
}
diff --git a/src/main/kotlin/events/WorldMouseMoveEvent.kt b/src/main/kotlin/events/WorldMouseMoveEvent.kt
new file mode 100644
index 0000000..7a17ba4
--- /dev/null
+++ b/src/main/kotlin/events/WorldMouseMoveEvent.kt
@@ -0,0 +1,5 @@
+package moe.nea.firmament.events
+
+data class WorldMouseMoveEvent(val deltaX: Double, val deltaY: Double) : FirmamentEvent.Cancellable() {
+ companion object : FirmamentEventBus<WorldMouseMoveEvent>()
+}
diff --git a/src/main/kotlin/events/WorldRenderLastEvent.kt b/src/main/kotlin/events/WorldRenderLastEvent.kt
index 3c2103d..93d7e8c 100644
--- a/src/main/kotlin/events/WorldRenderLastEvent.kt
+++ b/src/main/kotlin/events/WorldRenderLastEvent.kt
@@ -3,13 +3,9 @@
package moe.nea.firmament.events
import net.minecraft.client.render.Camera
-import net.minecraft.client.render.GameRenderer
-import net.minecraft.client.render.LightmapTextureManager
import net.minecraft.client.render.RenderTickCounter
import net.minecraft.client.render.VertexConsumerProvider
import net.minecraft.client.util.math.MatrixStack
-import net.minecraft.util.math.Position
-import net.minecraft.util.math.Vec3d
/**
* This event is called after all world rendering is done, but before any GUI rendering (including hand) has been done.
diff --git a/src/main/kotlin/events/registration/ChatEvents.kt b/src/main/kotlin/events/registration/ChatEvents.kt
index 4c1c63f..7fe040a 100644
--- a/src/main/kotlin/events/registration/ChatEvents.kt
+++ b/src/main/kotlin/events/registration/ChatEvents.kt
@@ -1,54 +1,68 @@
-
-
package moe.nea.firmament.events.registration
import net.fabricmc.fabric.api.client.message.v1.ClientReceiveMessageEvents
+import net.fabricmc.fabric.api.client.networking.v1.ClientPlayConnectionEvents
import net.fabricmc.fabric.api.event.player.AttackBlockCallback
import net.fabricmc.fabric.api.event.player.UseBlockCallback
+import net.fabricmc.fabric.api.event.player.UseItemCallback
import net.minecraft.text.Text
import net.minecraft.util.ActionResult
import moe.nea.firmament.events.AllowChatEvent
import moe.nea.firmament.events.AttackBlockEvent
+import moe.nea.firmament.events.JoinServerEvent
import moe.nea.firmament.events.ModifyChatEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.UseBlockEvent
+import moe.nea.firmament.events.UseItemEvent
private var lastReceivedMessage: Text? = null
fun registerFirmamentEvents() {
- ClientReceiveMessageEvents.ALLOW_CHAT.register(ClientReceiveMessageEvents.AllowChat { message, signedMessage, sender, params, receptionTimestamp ->
- lastReceivedMessage = message
- !ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled
- && !AllowChatEvent.publish(AllowChatEvent(message)).cancelled
- })
- ClientReceiveMessageEvents.ALLOW_GAME.register(ClientReceiveMessageEvents.AllowGame { message, overlay ->
- lastReceivedMessage = message
- overlay || (!ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled &&
- !AllowChatEvent.publish(AllowChatEvent(message)).cancelled)
- })
- ClientReceiveMessageEvents.MODIFY_GAME.register(ClientReceiveMessageEvents.ModifyGame { message, overlay ->
- if (overlay) message
- else ModifyChatEvent.publish(ModifyChatEvent(message)).replaceWith
- })
- ClientReceiveMessageEvents.GAME_CANCELED.register(ClientReceiveMessageEvents.GameCanceled { message, overlay ->
- if (!overlay && lastReceivedMessage !== message) {
- ProcessChatEvent.publish(ProcessChatEvent(message, true))
- }
- })
- ClientReceiveMessageEvents.CHAT_CANCELED.register(ClientReceiveMessageEvents.ChatCanceled { message, signedMessage, sender, params, receptionTimestamp ->
- if (lastReceivedMessage !== message) {
- ProcessChatEvent.publish(ProcessChatEvent(message, true))
- }
- })
+ ClientReceiveMessageEvents.ALLOW_CHAT.register(ClientReceiveMessageEvents.AllowChat { message, signedMessage, sender, params, receptionTimestamp ->
+ lastReceivedMessage = message
+ !ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled
+ && !AllowChatEvent.publish(AllowChatEvent(message)).cancelled
+ })
+ ClientReceiveMessageEvents.ALLOW_GAME.register(ClientReceiveMessageEvents.AllowGame { message, overlay ->
+ lastReceivedMessage = message
+ overlay || (!ProcessChatEvent.publish(ProcessChatEvent(message, false)).cancelled &&
+ !AllowChatEvent.publish(AllowChatEvent(message)).cancelled)
+ })
+ ClientReceiveMessageEvents.MODIFY_GAME.register(ClientReceiveMessageEvents.ModifyGame { message, overlay ->
+ if (overlay) message
+ else ModifyChatEvent.publish(ModifyChatEvent(message)).replaceWith
+ })
+ ClientReceiveMessageEvents.GAME_CANCELED.register(ClientReceiveMessageEvents.GameCanceled { message, overlay ->
+ if (!overlay && lastReceivedMessage !== message) {
+ ProcessChatEvent.publish(ProcessChatEvent(message, true))
+ }
+ })
+ ClientReceiveMessageEvents.CHAT_CANCELED.register(ClientReceiveMessageEvents.ChatCanceled { message, signedMessage, sender, params, receptionTimestamp ->
+ if (lastReceivedMessage !== message) {
+ ProcessChatEvent.publish(ProcessChatEvent(message, true))
+ }
+ })
- AttackBlockCallback.EVENT.register(AttackBlockCallback { player, world, hand, pos, direction ->
- if (AttackBlockEvent.publish(AttackBlockEvent(player, world, hand, pos, direction)).cancelled)
- ActionResult.CONSUME
- else ActionResult.PASS
- })
- UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult ->
- if (UseBlockEvent.publish(UseBlockEvent(player, world, hand, hitResult)).cancelled)
- ActionResult.CONSUME
- else ActionResult.PASS
- })
+ AttackBlockCallback.EVENT.register(AttackBlockCallback { player, world, hand, pos, direction ->
+ if (AttackBlockEvent.publish(AttackBlockEvent(player, world, hand, pos, direction)).cancelled)
+ ActionResult.FAIL
+ else ActionResult.PASS
+ })
+ UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult ->
+ if (UseBlockEvent.publish(UseBlockEvent(player, world, hand, hitResult)).cancelled)
+ ActionResult.FAIL
+ else ActionResult.PASS
+ })
+ UseBlockCallback.EVENT.register(UseBlockCallback { player, world, hand, hitResult ->
+ if (UseItemEvent.publish(UseItemEvent(player, world, hand)).cancelled)
+ ActionResult.FAIL
+ else ActionResult.PASS
+ })
+ UseItemCallback.EVENT.register(UseItemCallback { playerEntity, world, hand ->
+ if (UseItemEvent.publish(UseItemEvent(playerEntity, world, hand)).cancelled) ActionResult.FAIL
+ else ActionResult.PASS
+ })
+ ClientPlayConnectionEvents.JOIN.register { networkHandler, packetSender, _ ->
+ JoinServerEvent.publish(JoinServerEvent(networkHandler, packetSender))
+ }
}
diff --git a/src/main/kotlin/events/subscription/Subscription.kt b/src/main/kotlin/events/subscription/Subscription.kt
index 1c1d3bd..812da92 100644
--- a/src/main/kotlin/events/subscription/Subscription.kt
+++ b/src/main/kotlin/events/subscription/Subscription.kt
@@ -3,11 +3,7 @@ package moe.nea.firmament.events.subscription
import moe.nea.firmament.events.FirmamentEvent
import moe.nea.firmament.events.FirmamentEventBus
-import moe.nea.firmament.features.FirmamentFeature
-interface SubscriptionOwner {
- val delegateFeature: FirmamentFeature
-}
data class Subscription<T : FirmamentEvent>(
val owner: Any,
diff --git a/src/main/kotlin/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt
index 2110d09..d474d65 100644
--- a/src/main/kotlin/features/FeatureManager.kt
+++ b/src/main/kotlin/features/FeatureManager.kt
@@ -1,125 +1,25 @@
package moe.nea.firmament.features
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.serializer
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.events.FeaturesInitializedEvent
import moe.nea.firmament.events.FirmamentEvent
import moe.nea.firmament.events.subscription.Subscription
import moe.nea.firmament.events.subscription.SubscriptionList
-import moe.nea.firmament.features.chat.AutoCompletions
-import moe.nea.firmament.features.chat.ChatLinks
-import moe.nea.firmament.features.chat.QuickCommands
-import moe.nea.firmament.features.debug.DebugView
-import moe.nea.firmament.features.debug.DeveloperFeatures
-import moe.nea.firmament.features.debug.MinorTrolling
-import moe.nea.firmament.features.debug.PowerUserTools
-import moe.nea.firmament.features.diana.DianaWaypoints
-import moe.nea.firmament.features.events.anniversity.AnniversaryFeatures
-import moe.nea.firmament.features.events.carnival.CarnivalFeatures
-import moe.nea.firmament.features.fixes.CompatibliltyFeatures
-import moe.nea.firmament.features.fixes.Fixes
-import moe.nea.firmament.features.inventory.CraftingOverlay
-import moe.nea.firmament.features.inventory.ItemRarityCosmetics
-import moe.nea.firmament.features.inventory.PetFeatures
-import moe.nea.firmament.features.inventory.PriceData
-import moe.nea.firmament.features.inventory.SaveCursorPosition
-import moe.nea.firmament.features.inventory.SlotLocking
-import moe.nea.firmament.features.inventory.buttons.InventoryButtons
-import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlay
-import moe.nea.firmament.features.mining.PickaxeAbility
-import moe.nea.firmament.features.mining.PristineProfitTracker
-import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures
-import moe.nea.firmament.features.world.FairySouls
-import moe.nea.firmament.features.world.Waypoints
-import moe.nea.firmament.util.data.DataHolder
-
-object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "features", ::Config) {
- @Serializable
- data class Config(
- val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf()
- )
-
- private val features = mutableMapOf<String, FirmamentFeature>()
-
- val allFeatures: Collection<FirmamentFeature> get() = features.values
-
- private var hasAutoloaded = false
-
- init {
- autoload()
- }
-
- fun autoload() {
- synchronized(this) {
- if (hasAutoloaded) return
- loadFeature(MinorTrolling)
- loadFeature(FairySouls)
- loadFeature(AutoCompletions)
- // TODO: loadFeature(FishingWarning)
- loadFeature(SlotLocking)
- loadFeature(StorageOverlay)
- loadFeature(PristineProfitTracker)
- loadFeature(CraftingOverlay)
- loadFeature(PowerUserTools)
- loadFeature(Waypoints)
- loadFeature(ChatLinks)
- loadFeature(InventoryButtons)
- loadFeature(CompatibliltyFeatures)
- loadFeature(AnniversaryFeatures)
- loadFeature(QuickCommands)
- loadFeature(PetFeatures)
- loadFeature(SaveCursorPosition)
- loadFeature(CustomSkyBlockTextures)
- loadFeature(PriceData)
- loadFeature(Fixes)
- loadFeature(DianaWaypoints)
- loadFeature(ItemRarityCosmetics)
- loadFeature(PickaxeAbility)
- loadFeature(CarnivalFeatures)
- if (Firmament.DEBUG) {
- loadFeature(DeveloperFeatures)
- loadFeature(DebugView)
- }
- allFeatures.forEach { it.config }
- FeaturesInitializedEvent.publish(FeaturesInitializedEvent(allFeatures.toList()))
- hasAutoloaded = true
- }
- }
-
- fun subscribeEvents() {
- SubscriptionList.allLists.forEach {
- it.provideSubscriptions {
- it.owner.javaClass.classes.forEach {
- runCatching { it.getDeclaredField("INSTANCE").get(null) }
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.compatloader.ICompatMeta
+
+object FeatureManager {
+
+ fun subscribeEvents() {
+ SubscriptionList.allLists.forEach { list ->
+ if (ICompatMeta.shouldLoad(list.javaClass.name))
+ ErrorUtil.catch("Error while loading events from $list") {
+ list.provideSubscriptions {
+ subscribeSingleEvent(it)
+ }
}
- subscribeSingleEvent(it)
- }
- }
- }
-
- private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) {
- it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke)
- }
-
- fun loadFeature(feature: FirmamentFeature) {
- synchronized(features) {
- if (feature.identifier in features) {
- Firmament.logger.error("Double registering feature ${feature.identifier}. Ignoring second instance $feature")
- return
- }
- features[feature.identifier] = feature
- feature.onLoad()
- }
- }
-
- fun isEnabled(identifier: String): Boolean? =
- data.enabledFeatures[identifier]
-
-
- fun setEnabled(identifier: String, value: Boolean) {
- data.enabledFeatures[identifier] = value
- markDirty()
- }
+ }
+ }
+ private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) {
+ it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke)
+ }
}
diff --git a/src/main/kotlin/features/FirmamentFeature.kt b/src/main/kotlin/features/FirmamentFeature.kt
deleted file mode 100644
index 2cfc4fd..0000000
--- a/src/main/kotlin/features/FirmamentFeature.kt
+++ /dev/null
@@ -1,23 +0,0 @@
-
-
-package moe.nea.firmament.features
-
-import moe.nea.firmament.events.subscription.SubscriptionOwner
-import moe.nea.firmament.gui.config.ManagedConfig
-
-// TODO: remove this entire feature system and revamp config
-interface FirmamentFeature : SubscriptionOwner {
- val identifier: String
- val defaultEnabled: Boolean
- get() = true
- var isEnabled: Boolean
- get() = FeatureManager.isEnabled(identifier) ?: defaultEnabled
- set(value) {
- FeatureManager.setEnabled(identifier, value)
- }
- override val delegateFeature: FirmamentFeature
- get() = this
- val config: ManagedConfig? get() = null
- fun onLoad() {}
-
-}
diff --git a/src/main/kotlin/features/chat/AutoCompletions.kt b/src/main/kotlin/features/chat/AutoCompletions.kt
index 9e0de40..b48069d 100644
--- a/src/main/kotlin/features/chat/AutoCompletions.kt
+++ b/src/main/kotlin/features/chat/AutoCompletions.kt
@@ -8,21 +8,20 @@ import moe.nea.firmament.commands.thenArgument
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.MaskCommands
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
-object AutoCompletions : FirmamentFeature {
+object AutoCompletions {
+ @Config
object TConfig : ManagedConfig(identifier, Category.CHAT) {
val provideWarpTabCompletion by toggle("warp-complete") { true }
val replaceWarpIsByWarpIsland by toggle("warp-is") { true }
}
- override val config: ManagedConfig?
- get() = TConfig
- override val identifier: String
+ val identifier: String
get() = "auto-completions"
@Subscribe
@@ -44,9 +43,9 @@ object AutoCompletions : FirmamentFeature {
thenExecute {
val warpName = get(toArg)
if (warpName == "is" && TConfig.replaceWarpIsByWarpIsland) {
- MC.sendServerCommand("warp island")
+ MC.sendCommand("warp island")
} else {
- MC.sendServerCommand("warp $warpName")
+ MC.sendCommand("warp $warpName")
}
}
}
diff --git a/src/main/kotlin/features/chat/ChatLinks.kt b/src/main/kotlin/features/chat/ChatLinks.kt
index 5bce3f4..b05a3a0 100644
--- a/src/main/kotlin/features/chat/ChatLinks.kt
+++ b/src/main/kotlin/features/chat/ChatLinks.kt
@@ -1,22 +1,20 @@
-
-
package moe.nea.firmament.features.chat
import io.ktor.client.request.get
import io.ktor.client.statement.bodyAsChannel
import io.ktor.utils.io.jvm.javaio.toInputStream
+import java.net.URI
import java.net.URL
import java.util.Collections
-import moe.nea.jarvis.api.Point
+import java.util.concurrent.atomic.AtomicInteger
+import org.joml.Vector2i
import kotlinx.coroutines.Deferred
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.async
import kotlin.math.min
import net.minecraft.client.gui.screen.ChatScreen
-import net.minecraft.client.render.RenderLayer
import net.minecraft.client.texture.NativeImage
import net.minecraft.client.texture.NativeImageBackedTexture
-import net.minecraft.scoreboard.ScoreboardCriterion.RenderType
import net.minecraft.text.ClickEvent
import net.minecraft.text.HoverEvent
import net.minecraft.text.Style
@@ -27,138 +25,142 @@ import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ModifyChatEvent
import moe.nea.firmament.events.ScreenRenderPostEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.jarvis.JarvisIntegration
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.render.drawTexture
import moe.nea.firmament.util.transformEachRecursively
import moe.nea.firmament.util.unformattedString
-object ChatLinks : FirmamentFeature {
- override val identifier: String
- get() = "chat-links"
+object ChatLinks {
+ val identifier: String
+ get() = "chat-links"
- object TConfig : ManagedConfig(identifier, Category.CHAT) {
- val enableLinks by toggle("links-enabled") { true }
- val imageEnabled by toggle("image-enabled") { true }
- val allowAllHosts by toggle("allow-all-hosts") { false }
- val allowedHosts by string("allowed-hosts") { "cdn.discordapp.com,media.discordapp.com,media.discordapp.net,i.imgur.com" }
- val actualAllowedHosts get() = allowedHosts.split(",").map { it.trim() }
- val position by position("position", 16 * 20, 9 * 20) { Point(0.0, 0.0) }
- }
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.CHAT) {
+ val enableLinks by toggle("links-enabled") { true }
+ val imageEnabled by toggle("image-enabled") { true }
+ val allowAllHosts by toggle("allow-all-hosts") { false }
+ val allowedHosts by string("allowed-hosts") { "cdn.discordapp.com,media.discordapp.com,media.discordapp.net,i.imgur.com" }
+ val actualAllowedHosts get() = allowedHosts.split(",").map { it.trim() }
+ val position by position("position", 16 * 20, 9 * 20) { Vector2i(0, 0) }
+ }
- private fun isHostAllowed(host: String) =
- TConfig.allowAllHosts || TConfig.actualAllowedHosts.any { it.equals(host, ignoreCase = true) }
+ private fun isHostAllowed(host: String) =
+ TConfig.allowAllHosts || TConfig.actualAllowedHosts.any { it.equals(host, ignoreCase = true) }
- private fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/"))
+ private fun isUrlAllowed(url: String) = isHostAllowed(url.removePrefix("https://").substringBefore("/"))
- override val config get() = TConfig
- val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?( |$))".toRegex()
+ val urlRegex = "https://[^. ]+\\.[^ ]+(\\.?(\\s|$))".toRegex()
+ val nextTexId = AtomicInteger(0)
- data class Image(
- val texture: Identifier,
- val width: Int,
- val height: Int,
- )
+ data class Image(
+ val texture: Identifier,
+ val width: Int,
+ val height: Int,
+ )
- val imageCache: MutableMap<String, Deferred<Image?>> =
- Collections.synchronizedMap(mutableMapOf<String, Deferred<Image?>>())
+ val imageCache: MutableMap<String, Deferred<Image?>> =
+ Collections.synchronizedMap(mutableMapOf<String, Deferred<Image?>>())
- private fun tryCacheUrl(url: String) {
- if (!isUrlAllowed(url)) {
- return
- }
- if (url in imageCache) {
- return
- }
- imageCache[url] = Firmament.coroutineScope.async {
- try {
- val response = Firmament.httpClient.get(URL(url))
- if (response.status.value == 200) {
- val inputStream = response.bodyAsChannel().toInputStream(Firmament.globalJob)
- val image = NativeImage.read(inputStream)
- val texture = MC.textureManager.registerDynamicTexture(
- "dynamic_image_preview",
- NativeImageBackedTexture(image)
- )
- Image(texture, image.width, image.height)
- } else
- null
- } catch (exc: Exception) {
- exc.printStackTrace()
- null
- }
- }
- }
+ private fun tryCacheUrl(url: String) {
+ if (!isUrlAllowed(url)) {
+ return
+ }
+ if (url in imageCache) {
+ return
+ }
+ imageCache[url] = Firmament.coroutineScope.async {
+ try {
+ val response = Firmament.httpClient.get(URI.create(url).toURL())
+ if (response.status.value == 200) {
+ val inputStream = response.bodyAsChannel().toInputStream(Firmament.globalJob)
+ val image = NativeImage.read(inputStream)
+ val texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}")
+ MC.textureManager.registerTexture(
+ texId,
+ NativeImageBackedTexture({ texId.path }, image)
+ )
+ Image(texId, image.width, image.height)
+ } else
+ null
+ } catch (exc: Exception) {
+ exc.printStackTrace()
+ null
+ }
+ }
+ }
- val imageExtensions = listOf("jpg", "png", "gif", "jpeg")
- fun isImageUrl(url: String): Boolean {
- return (url.substringAfterLast('.').lowercase() in imageExtensions)
- }
+ val imageExtensions = listOf("jpg", "png", "gif", "jpeg")
+ fun isImageUrl(url: String): Boolean {
+ return (url.substringAfterLast('.').lowercase() in imageExtensions)
+ }
- @Subscribe
- @OptIn(ExperimentalCoroutinesApi::class)
- fun onRender(it: ScreenRenderPostEvent) {
- if (!TConfig.imageEnabled) return
- if (it.screen !is ChatScreen) return
- val hoveredComponent =
- MC.inGameHud.chatHud.getTextStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: return
- val hoverEvent = hoveredComponent.hoverEvent ?: return
- val value = hoverEvent.getValue(HoverEvent.Action.SHOW_TEXT) ?: return
- val url = urlRegex.matchEntire(value.unformattedString)?.groupValues?.get(0) ?: return
- if (!isImageUrl(url)) return
- val imageFuture = imageCache[url] ?: return
- if (!imageFuture.isCompleted) return
- val image = imageFuture.getCompleted() ?: return
- it.drawContext.matrices.push()
- val pos = TConfig.position
- pos.applyTransformations(it.drawContext.matrices)
- val scale = min(1F, min((9 * 20F) / image.height, (16 * 20F) / image.width))
- it.drawContext.matrices.scale(scale, scale, 1F)
- it.drawContext.drawTexture(
- image.texture,
- 0,
- 0,
- 1F,
- 1F,
- image.width,
- image.height,
- image.width,
- image.height,
- )
- it.drawContext.matrices.pop()
- }
+ @Subscribe
+ @OptIn(ExperimentalCoroutinesApi::class)
+ fun onRender(it: ScreenRenderPostEvent) {
+ if (!TConfig.imageEnabled) return
+ if (it.screen !is ChatScreen) return
+ val hoveredComponent =
+ MC.inGameHud.chatHud.getTextStyleAt(it.mouseX.toDouble(), it.mouseY.toDouble()) ?: return
+ val hoverEvent = hoveredComponent.hoverEvent as? HoverEvent.ShowText ?: return
+ val value = hoverEvent.value
+ val url = urlRegex.matchEntire(value.unformattedString)?.groupValues?.get(0) ?: return
+ if (!isImageUrl(url)) return
+ val imageFuture = imageCache[url] ?: return
+ if (!imageFuture.isCompleted) return
+ val image = imageFuture.getCompleted() ?: return
+ it.drawContext.matrices.pushMatrix()
+ val pos = TConfig.position
+ pos.applyTransformations(JarvisIntegration.jarvis, it.drawContext.matrices)
+ val scale = min(1F, min((9 * 20F) / image.height, (16 * 20F) / image.width))
+ it.drawContext.matrices.scale(scale, scale)
+ it.drawContext.drawTexture(
+ image.texture,
+ 0,
+ 0,
+ 1F,
+ 1F,
+ image.width,
+ image.height,
+ image.width,
+ image.height,
+ )
+ it.drawContext.matrices.popMatrix()
+ }
- @Subscribe
- fun onModifyChat(it: ModifyChatEvent) {
- if (!TConfig.enableLinks) return
- it.replaceWith = it.replaceWith.transformEachRecursively { child ->
- val text = child.string
- if ("://" !in text) return@transformEachRecursively child
- val s = Text.empty().setStyle(child.style)
- var index = 0
- while (index < text.length) {
- val nextMatch = urlRegex.find(text, index)
- if (nextMatch == null) {
- s.append(Text.literal(text.substring(index, text.length)))
- break
- }
- val range = nextMatch.groups[0]!!.range
- val url = nextMatch.groupValues[0]
- s.append(Text.literal(text.substring(index, range.first)))
- s.append(
- Text.literal(url).setStyle(
- Style.EMPTY.withUnderline(true).withColor(
- Formatting.AQUA
- ).withHoverEvent(HoverEvent(HoverEvent.Action.SHOW_TEXT, Text.literal(url)))
- .withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, url))
- )
- )
- if (isImageUrl(url))
- tryCacheUrl(url)
- index = range.last + 1
- }
- s
- }
- }
+ @Subscribe
+ fun onModifyChat(it: ModifyChatEvent) {
+ if (!TConfig.enableLinks) return
+ it.replaceWith = it.replaceWith.transformEachRecursively { child ->
+ val text = child.string
+ if ("://" !in text) return@transformEachRecursively child
+ val s = Text.empty().setStyle(child.style)
+ var index = 0
+ while (index < text.length) {
+ val nextMatch = urlRegex.find(text, index)
+ val url = nextMatch?.groupValues[0]
+ val uri = runCatching { url?.let(::URI) }.getOrNull()
+ if (nextMatch == null || url == null || uri == null) {
+ s.append(Text.literal(text.substring(index, text.length)))
+ break
+ }
+ val range = nextMatch.groups[0]!!.range
+ s.append(Text.literal(text.substring(index, range.first)))
+ s.append(
+ Text.literal(url).setStyle(
+ Style.EMPTY.withUnderline(true).withColor(
+ Formatting.AQUA
+ ).withHoverEvent(HoverEvent.ShowText(Text.literal(url)))
+ .withClickEvent(ClickEvent.OpenUrl(uri))
+ )
+ )
+ if (isImageUrl(url))
+ tryCacheUrl(url)
+ index = range.last + 1
+ }
+ s
+ }
+ }
}
diff --git a/src/main/kotlin/features/chat/CopyChat.kt b/src/main/kotlin/features/chat/CopyChat.kt
new file mode 100644
index 0000000..5c46465
--- /dev/null
+++ b/src/main/kotlin/features/chat/CopyChat.kt
@@ -0,0 +1,21 @@
+package moe.nea.firmament.features.chat
+
+import net.minecraft.text.OrderedText
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.reconstitute
+
+
+object CopyChat {
+ val identifier: String
+ get() = "copy-chat"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.CHAT) {
+ val copyChat by toggle("copy-chat") { false }
+ }
+
+ fun orderedTextToString(orderedText: OrderedText): String {
+ return orderedText.reconstitute().string
+ }
+}
diff --git a/src/main/kotlin/features/chat/PartyCommands.kt b/src/main/kotlin/features/chat/PartyCommands.kt
new file mode 100644
index 0000000..1b34946
--- /dev/null
+++ b/src/main/kotlin/features/chat/PartyCommands.kt
@@ -0,0 +1,136 @@
+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.util.ErrorUtil
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.tr
+import moe.nea.firmament.util.useMatch
+
+object PartyCommands {
+
+ val messageInChannel = "(?<channel>Party|Guild) >([^:]+?)? (?<name>[^: ]+): (?<message>.+)".toPattern()
+
+ @Subscribe
+ fun onChat(event: ProcessChatEvent) {
+ messageInChannel.useMatch(event.unformattedString) {
+ val channel = group("channel")
+ val message = group("message")
+ val name = group("name")
+ if (channel == "Party") {
+ PartyMessageReceivedEvent.publish(PartyMessageReceivedEvent(
+ event, message, name
+ ))
+ }
+ }
+ }
+
+ val commandPrefixes = "!-?$.&#+~€\"@°_;:³²`'´ß\\,|".toSet()
+
+ data class PartyCommandContext(
+ val name: String
+ )
+
+ val dispatch = CommandDispatcher<PartyCommandContext>().also { dispatch ->
+ fun register(
+ name: String,
+ vararg alias: String,
+ block: CaseInsensitiveLiteralCommandNode.Builder<PartyCommandContext>.() -> Unit = {},
+ ): LiteralCommandNode<PartyCommandContext> {
+ val node =
+ dispatch.register(CaseInsensitiveLiteralCommandNode.Builder<PartyCommandContext>(name).also(block))
+ alias.forEach { register(it) { redirect(node) } }
+ return node
+ }
+
+ register("warp", "pw", "pwarp", "partywarp") {
+ executes {
+ // TODO: add check if you are the party leader
+ MC.sendCommand("p warp")
+ 0
+ }
+ }
+
+ register("transfer", "pt", "ptme") {
+ executes {
+ MC.sendCommand("p transfer ${it.source.name}")
+ 0
+ }
+ }
+
+ register("allinvite", "allinv") {
+ executes {
+ MC.sendCommand("p settings allinvite")
+ 0
+ }
+ }
+
+ register("coords") {
+ executes {
+ val p = MC.player?.blockPos ?: BlockPos.ORIGIN
+ MC.sendCommand("pc x: ${p.x}, y: ${p.y}, z: ${p.z}")
+ 0
+ }
+ }
+ // TODO: downtime tracker (display message again at end of dungeon)
+ // instance ends: kuudra, dungeons, bacte
+ // TODO: at TPS command
+ }
+
+ @Config
+ 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/features/chat/QuickCommands.kt b/src/main/kotlin/features/chat/QuickCommands.kt
index 5944b92..5221205 100644
--- a/src/main/kotlin/features/chat/QuickCommands.kt
+++ b/src/main/kotlin/features/chat/QuickCommands.kt
@@ -1,8 +1,12 @@
-
-
package moe.nea.firmament.features.chat
+import com.mojang.brigadier.CommandDispatcher
import com.mojang.brigadier.context.CommandContext
+import net.fabricmc.fabric.api.client.command.v2.ClientCommandRegistrationCallback
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
+import net.fabricmc.fabric.impl.command.client.ClientCommandInternals
+import net.minecraft.command.CommandRegistryAccess
+import net.minecraft.network.packet.s2c.play.CommandTreeS2CPacket
import net.minecraft.text.Text
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.DefaultSource
@@ -11,90 +15,157 @@ 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.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedOption
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.grey
+import moe.nea.firmament.util.tr
+
+object QuickCommands {
+ val identifier: String
+ get() = "quick-commands"
+
+ @Config
+ object TConfig : ManagedConfig("quick-commands", Category.CHAT) {
+ val enableJoin by toggle("join") { true }
+ val enableDh by toggle("dh") { true }
+ override fun onChange(option: ManagedOption<*>) {
+ reloadCommands()
+ }
+ }
+
+ fun reloadCommands() {
+ val lastPacket = lastReceivedTreePacket ?: return
+ val network = MC.networkHandler ?: return
+ val fallback = ClientCommandInternals.getActiveDispatcher()
+ try {
+ val dispatcher = CommandDispatcher<FabricClientCommandSource>()
+ ClientCommandInternals.setActiveDispatcher(dispatcher)
+ ClientCommandRegistrationCallback.EVENT.invoker()
+ .register(
+ dispatcher, CommandRegistryAccess.of(
+ network.combinedDynamicRegistries,
+ network.enabledFeatures
+ )
+ )
+ ClientCommandInternals.finalizeInit()
+ network.onCommandTree(lastPacket)
+ } catch (ex: Exception) {
+ ClientCommandInternals.setActiveDispatcher(fallback)
+ throw ex
+ }
+ }
+
+
+ fun removePartialPrefix(text: String, prefix: String): String? {
+ var lf: String? = null
+ for (i in 1..prefix.length) {
+ if (text.startsWith(prefix.substring(0, i))) {
+ lf = text.substring(i)
+ }
+ }
+ return lf
+ }
-object QuickCommands : FirmamentFeature {
- override val identifier: String
- get() = "quick-commands"
+ var lastReceivedTreePacket: CommandTreeS2CPacket? = null
- fun removePartialPrefix(text: String, prefix: String): String? {
- var lf: String? = null
- for (i in 1..prefix.length) {
- if (text.startsWith(prefix.substring(0, i))) {
- lf = text.substring(i)
- }
- }
- return lf
- }
+ val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL")
+ val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN")
- val kuudraLevelNames = listOf("NORMAL", "HOT", "BURNING", "FIERY", "INFERNAL")
- val dungeonLevelNames = listOf("ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN")
+ @Subscribe
+ fun registerDh(event: CommandEvent) {
+ if (!TConfig.enableDh) return
+ event.register("dh") {
+ thenExecute {
+ MC.sendCommand("warp dhub")
+ }
+ }
+ event.register("dn") {
+ thenExecute {
+ MC.sendChat(tr("firmament.quickwarp.deez-nutz", "Warping to... Deez Nuts!").grey())
+ MC.sendCommand("warp dhub")
+ }
+ }
+ }
- @Subscribe
- fun onCommands(it: CommandEvent) {
- it.register("join") {
- thenArgument("what", RestArgumentType) { what ->
- thenExecute {
- val what = this[what]
- if (!SBData.isOnSkyblock) {
- MC.sendCommand("join $what")
- return@thenExecute
- }
- val joinName = getNameForFloor(what.replace(" ", "").lowercase())
- if (joinName == null) {
- source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what))
- } else {
- source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.success",
- joinName))
- MC.sendCommand("joininstance $joinName")
- }
- }
- }
- thenExecute {
- source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain"))
- }
- }
- }
+ @Subscribe
+ fun registerJoin(it: CommandEvent) {
+ if (!TConfig.enableJoin) return
+ it.register("join") {
+ thenArgument("what", RestArgumentType) { what ->
+ thenExecute {
+ val what = this[what]
+ if (!SBData.isOnSkyblock) {
+ MC.sendCommand("join $what")
+ return@thenExecute
+ }
+ val joinName = getNameForFloor(what.replace(" ", "").lowercase())
+ if (joinName == null) {
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown", what))
+ } else {
+ source.sendFeedback(
+ Text.stringifiedTranslatable(
+ "firmament.quick-commands.join.success",
+ joinName
+ )
+ )
+ MC.sendCommand("joininstance $joinName")
+ }
+ }
+ }
+ thenExecute {
+ source.sendFeedback(Text.translatable("firmament.quick-commands.join.explain"))
+ }
+ }
+ }
- fun CommandContext<DefaultSource>.getNameForFloor(w: String): String? {
- val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier")
- if (kuudraLevel != null) {
- val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst {
- it.startsWith(
- kuudraLevel,
- true
- )
- }
- if (l !in kuudraLevelNames.indices) {
- source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-kuudra",
- kuudraLevel))
- return null
- }
- return "KUUDRA_${kuudraLevelNames[l]}"
- }
- val masterLevel = removePartialPrefix(w, "master")
- val normalLevel =
- removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons")
- val dungeonLevel = masterLevel ?: normalLevel
- if (dungeonLevel != null) {
- val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst {
- it.startsWith(
- dungeonLevel,
- true
- )
- }
- if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) {
- return "CATACOMBS_ENTRANCE"
- }
- if (l !in dungeonLevelNames.indices) {
- source.sendFeedback(Text.stringifiedTranslatable("firmament.quick-commands.join.unknown-catacombs",
- kuudraLevel))
- return null
- }
- return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}"
- }
- return null
- }
+ fun CommandContext<DefaultSource>.getNameForFloor(w: String): String? {
+ val kuudraLevel = removePartialPrefix(w, "kuudratier") ?: removePartialPrefix(w, "tier")
+ if (kuudraLevel != null) {
+ val l = kuudraLevel.toIntOrNull()?.let { it - 1 } ?: kuudraLevelNames.indexOfFirst {
+ it.startsWith(
+ kuudraLevel,
+ true
+ )
+ }
+ if (l !in kuudraLevelNames.indices) {
+ source.sendFeedback(
+ Text.stringifiedTranslatable(
+ "firmament.quick-commands.join.unknown-kuudra",
+ kuudraLevel
+ )
+ )
+ return null
+ }
+ return "KUUDRA_${kuudraLevelNames[l]}"
+ }
+ val masterLevel = removePartialPrefix(w, "master")
+ val normalLevel =
+ removePartialPrefix(w, "floor") ?: removePartialPrefix(w, "catacombs") ?: removePartialPrefix(w, "dungeons")
+ val dungeonLevel = masterLevel ?: normalLevel
+ if (dungeonLevel != null) {
+ val l = dungeonLevel.toIntOrNull()?.let { it - 1 } ?: dungeonLevelNames.indexOfFirst {
+ it.startsWith(
+ dungeonLevel,
+ true
+ )
+ }
+ if (masterLevel == null && (l == -1 || null != removePartialPrefix(w, "entrance"))) {
+ return "CATACOMBS_ENTRANCE"
+ }
+ if (l !in dungeonLevelNames.indices) {
+ source.sendFeedback(
+ Text.stringifiedTranslatable(
+ "firmament.quick-commands.join.unknown-catacombs",
+ kuudraLevel
+ )
+ )
+ return null
+ }
+ return "${if (masterLevel != null) "MASTER_" else ""}CATACOMBS_FLOOR_${dungeonLevelNames[l]}"
+ }
+ return null
+ }
}
diff --git a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
new file mode 100644
index 0000000..4edccfb
--- /dev/null
+++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
@@ -0,0 +1,193 @@
+package moe.nea.firmament.features.debug
+
+import net.minecraft.command.argument.RegistryKeyArgumentType
+import net.minecraft.component.ComponentType
+import net.minecraft.entity.Entity
+import net.minecraft.entity.decoration.ArmorStandEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtOps
+import net.minecraft.registry.RegistryKeys
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.get
+import moe.nea.firmament.commands.thenArgument
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.EntityUpdateEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.math.GChainReconciliation
+import moe.nea.firmament.util.math.GChainReconciliation.shortenCycle
+import moe.nea.firmament.util.mc.NbtPrism
+import moe.nea.firmament.util.tr
+
+object AnimatedClothingScanner {
+
+ data class LensOfFashionTheft<T>(
+ val prism: NbtPrism,
+ val component: ComponentType<T>,
+ ) {
+ fun observe(itemStack: ItemStack): Collection<NbtElement> {
+ val x = itemStack.get(component) ?: return listOf()
+ val nbt = component.codecOrThrow.encodeStart(NbtOps.INSTANCE, x).orThrow
+ return prism.access(nbt)
+ }
+ }
+
+ var lens: LensOfFashionTheft<*>? = null
+ var subject: Entity? = null
+ var history: MutableList<String> = mutableListOf()
+ val metaHistory: MutableList<List<String>> = mutableListOf()
+
+ @OptIn(ExperimentalStdlibApi::class)
+ @Subscribe
+ fun onUpdate(event: EntityUpdateEvent) {
+ val s = subject ?: return
+ if (event.entity != s) return
+ val l = lens ?: return
+ if (event is EntityUpdateEvent.EquipmentUpdate) {
+ event.newEquipment.forEach {
+ val formatted = (l.observe(it.second)).joinToString()
+ history.add(formatted)
+ // TODO: add a slot filter
+ }
+ }
+ }
+
+ fun reduceHistory(reducer: (List<String>, List<String>) -> List<String>): List<String> {
+ return metaHistory.fold(history, reducer).shortenCycle()
+ }
+
+ @Subscribe
+ fun onSubCommand(event: CommandEvent.SubCommand) {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
+ thenLiteral("stealthisfit") {
+ thenLiteral("clear") {
+ thenExecute {
+ subject = null
+ metaHistory.clear()
+ history.clear()
+ MC.sendChat(tr("firmament.fitstealer.clear", "Cleared fit stealing history"))
+ }
+ }
+ thenLiteral("copy") {
+ thenExecute {
+ val history = reduceHistory { a, b -> a + b }
+ copyHistory(history)
+ MC.sendChat(tr("firmament.fitstealer.copied", "Copied the history"))
+ }
+ thenLiteral("deduplicated") {
+ thenExecute {
+ val history = reduceHistory { a, b ->
+ (a.toMutableSet() + b).toList()
+ }
+ copyHistory(history)
+ MC.sendChat(
+ tr(
+ "firmament.fitstealer.copied.deduplicated",
+ "Copied the deduplicated history"
+ )
+ )
+ }
+ }
+ thenLiteral("merged") {
+ thenExecute {
+ val history = reduceHistory(GChainReconciliation::reconcileCycles)
+ copyHistory(history)
+ MC.sendChat(tr("firmament.fitstealer.copied.merged", "Copied the merged history"))
+ }
+ }
+ }
+ thenLiteral("target") {
+ thenLiteral("self") {
+ thenExecute {
+ toggleObserve(MC.player!!)
+ }
+ }
+ thenLiteral("pet") {
+ thenExecute {
+ source.sendFeedback(
+ tr(
+ "firmament.fitstealer.stealingpet",
+ "Observing nearest marker armourstand"
+ )
+ )
+ val p = MC.player!!
+ val nearestPet = p.world.getEntitiesByClass(
+ ArmorStandEntity::class.java,
+ p.boundingBox.expand(10.0),
+ { it.isMarker })
+ .minBy { it.squaredDistanceTo(p) }
+ toggleObserve(nearestPet)
+ }
+ }
+ thenExecute {
+ val ent = MC.instance.targetedEntity
+ if (ent == null) {
+ source.sendFeedback(
+ tr(
+ "firmament.fitstealer.notargetundercursor",
+ "No entity under cursor"
+ )
+ )
+ } else {
+ toggleObserve(ent)
+ }
+ }
+ }
+ thenLiteral("path") {
+ thenArgument(
+ "component",
+ RegistryKeyArgumentType.registryKey(RegistryKeys.DATA_COMPONENT_TYPE)
+ ) { component ->
+ thenArgument("path", NbtPrism.Argument) { path ->
+ thenExecute {
+ lens = LensOfFashionTheft(
+ get(path),
+ MC.unsafeGetRegistryEntry(get(component))!!,
+ )
+ source.sendFeedback(
+ tr(
+ "firmament.fitstealer.lensset",
+ "Analyzing path ${get(path)} for component ${get(component).value}"
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ private fun copyHistory(toCopy: List<String>) {
+ ClipboardUtils.setTextContent(toCopy.joinToString("\n"))
+ }
+
+ @Subscribe
+ fun onWorldSwap(event: WorldReadyEvent) {
+ subject = null
+ if (history.isNotEmpty()) {
+ metaHistory.add(history)
+ history = mutableListOf()
+ }
+ }
+
+ private fun toggleObserve(entity: Entity?) {
+ subject = if (subject == null) entity else null
+ if (subject == null) {
+ metaHistory.add(history)
+ history = mutableListOf()
+ }
+ MC.sendChat(
+ subject?.let {
+ tr(
+ "firmament.fitstealer.targeted",
+ "Observing the equipment of ${it.name}."
+ )
+ } ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."),
+ )
+ }
+}
diff --git a/src/main/kotlin/features/debug/DebugLogger.kt b/src/main/kotlin/features/debug/DebugLogger.kt
index 2c6b962..02073b7 100644
--- a/src/main/kotlin/features/debug/DebugLogger.kt
+++ b/src/main/kotlin/features/debug/DebugLogger.kt
@@ -4,19 +4,23 @@ import kotlinx.serialization.serializer
import net.minecraft.text.Text
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.collections.InstanceList
+import moe.nea.firmament.util.data.Config
import moe.nea.firmament.util.data.DataHolder
class DebugLogger(val tag: String) {
companion object {
val allInstances = InstanceList<DebugLogger>("DebugLogger")
}
+
+ @Config
object EnabledLogs : DataHolder<MutableSet<String>>(serializer(), "DebugLogs", ::mutableSetOf)
init {
allInstances.add(this)
}
- fun isEnabled() = DeveloperFeatures.isEnabled && EnabledLogs.data.contains(tag)
+ fun isEnabled() = EnabledLogs.data.contains(tag)
+ fun log(text: String) = log { text }
fun log(text: () -> String) {
if (!isEnabled()) return
MC.sendChat(Text.literal(text()))
diff --git a/src/main/kotlin/features/debug/DebugView.kt b/src/main/kotlin/features/debug/DebugView.kt
deleted file mode 100644
index ee54260..0000000
--- a/src/main/kotlin/features/debug/DebugView.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-package moe.nea.firmament.features.debug
-
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.annotations.Subscribe
-import moe.nea.firmament.events.TickEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.util.TimeMark
-
-object DebugView : FirmamentFeature {
- private data class StoredVariable<T>(
- val obj: T,
- val timer: TimeMark,
- )
-
- private val storedVariables: MutableMap<String, StoredVariable<*>> = sortedMapOf()
- override val identifier: String
- get() = "debug-view"
- override val defaultEnabled: Boolean
- get() = Firmament.DEBUG
-
- fun <T : Any?> showVariable(label: String, obj: T) {
- synchronized(this) {
- storedVariables[label] = StoredVariable(obj, TimeMark.now())
- }
- }
-
-}
diff --git a/src/main/kotlin/features/debug/DeveloperFeatures.kt b/src/main/kotlin/features/debug/DeveloperFeatures.kt
index 8f0c25c..e86c6ad 100644
--- a/src/main/kotlin/features/debug/DeveloperFeatures.kt
+++ b/src/main/kotlin/features/debug/DeveloperFeatures.kt
@@ -3,6 +3,10 @@ package moe.nea.firmament.features.debug
import java.io.File
import java.nio.file.Path
import java.util.concurrent.CompletableFuture
+import org.objectweb.asm.ClassReader
+import org.objectweb.asm.Type
+import org.objectweb.asm.tree.ClassNode
+import org.spongepowered.asm.mixin.Mixin
import kotlinx.serialization.json.encodeToStream
import kotlin.io.path.absolute
import kotlin.io.path.exists
@@ -10,26 +14,27 @@ import net.minecraft.client.MinecraftClient
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.DebugInstantiateEvent
import moe.nea.firmament.events.TickEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.init.MixinPlugin
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.asm.AsmAnnotationUtil
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.iterate
-object DeveloperFeatures : FirmamentFeature {
- override val identifier: String
+object DeveloperFeatures {
+ val DEVELOPER_SUBCOMMAND: String = "dev"
+ val identifier: String
get() = "developer"
- override val config: TConfig
- get() = TConfig
- override val defaultEnabled: Boolean
- get() = Firmament.DEBUG
val gradleDir =
Path.of(".").absolute()
.iterate { it.parent }
.find { it.resolve("settings.gradle.kts").exists() }
+ @Config
object TConfig : ManagedConfig("developer", Category.DEV) {
val autoRebuildResources by toggle("auto-rebuild") { false }
}
@@ -42,6 +47,42 @@ object DeveloperFeatures : FirmamentFeature {
}
@Subscribe
+ fun loadAllMixinClasses(event: DebugInstantiateEvent) {
+ val allMixinClasses = mutableSetOf<String>()
+ MixinPlugin.instances.forEach { plugin ->
+ val prefix = plugin.mixinPackage + "."
+ val classes = plugin.mixins.map { prefix + it }
+ allMixinClasses.addAll(classes)
+ for (cls in classes) {
+ val targets = javaClass.classLoader.getResourceAsStream("${cls.replace(".", "/")}.class").use {
+ val node = ClassNode()
+ ClassReader(it).accept(node, 0)
+ val mixins = mutableListOf<Mixin>()
+ (node.visibleAnnotations.orEmpty() + node.invisibleAnnotations.orEmpty()).forEach {
+ val annotationType = Type.getType(it.desc)
+ val mixinType = Type.getType(Mixin::class.java)
+ if (mixinType == annotationType) {
+ mixins.add(AsmAnnotationUtil.createProxy(Mixin::class.java, it))
+ }
+ }
+ mixins.flatMap { it.targets.toList() } + mixins.flatMap { it.value.map { it.java.name } }
+ }
+ for (target in targets)
+ try {
+ Firmament.logger.debug("Loading ${target} to force instantiate ${cls}")
+ Class.forName(target, true, javaClass.classLoader)
+ } catch (ex: Throwable) {
+ Firmament.logger.error("Could not load class ${target} that has been mixind by $cls", ex)
+ }
+ }
+ }
+ Firmament.logger.info("Forceloaded all Firmament mixins:")
+ val applied = MixinPlugin.instances.flatMap { it.appliedMixins }.toSet()
+ applied.forEach { Firmament.logger.info(" - ${it}") }
+ require(allMixinClasses == applied)
+ }
+
+ @Subscribe
fun dumpMissingTranslations(tickEvent: TickEvent) {
val toDump = missingTranslations ?: return
missingTranslations = null
@@ -52,7 +93,7 @@ object DeveloperFeatures : FirmamentFeature {
@JvmStatic
fun hookOnBeforeResourceReload(client: MinecraftClient): CompletableFuture<Void> {
- val reloadFuture = if (TConfig.autoRebuildResources && isEnabled && gradleDir != null) {
+ val reloadFuture = if (TConfig.autoRebuildResources && Firmament.DEBUG && gradleDir != null) {
val builder = ProcessBuilder("./gradlew", ":processResources")
builder.directory(gradleDir.toFile())
builder.inheritIO()
@@ -60,9 +101,12 @@ object DeveloperFeatures : FirmamentFeature {
MC.sendChat(Text.translatable("firmament.dev.resourcerebuild.start"))
val startTime = TimeMark.now()
process.toHandle().onExit().thenApply {
- MC.sendChat(Text.stringifiedTranslatable(
- "firmament.dev.resourcerebuild.done",
- startTime.passedTime()))
+ MC.sendChat(
+ Text.stringifiedTranslatable(
+ "firmament.dev.resourcerebuild.done",
+ startTime.passedTime()
+ )
+ )
Unit
}
} else {
diff --git a/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt
new file mode 100644
index 0000000..bdc1f9a
--- /dev/null
+++ b/src/main/kotlin/features/debug/ExportedTestConstantMeta.kt
@@ -0,0 +1,27 @@
+package moe.nea.firmament.features.debug
+
+import com.mojang.serialization.Codec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import java.util.Optional
+import net.minecraft.SharedConstants
+import moe.nea.firmament.Firmament
+
+data class ExportedTestConstantMeta(
+ val dataVersion: Int,
+ val modVersion: Optional<String>,
+) {
+ companion object {
+ val current = ExportedTestConstantMeta(
+ SharedConstants.getGameVersion().dataVersion().id,
+ Optional.of("Firmament ${Firmament.version.friendlyString}")
+ )
+
+ val CODEC: Codec<ExportedTestConstantMeta> = RecordCodecBuilder.create {
+ it.group(
+ Codec.INT.fieldOf("dataVersion").forGetter(ExportedTestConstantMeta::dataVersion),
+ Codec.STRING.optionalFieldOf("modVersion").forGetter(ExportedTestConstantMeta::modVersion),
+ ).apply(it, ::ExportedTestConstantMeta)
+ }
+ val SOURCE_CODEC = CODEC.fieldOf("source").codec()
+ }
+}
diff --git a/src/main/kotlin/features/debug/MinorTrolling.kt b/src/main/kotlin/features/debug/MinorTrolling.kt
index 32035a6..d802d40 100644
--- a/src/main/kotlin/features/debug/MinorTrolling.kt
+++ b/src/main/kotlin/features/debug/MinorTrolling.kt
@@ -5,12 +5,10 @@ package moe.nea.firmament.features.debug
import net.minecraft.text.Text
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ModifyChatEvent
-import moe.nea.firmament.features.FirmamentFeature
-
// In memorian Dulkir
-object MinorTrolling : FirmamentFeature {
- override val identifier: String
+object MinorTrolling {
+ val identifier: String
get() = "minor-trolling"
val trollers = listOf("nea89o", "lrg89")
diff --git a/src/main/kotlin/features/debug/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt
index 13320dc..049a0fb 100644
--- a/src/main/kotlin/features/debug/PowerUserTools.kt
+++ b/src/main/kotlin/features/debug/PowerUserTools.kt
@@ -5,13 +5,18 @@ import kotlin.jvm.optionals.getOrNull
import net.minecraft.block.SkullBlock
import net.minecraft.block.entity.SkullBlockEntity
import net.minecraft.component.DataComponentTypes
+import net.minecraft.component.type.ProfileComponent
import net.minecraft.entity.Entity
import net.minecraft.entity.LivingEntity
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
+import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtOps
+import net.minecraft.predicate.NbtPredicate
import net.minecraft.text.Text
import net.minecraft.text.TextCodecs
+import net.minecraft.util.Identifier
+import net.minecraft.util.Nameable
import net.minecraft.util.hit.BlockHitResult
import net.minecraft.util.hit.EntityHitResult
import net.minecraft.util.hit.HitResult
@@ -22,22 +27,27 @@ import moe.nea.firmament.events.ItemTooltipEvent
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.events.WorldKeyboardEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.focusedItemStack
+import moe.nea.firmament.util.grey
+import moe.nea.firmament.util.mc.IntrospectableItemModelManager
+import moe.nea.firmament.util.mc.SNbtFormatter
import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.iterableArmorItems
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.tr
-object PowerUserTools : FirmamentFeature {
- override val identifier: String
+object PowerUserTools {
+ val identifier: String
get() = "power-user"
+ @Config
object TConfig : ManagedConfig(identifier, Category.DEV) {
val showItemIds by toggle("show-item-id") { false }
val copyItemId by keyBindingWithDefaultUnbound("copy-item-id")
@@ -47,22 +57,24 @@ object PowerUserTools : FirmamentFeature {
val copySkullTexture by keyBindingWithDefaultUnbound("copy-skull-texture")
val copyEntityData by keyBindingWithDefaultUnbound("entity-data")
val copyItemStack by keyBindingWithDefaultUnbound("copy-item-stack")
+ val copyTitle by keyBindingWithDefaultUnbound("copy-title")
+ val exportItemStackToRepo by keyBindingWithDefaultUnbound("export-item-stack")
+ val exportUIRecipes by keyBindingWithDefaultUnbound("export-recipe")
+ val exportNpcLocation by keyBindingWithDefaultUnbound("export-npc-location")
+ val highlightNonOverlayItems by toggle("highlight-non-overlay") { false }
+ val dontHighlightSemicolonItems by toggle("dont-highlight-semicolon-items") { false }
}
- override val config
- get() = TConfig
-
var lastCopiedStack: Pair<ItemStack, Text>? = null
set(value) {
field = value
- if (value != null) lastCopiedStackViewTime = true
+ if (value != null) lastCopiedStackViewTime = 2
}
- var lastCopiedStackViewTime = false
+ var lastCopiedStackViewTime = 0
@Subscribe
fun resetLastCopiedStack(event: TickEvent) {
- if (!lastCopiedStackViewTime) lastCopiedStack = null
- lastCopiedStackViewTime = false
+ if (lastCopiedStackViewTime-- < 0) lastCopiedStack = null
}
@Subscribe
@@ -86,12 +98,17 @@ object PowerUserTools : FirmamentFeature {
}
fun showEntity(target: Entity) {
+ val nbt = NbtPredicate.entityToNbt(target)
+ nbt.remove("Inventory")
+ nbt.put("StyledName", TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, target.styledDisplayName).orThrow)
+ println(SNbtFormatter.prettify(nbt))
+ ClipboardUtils.setTextContent(SNbtFormatter.prettify(nbt))
MC.sendChat(Text.translatable("firmament.poweruser.entity.type", target.type))
MC.sendChat(Text.translatable("firmament.poweruser.entity.name", target.name))
MC.sendChat(Text.stringifiedTranslatable("firmament.poweruser.entity.position", target.pos))
if (target is LivingEntity) {
MC.sendChat(Text.translatable("firmament.poweruser.entity.armor"))
- for (armorItem in target.armorItems) {
+ for ((slot, armorItem) in target.iterableArmorItems) {
MC.sendChat(Text.translatable("firmament.poweruser.entity.armor.item", debugFormat(armorItem)))
}
}
@@ -101,6 +118,8 @@ object PowerUserTools : FirmamentFeature {
}
}
+ // TODO: leak this through some other way, maybe.
+ lateinit var getSkullId: (profile: ProfileComponent) -> Identifier?
@Subscribe
fun copyInventoryInfo(it: HandledScreenKeyPressedEvent) {
@@ -116,7 +135,11 @@ object PowerUserTools : FirmamentFeature {
lastCopiedStack =
Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skyblockid", sbId.neuItem))
} else if (it.matches(TConfig.copyTexturePackId)) {
- val model = CustomItemModelEvent.getModelIdentifier(item)
+ val model = CustomItemModelEvent.getModelIdentifier0(item, object : IntrospectableItemModelManager {
+ override fun hasModel_firmament(identifier: Identifier): Boolean {
+ return true
+ }
+ }).getOrNull() // TODO: remove global texture overrides, maybe
if (model == null) {
lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.modelid.fail"))
return
@@ -146,7 +169,7 @@ object PowerUserTools : FirmamentFeature {
lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-profile"))
return
}
- val skullTexture = CustomSkyBlockTextures.getSkullTexture(profile)
+ val skullTexture = getSkullId(profile)
if (skullTexture == null) {
lastCopiedStack = Pair(item, Text.translatable("firmament.tooltip.copied.skull-id.fail.no-texture"))
return
@@ -156,11 +179,23 @@ object PowerUserTools : FirmamentFeature {
Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.skull-id", skullTexture.toString()))
println("Copied skull id: $skullTexture")
} else if (it.matches(TConfig.copyItemStack)) {
- ClipboardUtils.setTextContent(
- ItemStack.CODEC
- .encodeStart(MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE), item)
- .orThrow.toPrettyString())
+ val nbt = ItemStack.CODEC
+ .encodeStart(MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE), item)
+ .orThrow
+ ClipboardUtils.setTextContent(nbt.toPrettyString())
lastCopiedStack = Pair(item, Text.stringifiedTranslatable("firmament.tooltip.copied.stack"))
+ } else if (it.matches(TConfig.copyTitle)) {
+ val allTitles = NbtList()
+ val inventoryNames =
+ it.screen.screenHandler.slots
+ .mapNotNullTo(mutableSetOf()) { it.inventory }
+ .filterIsInstance<Nameable>()
+ .map { it.name }
+ for (it in listOf(it.screen.title) + inventoryNames) {
+ allTitles.add(TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it).result().getOrNull()!!)
+ }
+ ClipboardUtils.setTextContent(allTitles.toPrettyString())
+ MC.sendChat(tr("firmament.power-user.title.copied", "Copied screen and inventory titles"))
}
}
@@ -179,7 +214,7 @@ object PowerUserTools : FirmamentFeature {
MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail"))
return
}
- val id = CustomSkyBlockTextures.getSkullTexture(entity.owner!!)
+ val id = getSkullId(entity.owner!!)
if (id == null) {
MC.sendChat(Text.translatable("firmament.tooltip.copied.skull.fail"))
} else {
@@ -193,14 +228,14 @@ object PowerUserTools : FirmamentFeature {
fun addItemId(it: ItemTooltipEvent) {
if (TConfig.showItemIds) {
val id = it.stack.skyBlockId ?: return
- it.lines.add(Text.stringifiedTranslatable("firmament.tooltip.skyblockid", id.neuItem))
+ it.lines.add(Text.stringifiedTranslatable("firmament.tooltip.skyblockid", id.neuItem).grey())
}
val (item, text) = lastCopiedStack ?: return
if (!ItemStack.areEqual(item, it.stack)) {
lastCopiedStack = null
return
}
- lastCopiedStackViewTime = true
+ lastCopiedStackViewTime = 0
it.lines.add(text)
}
diff --git a/src/main/kotlin/features/debug/SoundVisualizer.kt b/src/main/kotlin/features/debug/SoundVisualizer.kt
new file mode 100644
index 0000000..f805e6b
--- /dev/null
+++ b/src/main/kotlin/features/debug/SoundVisualizer.kt
@@ -0,0 +1,65 @@
+package moe.nea.firmament.features.debug
+
+import net.minecraft.text.Text
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.SoundReceiveEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.util.red
+import moe.nea.firmament.util.render.RenderInWorldContext
+
+object SoundVisualizer {
+
+ var showSounds = false
+
+ var sounds = mutableListOf<SoundReceiveEvent>()
+
+
+ @Subscribe
+ fun onSubCommand(event: CommandEvent.SubCommand) {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
+ thenLiteral("sounds") {
+ thenExecute {
+ showSounds = !showSounds
+ if (!showSounds) {
+ sounds.clear()
+ }
+ }
+ }
+ }
+ }
+
+ @Subscribe
+ fun onWorldSwap(event: WorldReadyEvent) {
+ sounds.clear()
+ }
+
+ @Subscribe
+ fun onRender(event: WorldRenderLastEvent) {
+ RenderInWorldContext.renderInWorld(event) {
+ sounds.forEach { event ->
+ withFacingThePlayer(event.position) {
+ text(
+ Text.literal(event.sound.value().id.toString()).also {
+ if (event.cancelled)
+ it.red()
+ },
+ verticalAlign = RenderInWorldContext.VerticalAlign.CENTER,
+ )
+ }
+ }
+ }
+ }
+
+ @Subscribe
+ fun onSoundReceive(event: SoundReceiveEvent) {
+ if (!showSounds) return
+ if (sounds.size > 1000) {
+ sounds.subList(0, 200).clear()
+ }
+ sounds.add(event)
+ }
+}
diff --git a/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt
new file mode 100644
index 0000000..9356dd3
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/ExportRecipe.kt
@@ -0,0 +1,255 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import kotlinx.coroutines.launch
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import net.minecraft.client.network.AbstractClientPlayerEntity
+import net.minecraft.entity.decoration.ArmorStandEntity
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.events.WorldKeyboardEvent
+import moe.nea.firmament.features.debug.PowerUserTools
+import moe.nea.firmament.repo.ItemNameLookup
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.async.waitForTextInput
+import moe.nea.firmament.util.ifDropLast
+import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.mc.setSkullOwner
+import moe.nea.firmament.util.parseShortNumber
+import moe.nea.firmament.util.red
+import moe.nea.firmament.util.removeColorCodes
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+import moe.nea.firmament.util.tr
+import moe.nea.firmament.util.unformattedString
+import moe.nea.firmament.util.useMatch
+
+object ExportRecipe {
+
+
+ val xNames = "123"
+ val yNames = "ABC"
+
+ val slotIndices = (0..<9).map {
+ val x = it % 3
+ val y = it / 3
+
+ (yNames[y].toString() + xNames[x].toString()) to x + y * 9 + 10
+ }
+ val resultSlot = 25
+ val craftingTableSlut = resultSlot - 2
+
+ @Subscribe
+ fun exportNpcLocation(event: WorldKeyboardEvent) {
+ if (!event.matches(PowerUserTools.TConfig.exportNpcLocation)) {
+ return
+ }
+ val entity = MC.instance.targetedEntity
+ if (entity == null) {
+ MC.sendChat(tr("firmament.repo.export.npc.noentity", "Could not find entity to export"))
+ return
+ }
+ Firmament.coroutineScope.launch {
+ val guessName = entity.world.getEntitiesByClass(
+ ArmorStandEntity::class.java,
+ entity.boundingBox.expand(0.1),
+ { !it.name.string.contains("CLICK") })
+ .firstOrNull()?.customName?.string
+ ?: ""
+ val reply = waitForTextInput("$guessName (NPC)", "Export stub")
+ val id = generateName(reply)
+ ItemExporter.exportStub(id, "§9$reply") {
+ val playerEntity = entity as? AbstractClientPlayerEntity
+ val textureUrl = playerEntity?.skinTextures?.textureUrl
+ if (textureUrl != null)
+ it.setSkullOwner(playerEntity.uuid, textureUrl)
+ }
+ ItemExporter.modifyJson(id) {
+ val mutJson = it.toMutableMap()
+ mutJson["island"] = JsonPrimitive(SBData.skyblockLocation?.locrawMode ?: "unknown")
+ mutJson["x"] = JsonPrimitive(entity.blockX)
+ mutJson["y"] = JsonPrimitive(entity.blockY)
+ mutJson["z"] = JsonPrimitive(entity.blockZ)
+ JsonObject(mutJson)
+ }
+ }
+ }
+
+ @Subscribe
+ fun onRecipeKeyBind(event: HandledScreenKeyPressedEvent) {
+ if (!event.matches(PowerUserTools.TConfig.exportUIRecipes)) {
+ return
+ }
+ val title = event.screen.title.string
+ val sellSlot = event.screen.getSlotByIndex(49, false)?.stack
+ val craftingTableSlot = event.screen.getSlotByIndex(craftingTableSlut, false)
+ if (craftingTableSlot?.stack?.displayNameAccordingToNbt?.unformattedString == "Crafting Table") {
+ slotIndices.forEach { (_, index) ->
+ event.screen.getSlotByIndex(index, false)?.stack?.let(ItemExporter::ensureExported)
+ }
+ val inputs = slotIndices.associate { (name, index) ->
+ val id = event.screen.getSlotByIndex(index, false)?.stack?.takeIf { !it.isEmpty() }?.let {
+ "${it.skyBlockId?.neuItem}:${it.count}"
+ } ?: ""
+ name to JsonPrimitive(id)
+ }
+ val output = event.screen.getSlotByIndex(resultSlot, false)?.stack!!
+ val overrideOutputId = output.skyBlockId!!.neuItem
+ val count = output.count
+ val recipe = JsonObject(
+ inputs + mapOf(
+ "type" to JsonPrimitive("crafting"),
+ "count" to JsonPrimitive(count),
+ "overrideOutputId" to JsonPrimitive(overrideOutputId)
+ )
+ )
+ ItemExporter.appendRecipe(output.skyBlockId!!, recipe)
+ MC.sendChat(tr("firmament.repo.export.recipe", "Recipe for ${output.skyBlockId} exported."))
+ return
+ } else if (sellSlot?.displayNameAccordingToNbt?.string == "Sell Item" || (sellSlot?.loreAccordingToNbt
+ ?: listOf()).any { it.string == "Click to buyback!" }
+ ) {
+ val shopId = SkyblockId(title.uppercase().replace(" ", "_") + "_NPC")
+ if (!ItemExporter.isExported(shopId)) {
+ // TODO: export location + skin of last clicked npc
+ ItemExporter.exportStub(shopId, "§9$title (NPC)")
+ }
+ for (index in (9..9 * 5)) {
+ val item = event.screen.getSlotByIndex(index, false)?.stack ?: continue
+ val skyblockId = item.skyBlockId ?: continue
+ val costLines = item.loreAccordingToNbt
+ .map { it.string.trim() }
+ .dropWhile { !it.startsWith("Cost") }
+ .dropWhile { it == "Cost" }
+ .takeWhile { it != "Click to trade!" }
+ .takeWhile { it != "Stock" }
+ .filter { !it.isBlank() }
+ .map { it.removePrefix("Cost: ") }
+
+
+ val costs = costLines.mapNotNull { lineText ->
+ val line = findStackableItemByName(lineText)
+ if (line == null) {
+ MC.sendChat(
+ tr(
+ "firmament.repo.itemshop.fail",
+ "Could not parse cost item ${lineText} for ${item.displayNameAccordingToNbt}"
+ ).red()
+ )
+ }
+ line
+ }
+
+
+ ItemExporter.appendRecipe(
+ shopId, JsonObject(
+ mapOf(
+ "type" to JsonPrimitive("npc_shop"),
+ "cost" to JsonArray(costs.map { JsonPrimitive("${it.first.neuItem}:${it.second}") }),
+ "result" to JsonPrimitive("${skyblockId.neuItem}:${item.count}"),
+ )
+ )
+ )
+ }
+ MC.sendChat(tr("firmament.repo.export.itemshop", "Item Shop export for ${title} complete."))
+ } else {
+ MC.sendChat(tr("firmament.repo.export.recipe.fail", "No Recipe found"))
+ }
+ }
+
+ private val coinRegex = "(?<amount>$SHORT_NUMBER_FORMAT) Coins?".toPattern()
+ private val stackedItemRegex = "(?<name>.*) x(?<count>$SHORT_NUMBER_FORMAT)".toPattern()
+ private val reverseStackedItemRegex = "(?<count>$SHORT_NUMBER_FORMAT)x (?<name>.*)".toPattern()
+ private val essenceRegex = "(?<essence>.*) Essence x(?<count>$SHORT_NUMBER_FORMAT)".toPattern()
+ private val numberedItemRegex = "(?<count>$SHORT_NUMBER_FORMAT) (?<what>.*)".toPattern()
+
+ private val etherialRewardPattern = "\\+(?<amount>${SHORT_NUMBER_FORMAT})x? (?<what>.*)".toPattern()
+
+ fun findForName(name: String, fallbackToGenerated: Boolean = true): SkyblockId? {
+ var id = ItemNameLookup.guessItemByName(name, true)
+ if (id == null && fallbackToGenerated) {
+ id = generateName(name)
+ }
+ return id
+ }
+
+ fun skill(name: String): SkyblockId {
+ return SkyblockId("SKYBLOCK_SKILL_${name}")
+ }
+
+ fun generateName(name: String): SkyblockId {
+ return SkyblockId(name.uppercase().replace(" ", "_").replace(Regex("[^A-Z_]+"), ""))
+ }
+
+ fun findStackableItemByName(name: String, fallbackToGenerated: Boolean = false): Pair<SkyblockId, Double>? {
+ val properName = name.removeColorCodes().trim()
+ if (properName == "FREE" || properName == "This Chest is Free!") {
+ return Pair(SkyBlockItems.COINS, 0.0)
+ }
+ coinRegex.useMatch(properName) {
+ return Pair(SkyBlockItems.COINS, parseShortNumber(group("amount")))
+ }
+ etherialRewardPattern.useMatch(properName) {
+ val id = when (val id = group("what")) {
+ "Copper" -> SkyblockId("SKYBLOCK_COPPER")
+ "Bits" -> SkyblockId("SKYBLOCK_BIT")
+ "Garden Experience" -> SkyblockId("SKYBLOCK_SKILL_GARDEN")
+ "Farming XP" -> SkyblockId("SKYBLOCK_SKILL_FARMING")
+ "Gold Essence" -> SkyblockId("ESSENCE_GOLD")
+ "Gemstone Powder" -> SkyblockId("SKYBLOCK_POWDER_GEMSTONE")
+ "Mithril Powder" -> SkyblockId("SKYBLOCK_POWDER_MITHRIL")
+ "Pelts" -> SkyblockId("SKYBLOCK_PELT")
+ "Fine Flour" -> SkyblockId("FINE_FLOUR")
+ else -> {
+ id.ifDropLast(" Experience") {
+ skill(generateName(it).neuItem)
+ } ?: id.ifDropLast(" XP") {
+ skill(generateName(it).neuItem)
+ } ?: id.ifDropLast(" Powder") {
+ SkyblockId("SKYBLOCK_POWDER_${generateName(it).neuItem}")
+ } ?: id.ifDropLast(" Essence") {
+ SkyblockId("ESSENCE_${generateName(it).neuItem}")
+ } ?: generateName(id)
+ }
+ }
+ return Pair(id, parseShortNumber(group("amount")))
+ }
+ essenceRegex.useMatch(properName) {
+ return Pair(
+ SkyblockId("ESSENCE_${group("essence").uppercase()}"),
+ parseShortNumber(group("count"))
+ )
+ }
+ stackedItemRegex.useMatch(properName) {
+ val item = findForName(group("name"), fallbackToGenerated)
+ if (item != null) {
+ val count = parseShortNumber(group("count"))
+ return Pair(item, count)
+ }
+ }
+ reverseStackedItemRegex.useMatch(properName) {
+ val item = findForName(group("name"), fallbackToGenerated)
+ if (item != null) {
+ val count = parseShortNumber(group("count"))
+ return Pair(item, count)
+ }
+ }
+ numberedItemRegex.useMatch(properName) {
+ val item = findForName(group("what"), fallbackToGenerated)
+ if (item != null) {
+ val count = parseShortNumber(group("count"))
+ return Pair(item, count)
+ }
+ }
+
+ return findForName(properName, fallbackToGenerated)?.let { Pair(it, 1.0) }
+ }
+
+}
diff --git a/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt
new file mode 100644
index 0000000..ccbd7e0
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt
@@ -0,0 +1,250 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import kotlinx.coroutines.launch
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.jsonObject
+import kotlinx.serialization.json.jsonPrimitive
+import kotlin.io.path.createParentDirectories
+import kotlin.io.path.exists
+import kotlin.io.path.notExists
+import kotlin.io.path.readText
+import kotlin.io.path.relativeTo
+import kotlin.io.path.writeText
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import net.minecraft.nbt.NbtString
+import net.minecraft.text.Text
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+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.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.events.SlotRenderEvents
+import moe.nea.firmament.features.debug.DeveloperFeatures
+import moe.nea.firmament.features.debug.ExportedTestConstantMeta
+import moe.nea.firmament.features.debug.PowerUserTools
+import moe.nea.firmament.repo.RepoDownloadManager
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.LegacyTagParser
+import moe.nea.firmament.util.LegacyTagWriter.Companion.toLegacyString
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.focusedItemStack
+import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.mc.toNbtList
+import moe.nea.firmament.util.render.drawGuiTexture
+import moe.nea.firmament.util.setSkyBlockId
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.tr
+
+object ItemExporter {
+
+ fun exportItem(itemStack: ItemStack): Text {
+ nonOverlayCache.clear()
+ val exporter = LegacyItemExporter.createExporter(itemStack)
+ var json = exporter.exportJson()
+ val fileName = json.jsonObject["internalname"]?.jsonPrimitive?.takeIf { it.isString }?.content
+ if (fileName == null) {
+ return tr(
+ "firmament.repoexport.nointernalname",
+ "Could not find internal name to export for this item (null.json)"
+ )
+ }
+ val itemFile = RepoDownloadManager.repoSavedLocation.resolve("items").resolve("${fileName}.json")
+ itemFile.createParentDirectories()
+ if (itemFile.exists()) {
+ val existing = try {
+ Firmament.json.decodeFromString<JsonObject>(itemFile.readText())
+ } catch (ex: Exception) {
+ ex.printStackTrace()
+ JsonObject(mapOf())
+ }
+ val mut = json.jsonObject.toMutableMap()
+ for (prop in existing) {
+ if (prop.key !in mut || mut[prop.key]!!.let {
+ (it is JsonPrimitive && (it.content.isEmpty() || it.content == "0")) || (it is JsonArray && it.isEmpty()) || (it is JsonObject && it.isEmpty())
+ })
+ mut[prop.key] = prop.value
+ }
+ json = JsonObject(mut)
+ }
+ val jsonFormatted = Firmament.twoSpaceJson.encodeToString(json)
+ itemFile.writeText(jsonFormatted)
+ val overlayFile = RepoDownloadManager.repoSavedLocation.resolve("itemsOverlay")
+ .resolve(ExportedTestConstantMeta.current.dataVersion.toString())
+ .resolve("${fileName}.snbt")
+ overlayFile.createParentDirectories()
+ overlayFile.writeText(exporter.exportModernSnbt().toPrettyString())
+ return tr(
+ "firmament.repoexport.success",
+ "Exported item to ${itemFile.relativeTo(RepoDownloadManager.repoSavedLocation)}${
+ exporter.warnings.joinToString(
+ ""
+ ) { "\nWarning: $it" }
+ }"
+ )
+ }
+
+ fun pathFor(skyBlockId: SkyblockId) =
+ RepoManager.neuRepo.baseFolder.resolve("items/${skyBlockId.neuItem}.json")
+
+ fun isExported(skyblockId: SkyblockId) =
+ pathFor(skyblockId).exists()
+
+ fun ensureExported(itemStack: ItemStack) {
+ if (!isExported(itemStack.skyBlockId ?: return))
+ MC.sendChat(exportItem(itemStack))
+ }
+
+ fun modifyJson(skyblockId: SkyblockId, modify: (JsonObject) -> JsonObject) {
+ val oldJson = Firmament.json.decodeFromString<JsonObject>(pathFor(skyblockId).readText())
+ val newJson = modify(oldJson)
+ pathFor(skyblockId).writeText(Firmament.twoSpaceJson.encodeToString(JsonObject(newJson)))
+ }
+
+ fun appendRecipe(skyblockId: SkyblockId, recipe: JsonObject) {
+ modifyJson(skyblockId) { oldJson ->
+ val mutableJson = oldJson.toMutableMap()
+ val recipes = ((mutableJson["recipes"] as JsonArray?) ?: listOf()).toMutableList()
+ recipes.add(recipe)
+ mutableJson["recipes"] = JsonArray(recipes)
+ JsonObject(mutableJson)
+ }
+ }
+
+ @Subscribe
+ fun onCommand(event: CommandEvent.SubCommand) {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
+ thenLiteral("reexportlore") {
+ thenArgument("itemid", RestArgumentType) { itemid ->
+ suggests { ctx, builder ->
+ val spaceIndex = builder.remaining.lastIndexOf(" ")
+ val (before, after) =
+ if (spaceIndex < 0) Pair("", builder.remaining)
+ else Pair(
+ builder.remaining.substring(0, spaceIndex + 1),
+ builder.remaining.substring(spaceIndex + 1)
+ )
+ RepoManager.neuRepo.items.items.keys
+ .asSequence()
+ .filter { it.startsWith(after, ignoreCase = true) }
+ .forEach {
+ builder.suggest(before + it)
+ }
+
+ builder.buildFuture()
+ }
+ thenExecute {
+ for (itemid in get(itemid).split(" ").map { SkyblockId(it) }) {
+ if (pathFor(itemid).notExists()) {
+ MC.sendChat(
+ tr(
+ "firmament.repo.export.relore.fail",
+ "Could not find json file to relore for ${itemid}"
+ )
+ )
+ }
+ fixLoreNbtFor(itemid)
+ MC.sendChat(
+ tr(
+ "firmament.repo.export.relore",
+ "Updated lore / display name for $itemid"
+ )
+ )
+ }
+ }
+ }
+ thenLiteral("all") {
+ thenExecute {
+ var i = 0
+ val chunkSize = 100
+ val items = RepoManager.neuRepo.items.items.keys
+ Firmament.coroutineScope.launch {
+ items.chunked(chunkSize).forEach { key ->
+ MC.sendChat(
+ tr(
+ "firmament.repo.export.relore.progress",
+ "Updated lore / display for ${i * chunkSize} / ${items.size}."
+ )
+ )
+ i++
+ key.forEach {
+ fixLoreNbtFor(SkyblockId(it))
+ }
+ }
+ MC.sendChat(tr("firmament.repo.export.relore.alldone", "All lores updated."))
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fun fixLoreNbtFor(itemid: SkyblockId) {
+ modifyJson(itemid) {
+ val mutJson = it.toMutableMap()
+ val legacyTag = LegacyTagParser.parse(mutJson["nbttag"]!!.jsonPrimitive.content)
+ val display = legacyTag.getCompoundOrEmpty("display")
+ legacyTag.put("display", display)
+ display.putString("Name", mutJson["displayname"]!!.jsonPrimitive.content)
+ display.put(
+ "Lore",
+ (mutJson["lore"] as JsonArray).map { NbtString.of(it.jsonPrimitive.content) }
+ .toNbtList()
+ )
+ mutJson["nbttag"] = JsonPrimitive(legacyTag.toLegacyString())
+ JsonObject(mutJson)
+ }
+ }
+
+ @Subscribe
+ fun onKeyBind(event: HandledScreenKeyPressedEvent) {
+ if (event.matches(PowerUserTools.TConfig.exportItemStackToRepo)) {
+ val itemStack = event.screen.focusedItemStack ?: return
+ PowerUserTools.lastCopiedStack = (itemStack to exportItem(itemStack))
+ }
+ }
+
+ val nonOverlayCache = mutableMapOf<SkyblockId, Boolean>()
+
+ @Subscribe
+ fun onRender(event: SlotRenderEvents.Before) {
+ if (!PowerUserTools.TConfig.highlightNonOverlayItems) {
+ return
+ }
+ val stack = event.slot.stack ?: return
+ val id = event.slot.stack.skyBlockId?.neuItem
+ if (PowerUserTools.TConfig.dontHighlightSemicolonItems && id != null && id.contains(";")) return
+ val sbId = stack.skyBlockId ?: return
+ val isExported = nonOverlayCache.getOrPut(sbId) {
+ RepoManager.overlayData.getOverlayFiles(sbId).isNotEmpty() || // This extra case is here so that an export works immediately, without repo reload
+ RepoDownloadManager.repoSavedLocation.resolve("itemsOverlay")
+ .resolve(ExportedTestConstantMeta.current.dataVersion.toString())
+ .resolve("${stack.skyBlockId}.snbt")
+ .exists()
+ }
+ if (!isExported)
+ event.context.drawGuiTexture(
+ Firmament.identifier("selected_pet_background"),
+ event.slot.x, event.slot.y, 16, 16,
+ )
+ }
+
+ fun exportStub(skyblockId: SkyblockId, title: String, extra: (ItemStack) -> Unit = {}) {
+ exportItem(ItemStack(Items.PLAYER_HEAD).also {
+ it.displayNameAccordingToNbt = Text.literal(title)
+ it.loreAccordingToNbt = listOf(Text.literal(""))
+ it.setSkyBlockId(skyblockId)
+ extra(it) // LOL
+ })
+ MC.sendChat(tr("firmament.repo.export.stub", "Exported a stub item for $skyblockId"))
+ }
+}
diff --git a/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt
new file mode 100644
index 0000000..4b647c7
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt
@@ -0,0 +1,87 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import kotlinx.serialization.Serializable
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.util.Identifier
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
+import moe.nea.firmament.repo.ItemCache
+import moe.nea.firmament.util.StringUtil.camelWords
+import moe.nea.firmament.util.mc.loadItemFromNbt
+
+/**
+ * Load data based on [prismarine.js' 1.8 item data](https://github.com/PrismarineJS/minecraft-data/blob/master/data/pc/1.8/items.json)
+ */
+object LegacyItemData {
+ @Serializable
+ data class ItemData(
+ val id: Int,
+ val name: String,
+ val displayName: String,
+ val stackSize: Int,
+ val variations: List<Variation> = listOf()
+ ) {
+ val properId = if (name.contains(":")) name else "minecraft:$name"
+
+ fun allVariants() =
+ variations.map { LegacyItemType(properId, it.metadata.toShort()) } + LegacyItemType(properId, 0)
+ }
+
+ @Serializable
+ data class Variation(
+ val metadata: Int, val displayName: String
+ )
+
+ data class LegacyItemType(
+ val name: String,
+ val metadata: Short
+ ) {
+ override fun toString(): String {
+ return "$name:$metadata"
+ }
+ }
+
+ @Serializable
+ data class EnchantmentData(
+ val id: Int,
+ val name: String,
+ val displayName: String,
+ )
+
+ inline fun <reified T : Any> getLegacyData(name: String) =
+ Firmament.tryDecodeJsonFromStream<T>(
+ LegacyItemData::class.java.getResourceAsStream("/legacy_data/$name.json")!!
+ ).getOrThrow()
+
+ val enchantmentData = getLegacyData<List<EnchantmentData>>("enchantments")
+ val enchantmentLut = enchantmentData.associateBy { Identifier.ofVanilla(it.name) }
+
+ val itemDat = getLegacyData<List<ItemData>>("items")
+
+ @OptIn(ExpensiveItemCacheApi::class) // This is fine, we get loaded in a thread.
+ val itemLut = itemDat.flatMap { item ->
+ item.allVariants().map { legacyItemType ->
+ val nbt = ItemCache.convert189ToModern(NbtCompound().apply {
+ putString("id", legacyItemType.name)
+ putByte("Count", 1)
+ putShort("Damage", legacyItemType.metadata)
+ })!!
+ nbt.remove("components")
+ val stack = loadItemFromNbt(nbt) ?: error("Could not transform $legacyItemType: $nbt")
+ stack.item to legacyItemType
+ }
+ }.toMap()
+
+ @Serializable
+ data class LegacyEffect(
+ val id: Int,
+ val name: String,
+ val displayName: String,
+ val type: String
+ )
+
+ val effectList = getLegacyData<List<LegacyEffect>>("effects")
+ .associateBy {
+ it.name.camelWords().map { it.trim().lowercase() }.joinToString("_")
+ }
+}
diff --git a/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt
new file mode 100644
index 0000000..20ab2c3
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt
@@ -0,0 +1,317 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.put
+import kotlin.concurrent.thread
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.component.DataComponentTypes
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtByte
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtInt
+import net.minecraft.nbt.NbtList
+import net.minecraft.nbt.NbtOps
+import net.minecraft.nbt.NbtString
+import net.minecraft.registry.tag.ItemTags
+import net.minecraft.text.Text
+import net.minecraft.util.Unit
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ClientStartedEvent
+import moe.nea.firmament.features.debug.ExportedTestConstantMeta
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.util.HypixelPetInfo
+import moe.nea.firmament.util.LegacyTagWriter.Companion.toLegacyString
+import moe.nea.firmament.util.StringUtil.words
+import moe.nea.firmament.util.directLiteralStringContent
+import moe.nea.firmament.util.extraAttributes
+import moe.nea.firmament.util.getLegacyFormatString
+import moe.nea.firmament.util.json.toJsonArray
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.mc.toNbtList
+import moe.nea.firmament.util.modifyExtraAttributes
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.Rarity
+import moe.nea.firmament.util.transformEachRecursively
+import moe.nea.firmament.util.unformattedString
+
+class LegacyItemExporter private constructor(var itemStack: ItemStack) {
+ init {
+ require(!itemStack.isEmpty)
+ itemStack.count = 1
+ }
+
+ var lore = itemStack.loreAccordingToNbt
+ val originalId = itemStack.extraAttributes.getString("id")
+ var name = itemStack.displayNameAccordingToNbt
+ val extraAttribs = itemStack.extraAttributes.copy()
+ val legacyNbt = NbtCompound()
+ val warnings = mutableListOf<String>()
+
+ // TODO: check if lore contains non 1.8.9 able hex codes and emit lore in overlay files if so
+
+ fun preprocess() {
+ // TODO: split up preprocess steps into preprocess actions that can be toggled in a ui
+ extraAttribs.remove("timestamp")
+ extraAttribs.remove("uuid")
+ extraAttribs.remove("modifier")
+ extraAttribs.getString("petInfo").ifPresent { petInfoJson ->
+ var petInfo = Firmament.json.decodeFromString<HypixelPetInfo>(petInfoJson)
+ petInfo = petInfo.copy(candyUsed = 0, heldItem = null, exp = 0.0, active = null, uuid = null)
+ extraAttribs.putString("petInfo", Firmament.tightJson.encodeToString(petInfo))
+ }
+ itemStack.skyBlockId?.let {
+ extraAttribs.putString("id", it.neuItem)
+ }
+ trimLore()
+ itemStack.loreAccordingToNbt = itemStack.item.defaultStack.loreAccordingToNbt
+ itemStack.remove(DataComponentTypes.CUSTOM_NAME)
+ }
+
+ fun trimLore() {
+ val rarityIdx = lore.indexOfLast {
+ val firstWordInLine = it.unformattedString.words().filter { it.length > 2 }.firstOrNull()
+ firstWordInLine?.let(Rarity::fromString) != null
+ }
+ if (rarityIdx >= 0) {
+ lore = lore.subList(0, rarityIdx + 1)
+ }
+
+ trimStats()
+
+ deleteLineUntilNextSpace { it.startsWith("Held Item: ") }
+ deleteLineUntilNextSpace { it.startsWith("Progress to Level ") }
+ deleteLineUntilNextSpace { it.startsWith("MAX LEVEL") }
+ deleteLineUntilNextSpace { it.startsWith("Click to view recipe!") }
+ collapseWhitespaces()
+
+ name = name.transformEachRecursively {
+ var string = it.directLiteralStringContent ?: return@transformEachRecursively it
+ string = string.replace("Lvl \\d+".toRegex(), "Lvl {LVL}")
+ Text.literal(string).setStyle(it.style)
+ }
+
+ if (lore.isEmpty())
+ lore = listOf(Text.empty())
+ }
+
+ private fun trimStats() {
+ val lore = this.lore.toMutableList()
+ for (index in lore.indices) {
+ val value = lore[index]
+ val statLine = SBItemStack.parseStatLine(value)
+ if (statLine == null) break
+ val v = value.copy()
+ require(value.directLiteralStringContent == "")
+ v.siblings.removeIf { it.directLiteralStringContent!!.contains("(") }
+ val last = v.siblings.last()
+ v.siblings[v.siblings.lastIndex] =
+ Text.literal(last.directLiteralStringContent!!.trimEnd())
+ .setStyle(last.style)
+ lore[index] = v
+ }
+ this.lore = lore
+ }
+
+ fun collapseWhitespaces() {
+ lore = (listOf(null as Text?) + lore).zipWithNext()
+ .filter { !it.first?.unformattedString.isNullOrBlank() || !it.second?.unformattedString.isNullOrBlank() }
+ .map { it.second!! }
+ }
+
+ fun deleteLineUntilNextSpace(search: (String) -> Boolean) {
+ val idx = lore.indexOfFirst { search(it.unformattedString) }
+ if (idx < 0) return
+ val l = lore.toMutableList()
+ val p = l.subList(idx, l.size)
+ val nextBlank = p.indexOfFirst { it.unformattedString.isEmpty() }
+ if (nextBlank < 0)
+ p.clear()
+ else
+ p.subList(0, nextBlank).clear()
+ lore = l
+ }
+
+ fun processNbt() {
+ // TODO: calculate hideflags
+ legacyNbt.put("HideFlags", NbtInt.of(254))
+ copyUnbreakable()
+ copyItemModel()
+ copyPotion()
+ copyExtraAttributes()
+ copyLegacySkullNbt()
+ copyDisplay()
+ copyColour()
+ copyEnchantments()
+ copyEnchantGlint()
+ // TODO: copyDisplay
+ }
+
+ private fun copyPotion() {
+ val effects = itemStack.get(DataComponentTypes.POTION_CONTENTS) ?: return
+ legacyNbt.put("CustomPotionEffects", NbtList().also {
+ effects.effects.forEach { effect ->
+ val effectId = effect.effectType.key.get().value.path
+ val duration = effect.duration
+ val legacyId = LegacyItemData.effectList[effectId]!!
+
+ it.add(NbtCompound().apply {
+ put("Ambient", NbtByte.of(false))
+ put("Duration", NbtInt.of(duration))
+ put("Id", NbtByte.of(legacyId.id.toByte()))
+ put("Amplifier", NbtByte.of(effect.amplifier.toByte()))
+ })
+ }
+ })
+ }
+
+ fun NbtCompound.getOrPutCompound(name: String): NbtCompound {
+ val compound = getCompoundOrEmpty(name)
+ put(name, compound)
+ return compound
+ }
+
+ private fun copyColour() {
+ if (!itemStack.isIn(ItemTags.DYEABLE)) {
+ itemStack.remove(DataComponentTypes.DYED_COLOR)
+ return
+ }
+ val leatherTint = itemStack.componentChanges.get(DataComponentTypes.DYED_COLOR)?.getOrNull() ?: return
+ legacyNbt.getOrPutCompound("display").put("color", NbtInt.of(leatherTint.rgb))
+ }
+
+ private fun copyItemModel() {
+ val itemModel = itemStack.get(DataComponentTypes.ITEM_MODEL) ?: return
+ legacyNbt.put("ItemModel", NbtString.of(itemModel.toString()))
+ }
+
+ private fun copyDisplay() {
+ legacyNbt.getOrPutCompound("display").apply {
+ put("Lore", lore.map { NbtString.of(it.getLegacyFormatString(trimmed = true)) }.toNbtList())
+ putString("Name", name.getLegacyFormatString(trimmed = true))
+ }
+ }
+
+ fun exportModernSnbt(): NbtElement {
+ val overlay = ItemStack.CODEC.encodeStart(NbtOps.INSTANCE, itemStack.copy().also {
+ it.modifyExtraAttributes { attribs ->
+ originalId.ifPresent { attribs.putString("id", it) }
+ attribs
+ }
+ }).orThrow
+ val overlayWithVersion =
+ ExportedTestConstantMeta.SOURCE_CODEC.encode(ExportedTestConstantMeta.current, NbtOps.INSTANCE, overlay)
+ .orThrow
+ return overlayWithVersion
+ }
+
+ fun prepare() {
+ preprocess()
+ processNbt()
+ itemStack.extraAttributes = extraAttribs
+ }
+
+ fun exportJson(): JsonElement {
+ return buildJsonObject {
+ val (itemId, damage) = legacyifyItemStack()
+ put("itemid", itemId)
+ put("displayname", name.getLegacyFormatString(trimmed = true))
+ put("nbttag", legacyNbt.toLegacyString())
+ put("damage", damage)
+ put("lore", lore.map { it.getLegacyFormatString(trimmed = true) }.toJsonArray())
+ val sbId = itemStack.skyBlockId
+ if (sbId == null)
+ warnings.add("Could not find skyblock id")
+ put("internalname", sbId?.neuItem)
+ put("clickcommand", "")
+ put("crafttext", "")
+ put("modver", "Firmament ${Firmament.version.friendlyString}")
+ put("infoType", "")
+ put("info", JsonArray(listOf()))
+ }
+
+ }
+
+ companion object {
+ fun createExporter(itemStack: ItemStack): LegacyItemExporter {
+ return LegacyItemExporter(itemStack.copy()).also { it.prepare() }
+ }
+
+ @Subscribe
+ fun load(event: ClientStartedEvent) {
+ thread(start = true, name = "ItemExporter Meta Load Thread") {
+ LegacyItemData.itemLut
+ }
+ }
+ }
+
+ fun copyEnchantGlint() {
+ if (itemStack.get(DataComponentTypes.ENCHANTMENT_GLINT_OVERRIDE) == true) {
+ val ench = legacyNbt.getListOrEmpty("ench")
+ legacyNbt.put("ench", ench)
+ }
+ }
+
+ private fun copyUnbreakable() {
+ if (itemStack.get(DataComponentTypes.UNBREAKABLE) == Unit.INSTANCE) {
+ legacyNbt.putBoolean("Unbreakable", true)
+ }
+ }
+
+ fun copyEnchantments() {
+ val enchantments = itemStack.get(DataComponentTypes.ENCHANTMENTS)?.takeIf { !it.isEmpty } ?: return
+ val enchTag = legacyNbt.getListOrEmpty("ench")
+ legacyNbt.put("ench", enchTag)
+ enchantments.enchantmentEntries.forEach { entry ->
+ val id = entry.key.key.get().value
+ val legacyId = LegacyItemData.enchantmentLut[id]
+ if (legacyId == null) {
+ warnings.add("Could not find legacy enchantment id for ${id}")
+ return@forEach
+ }
+ enchTag.add(NbtCompound().apply {
+ putShort("lvl", entry.intValue.toShort())
+ putShort(
+ "id",
+ legacyId.id.toShort()
+ )
+ })
+ }
+ }
+
+ fun copyExtraAttributes() {
+ legacyNbt.put("ExtraAttributes", extraAttribs)
+ }
+
+ fun copyLegacySkullNbt() {
+ val profile = itemStack.get(DataComponentTypes.PROFILE) ?: return
+ legacyNbt.put("SkullOwner", NbtCompound().apply {
+ profile.uuid.ifPresent {
+ putString("Id", it.toString())
+ }
+ putBoolean("hypixelPopulated", true)
+ put("Properties", NbtCompound().apply {
+ profile.properties().forEach { prop, value ->
+ val list = getListOrEmpty(prop)
+ put(prop, list)
+ list.add(NbtCompound().apply {
+ value.signature?.let {
+ putString("Signature", it)
+ }
+ putString("Value", value.value)
+ putString("Name", value.name)
+ })
+ }
+ })
+ })
+ }
+
+ fun legacyifyItemStack(): LegacyItemData.LegacyItemType {
+ // TODO: add a default here
+ return LegacyItemData.itemLut[itemStack.item]!!
+ }
+}
diff --git a/src/main/kotlin/features/diana/AncestralSpadeSolver.kt b/src/main/kotlin/features/diana/AncestralSpadeSolver.kt
index ff85c00..a2869f0 100644
--- a/src/main/kotlin/features/diana/AncestralSpadeSolver.kt
+++ b/src/main/kotlin/features/diana/AncestralSpadeSolver.kt
@@ -10,19 +10,16 @@ import moe.nea.firmament.events.SoundReceiveEvent
import moe.nea.firmament.events.WorldKeyboardEvent
import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.events.WorldRenderLastEvent
-import moe.nea.firmament.events.subscription.SubscriptionOwner
-import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SkyBlockIsland
-import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.WarpUtil
import moe.nea.firmament.util.render.RenderInWorldContext
import moe.nea.firmament.util.skyBlockId
import moe.nea.firmament.util.skyblock.SkyBlockItems
-object AncestralSpadeSolver : SubscriptionOwner {
+object AncestralSpadeSolver {
var lastDing = TimeMark.farPast()
private set
private val pitches = mutableListOf<Float>()
@@ -106,13 +103,11 @@ object AncestralSpadeSolver : SubscriptionOwner {
nextGuess?.let {
tinyBlock(it, 1f, 0x80FFFFFF.toInt())
// TODO: replace this
- color(1f, 1f, 0f, 1f)
- tracer(it, lineWidth = 3f)
+ tracer(it, lineWidth = 3f, color = 0x80FFFFFF.toInt())
}
if (particlePositions.size > 2 && lastDing.passedTime() < 10.seconds && nextGuess != null) {
// TODO: replace this // TODO: add toggle
- color(0f, 1f, 0f, 0.7f)
- line(particlePositions)
+ line(particlePositions, color = 0x80FFFFFF.toInt())
}
}
}
@@ -124,8 +119,4 @@ object AncestralSpadeSolver : SubscriptionOwner {
pitches.clear()
lastDing = TimeMark.farPast()
}
-
- override val delegateFeature: FirmamentFeature
- get() = DianaWaypoints
-
}
diff --git a/src/main/kotlin/features/diana/DianaWaypoints.kt b/src/main/kotlin/features/diana/DianaWaypoints.kt
index 6d87262..650e6f9 100644
--- a/src/main/kotlin/features/diana/DianaWaypoints.kt
+++ b/src/main/kotlin/features/diana/DianaWaypoints.kt
@@ -3,29 +3,29 @@ package moe.nea.firmament.features.diana
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.AttackBlockEvent
import moe.nea.firmament.events.UseBlockEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
-object DianaWaypoints : FirmamentFeature {
- override val identifier get() = "diana"
- override val config get() = TConfig
+object DianaWaypoints {
+ val identifier get() = "diana"
- object TConfig : ManagedConfig(identifier, Category.EVENTS) {
- val ancestralSpadeSolver by toggle("ancestral-spade") { true }
- val ancestralSpadeTeleport by keyBindingWithDefaultUnbound("ancestral-teleport")
- val nearbyWaypoints by toggle("nearby-waypoints") { true }
- }
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.EVENTS) {
+ val ancestralSpadeSolver by toggle("ancestral-spade") { true }
+ val ancestralSpadeTeleport by keyBindingWithDefaultUnbound("ancestral-teleport")
+ val nearbyWaypoints by toggle("nearby-waypoints") { true }
+ }
- @Subscribe
- fun onBlockUse(event: UseBlockEvent) {
- NearbyBurrowsSolver.onBlockClick(event.hitResult.blockPos)
- }
+ @Subscribe
+ fun onBlockUse(event: UseBlockEvent) {
+ NearbyBurrowsSolver.onBlockClick(event.hitResult.blockPos)
+ }
- @Subscribe
- fun onBlockAttack(event: AttackBlockEvent) {
- NearbyBurrowsSolver.onBlockClick(event.blockPos)
- }
+ @Subscribe
+ fun onBlockAttack(event: AttackBlockEvent) {
+ NearbyBurrowsSolver.onBlockClick(event.blockPos)
+ }
}
diff --git a/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt b/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt
index 2fb4002..e1fb856 100644
--- a/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt
+++ b/src/main/kotlin/features/diana/NearbyBurrowsSolver.kt
@@ -11,13 +11,11 @@ import moe.nea.firmament.events.ParticleSpawnEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.events.WorldRenderLastEvent
-import moe.nea.firmament.events.subscription.SubscriptionOwner
-import moe.nea.firmament.features.FirmamentFeature
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.collections.mutableMapWithMaxSize
import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld
-object NearbyBurrowsSolver : SubscriptionOwner {
+object NearbyBurrowsSolver {
private val recentlyDugBurrows: MutableMap<BlockPos, TimeMark> = mutableMapWithMaxSize(20)
@@ -134,9 +132,6 @@ object NearbyBurrowsSolver : SubscriptionOwner {
burrows.remove(blockPos)
lastBlockClick = blockPos
}
-
- override val delegateFeature: FirmamentFeature
- get() = DianaWaypoints
}
fun Position.toBlockPos(): BlockPos {
diff --git a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
index 5151862..80b30ee 100644
--- a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
+++ b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
@@ -1,9 +1,8 @@
-
package moe.nea.firmament.features.events.anniversity
import io.github.notenoughupdates.moulconfig.observer.ObservableList
import io.github.notenoughupdates.moulconfig.xml.Bind
-import moe.nea.jarvis.api.Point
+import org.joml.Vector2i
import kotlin.time.Duration.Companion.seconds
import net.minecraft.entity.passive.PigEntity
import net.minecraft.util.math.BlockPos
@@ -12,213 +11,213 @@ import moe.nea.firmament.events.EntityInteractionEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.events.WorldReadyEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.gui.hud.MoulConfigHud
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.ItemNameLookup
import moe.nea.firmament.repo.SBItemStack
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.parseShortNumber
import moe.nea.firmament.util.useMatch
-object AnniversaryFeatures : FirmamentFeature {
- override val identifier: String
- get() = "anniversary"
-
- object TConfig : ManagedConfig(identifier, Category.EVENTS) {
- val enableShinyPigTracker by toggle("shiny-pigs") {true}
- val trackPigCooldown by position("pig-hud", 200, 300) { Point(0.1, 0.2) }
- }
-
- override val config: ManagedConfig?
- get() = TConfig
-
- data class ClickedPig(
- val clickedAt: TimeMark,
- val startLocation: BlockPos,
- val pigEntity: PigEntity
- ) {
- @Bind("timeLeft")
- fun getTimeLeft(): Double = 1 - clickedAt.passedTime() / pigDuration
- }
-
- val clickedPigs = ObservableList<ClickedPig>(mutableListOf())
- var lastClickedPig: PigEntity? = null
-
- val pigDuration = 90.seconds
-
- @Subscribe
- fun onTick(event: TickEvent) {
- clickedPigs.removeIf { it.clickedAt.passedTime() > pigDuration }
- }
-
- val pattern = "SHINY! You extracted (?<reward>.*) from the piglet's orb!".toPattern()
-
- @Subscribe
- fun onChat(event: ProcessChatEvent) {
- if(!TConfig.enableShinyPigTracker)return
- if (event.unformattedString == "Oink! Bring the pig back to the Shiny Orb!") {
- val pig = lastClickedPig ?: return
- // TODO: store proper location based on the orb location, maybe
- val startLocation = pig.blockPos ?: return
- clickedPigs.add(ClickedPig(TimeMark.now(), startLocation, pig))
- lastClickedPig = null
- }
- if (event.unformattedString == "SHINY! The orb is charged! Click on it for loot!") {
- val player = MC.player ?: return
- val lowest =
- clickedPigs.minByOrNull { it.startLocation.getSquaredDistance(player.pos) } ?: return
- clickedPigs.remove(lowest)
- }
- pattern.useMatch(event.unformattedString) {
- val reward = group("reward")
- val parsedReward = parseReward(reward)
- addReward(parsedReward)
- PigCooldown.rewards.atOnce {
- PigCooldown.rewards.clear()
- rewards.mapTo(PigCooldown.rewards) { PigCooldown.DisplayReward(it) }
- }
- }
- }
-
- fun addReward(reward: Reward) {
- val it = rewards.listIterator()
- while (it.hasNext()) {
- val merged = reward.mergeWith(it.next()) ?: continue
- it.set(merged)
- return
- }
- rewards.add(reward)
- }
-
- val rewards = mutableListOf<Reward>()
-
- fun <T> ObservableList<T>.atOnce(block: () -> Unit) {
- val oldObserver = observer
- observer = null
- block()
- observer = oldObserver
- update()
- }
-
- sealed interface Reward {
- fun mergeWith(other: Reward): Reward?
- data class EXP(val amount: Double, val skill: String) : Reward {
- override fun mergeWith(other: Reward): Reward? {
- if (other is EXP && other.skill == skill)
- return EXP(amount + other.amount, skill)
- return null
- }
- }
-
- data class Coins(val amount: Double) : Reward {
- override fun mergeWith(other: Reward): Reward? {
- if (other is Coins)
- return Coins(other.amount + amount)
- return null
- }
- }
-
- data class Items(val amount: Int, val item: SkyblockId) : Reward {
- override fun mergeWith(other: Reward): Reward? {
- if (other is Items && other.item == item)
- return Items(amount + other.amount, item)
- return null
- }
- }
-
- data class Unknown(val text: String) : Reward {
- override fun mergeWith(other: Reward): Reward? {
- return null
- }
- }
- }
-
- val expReward = "\\+(?<exp>$SHORT_NUMBER_FORMAT) (?<kind>[^ ]+) XP".toPattern()
- val coinReward = "\\+(?<amount>$SHORT_NUMBER_FORMAT) coins".toPattern()
- val itemReward = "(?:(?<amount>[0-9]+)x )?(?<name>.*)".toPattern()
- fun parseReward(string: String): Reward {
- expReward.useMatch<Unit>(string) {
- val exp = parseShortNumber(group("exp"))
- val kind = group("kind")
- return Reward.EXP(exp, kind)
- }
- coinReward.useMatch<Unit>(string) {
- val coins = parseShortNumber(group("amount"))
- return Reward.Coins(coins)
- }
- itemReward.useMatch(string) {
- val amount = group("amount")?.toIntOrNull() ?: 1
- val name = group("name")
- val item = ItemNameLookup.guessItemByName(name, false) ?: return@useMatch
- return Reward.Items(amount, item)
- }
- return Reward.Unknown(string)
- }
-
- @Subscribe
- fun onWorldClear(event: WorldReadyEvent) {
- lastClickedPig = null
- clickedPigs.clear()
- }
-
- @Subscribe
- fun onEntityClick(event: EntityInteractionEvent) {
- if (event.entity is PigEntity) {
- lastClickedPig = event.entity
- }
- }
-
- @Subscribe
- fun init(event: WorldReadyEvent) {
- PigCooldown.forceInit()
- }
-
- object PigCooldown : MoulConfigHud("anniversary_pig", TConfig.trackPigCooldown) {
- override fun shouldRender(): Boolean {
- return clickedPigs.isNotEmpty() && TConfig.enableShinyPigTracker
- }
-
- @Bind("pigs")
- fun getPigs() = clickedPigs
-
- class DisplayReward(val backedBy: Reward) {
- @Bind
- fun count(): String {
- return when (backedBy) {
- is Reward.Coins -> backedBy.amount
- is Reward.EXP -> backedBy.amount
- is Reward.Items -> backedBy.amount
- is Reward.Unknown -> 0
- }.toString()
- }
-
- val itemStack = if (backedBy is Reward.Items) {
- SBItemStack(backedBy.item, backedBy.amount)
- } else {
- SBItemStack(SkyblockId.NULL)
- }
-
- @Bind
- fun name(): String {
- return when (backedBy) {
- is Reward.Coins -> "Coins"
- is Reward.EXP -> backedBy.skill
- is Reward.Items -> itemStack.asImmutableItemStack().name.string
- is Reward.Unknown -> backedBy.text
- }
- }
-
- @Bind
- fun isKnown() = backedBy !is Reward.Unknown
- }
-
- @get:Bind("rewards")
- val rewards = ObservableList<DisplayReward>(mutableListOf())
-
- }
+object AnniversaryFeatures {
+ val identifier: String
+ get() = "anniversary"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.EVENTS) {
+ val enableShinyPigTracker by toggle("shiny-pigs") { true }
+ val trackPigCooldown by position("pig-hud", 200, 300) { Vector2i(100, 200) }
+ }
+
+ data class ClickedPig(
+ val clickedAt: TimeMark,
+ val startLocation: BlockPos,
+ val pigEntity: PigEntity
+ ) {
+ @Bind("timeLeft")
+ fun getTimeLeft(): Double = 1 - clickedAt.passedTime() / pigDuration
+ }
+
+ val clickedPigs = ObservableList<ClickedPig>(mutableListOf())
+ var lastClickedPig: PigEntity? = null
+
+ val pigDuration = 90.seconds
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ clickedPigs.removeIf { it.clickedAt.passedTime() > pigDuration }
+ }
+
+ val pattern = "SHINY! You extracted (?<reward>.*) from the piglet's orb!".toPattern()
+
+ @Subscribe
+ fun onChat(event: ProcessChatEvent) {
+ if (!TConfig.enableShinyPigTracker) return
+ if (event.unformattedString == "Oink! Bring the pig back to the Shiny Orb!") {
+ val pig = lastClickedPig ?: return
+ // TODO: store proper location based on the orb location, maybe
+ val startLocation = pig.blockPos ?: return
+ clickedPigs.add(ClickedPig(TimeMark.now(), startLocation, pig))
+ lastClickedPig = null
+ }
+ if (event.unformattedString == "SHINY! The orb is charged! Click on it for loot!") {
+ val player = MC.player ?: return
+ val lowest =
+ clickedPigs.minByOrNull { it.startLocation.getSquaredDistance(player.pos) } ?: return
+ clickedPigs.remove(lowest)
+ }
+ pattern.useMatch(event.unformattedString) {
+ val reward = group("reward")
+ val parsedReward = parseReward(reward)
+ addReward(parsedReward)
+ PigCooldown.rewards.atOnce {
+ PigCooldown.rewards.clear()
+ rewards.mapTo(PigCooldown.rewards) { PigCooldown.DisplayReward(it) }
+ }
+ }
+ }
+
+ fun addReward(reward: Reward) {
+ val it = rewards.listIterator()
+ while (it.hasNext()) {
+ val merged = reward.mergeWith(it.next()) ?: continue
+ it.set(merged)
+ return
+ }
+ rewards.add(reward)
+ }
+
+ val rewards = mutableListOf<Reward>()
+
+ fun <T> ObservableList<T>.atOnce(block: () -> Unit) {
+ val oldObserver = observer
+ observer = null
+ block()
+ observer = oldObserver
+ update()
+ }
+
+ sealed interface Reward {
+ fun mergeWith(other: Reward): Reward?
+ data class EXP(val amount: Double, val skill: String) : Reward {
+ override fun mergeWith(other: Reward): Reward? {
+ if (other is EXP && other.skill == skill)
+ return EXP(amount + other.amount, skill)
+ return null
+ }
+ }
+
+ data class Coins(val amount: Double) : Reward {
+ override fun mergeWith(other: Reward): Reward? {
+ if (other is Coins)
+ return Coins(other.amount + amount)
+ return null
+ }
+ }
+
+ data class Items(val amount: Int, val item: SkyblockId) : Reward {
+ override fun mergeWith(other: Reward): Reward? {
+ if (other is Items && other.item == item)
+ return Items(amount + other.amount, item)
+ return null
+ }
+ }
+
+ data class Unknown(val text: String) : Reward {
+ override fun mergeWith(other: Reward): Reward? {
+ return null
+ }
+ }
+ }
+
+ val expReward = "\\+(?<exp>$SHORT_NUMBER_FORMAT) (?<kind>[^ ]+) XP".toPattern()
+ val coinReward = "(?i)\\+(?<amount>$SHORT_NUMBER_FORMAT) Coins".toPattern()
+ val itemReward = "(?:(?<amount>[0-9]+)x )?(?<name>.*)".toPattern()
+ fun parseReward(string: String): Reward {
+ expReward.useMatch<Unit>(string) {
+ val exp = parseShortNumber(group("exp"))
+ val kind = group("kind")
+ return Reward.EXP(exp, kind)
+ }
+ coinReward.useMatch<Unit>(string) {
+ val coins = parseShortNumber(group("amount"))
+ return Reward.Coins(coins)
+ }
+ itemReward.useMatch(string) {
+ val amount = group("amount")?.toIntOrNull() ?: 1
+ val name = group("name")
+ val item = ItemNameLookup.guessItemByName(name, false) ?: return@useMatch
+ return Reward.Items(amount, item)
+ }
+ return Reward.Unknown(string)
+ }
+
+ @Subscribe
+ fun onWorldClear(event: WorldReadyEvent) {
+ lastClickedPig = null
+ clickedPigs.clear()
+ }
+
+ @Subscribe
+ fun onEntityClick(event: EntityInteractionEvent) {
+ if (event.entity is PigEntity) {
+ lastClickedPig = event.entity
+ }
+ }
+
+ @Subscribe
+ fun init(event: WorldReadyEvent) {
+ PigCooldown.forceInit()
+ }
+
+ object PigCooldown : MoulConfigHud("anniversary_pig", TConfig.trackPigCooldown) {
+ override fun shouldRender(): Boolean {
+ return clickedPigs.isNotEmpty() && TConfig.enableShinyPigTracker
+ }
+
+ @Bind("pigs")
+ fun getPigs() = clickedPigs
+
+ class DisplayReward(val backedBy: Reward) {
+ @Bind
+ fun count(): String {
+ return when (backedBy) {
+ is Reward.Coins -> backedBy.amount
+ is Reward.EXP -> backedBy.amount
+ is Reward.Items -> backedBy.amount
+ is Reward.Unknown -> 0
+ }.toString()
+ }
+
+ val itemStack = if (backedBy is Reward.Items) {
+ SBItemStack(backedBy.item, backedBy.amount)
+ } else {
+ SBItemStack(SkyblockId.NULL)
+ }
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ @Bind
+ fun name(): String {
+ return when (backedBy) {
+ is Reward.Coins -> "Coins"
+ is Reward.EXP -> backedBy.skill
+ is Reward.Items -> itemStack.asImmutableItemStack().name.string
+ is Reward.Unknown -> backedBy.text
+ }
+ }
+
+ @Bind
+ fun isKnown() = backedBy !is Reward.Unknown
+ }
+
+ @get:Bind("rewards")
+ val rewards = ObservableList<DisplayReward>(mutableListOf())
+
+ }
}
diff --git a/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt
new file mode 100644
index 0000000..13ecd7b
--- /dev/null
+++ b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt
@@ -0,0 +1,65 @@
+package moe.nea.firmament.features.events.anniversity
+
+import java.util.Optional
+import me.shedaniel.math.Color
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.entity.player.PlayerEntity
+import net.minecraft.text.Style
+import net.minecraft.util.Formatting
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.EntityRenderTintEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.render.TintedOverlayTexture
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+
+object CenturyRaffleFeatures {
+ @Config
+ object TConfig : ManagedConfig("centuryraffle", Category.EVENTS) {
+ val highlightPlayersForSlice by toggle("highlight-cake-players") { true }
+// val highlightAllPlayers by toggle("highlight-all-cake-players") { true }
+ }
+
+ val cakeIcon = "⛃"
+
+ val cakeColors = listOf(
+ CakeTeam(SkyBlockItems.SLICE_OF_BLUEBERRY_CAKE, Formatting.BLUE),
+ CakeTeam(SkyBlockItems.SLICE_OF_CHEESECAKE, Formatting.YELLOW),
+ CakeTeam(SkyBlockItems.SLICE_OF_GREEN_VELVET_CAKE, Formatting.GREEN),
+ CakeTeam(SkyBlockItems.SLICE_OF_RED_VELVET_CAKE, Formatting.RED),
+ CakeTeam(SkyBlockItems.SLICE_OF_STRAWBERRY_SHORTCAKE, Formatting.LIGHT_PURPLE),
+ )
+
+ data class CakeTeam(
+ val id: SkyblockId,
+ val formatting: Formatting,
+ ) {
+ val searchedTextRgb = formatting.colorValue!!
+ val brightenedRgb = Color.ofOpaque(searchedTextRgb)//.brighter(2.0)
+ val tintOverlay by lazy {
+ TintedOverlayTexture().setColor(brightenedRgb)
+ }
+ }
+
+ val sliceToColor = cakeColors.associateBy { it.id }
+
+ @Subscribe
+ fun onEntityRender(event: EntityRenderTintEvent) {
+ if (!TConfig.highlightPlayersForSlice) return
+ val requestedCakeTeam = sliceToColor[MC.stackInHand?.skyBlockId] ?: return
+ // TODO: cache the requested color
+ val player = event.entity as? PlayerEntity ?: return
+ val cakeColor: Style = player.styledDisplayName.visit(
+ { style, text ->
+ if (text == cakeIcon) Optional.of(style)
+ else Optional.empty()
+ }, Style.EMPTY).getOrNull() ?: return
+ if (cakeColor.color?.rgb == requestedCakeTeam.searchedTextRgb) {
+ event.renderState.overlayTexture_firmament = requestedCakeTeam.tintOverlay
+ }
+ }
+
+}
diff --git a/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt b/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt
index 840fb8c..3f149ff 100644
--- a/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt
+++ b/src/main/kotlin/features/events/carnival/CarnivalFeatures.kt
@@ -1,17 +1,16 @@
package moe.nea.firmament.features.events.carnival
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
-object CarnivalFeatures : FirmamentFeature {
- object TConfig : ManagedConfig(identifier, Category.EVENTS) {
+object CarnivalFeatures {
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.EVENTS) {
val enableBombSolver by toggle("bombs-solver") { true }
val displayTutorials by toggle("tutorials") { true }
}
- override val config: ManagedConfig?
- get() = TConfig
- override val identifier: String
+ val identifier: String
get() = "carnival"
}
diff --git a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
index 1824225..3baf5a5 100644
--- a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
+++ b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
@@ -2,7 +2,7 @@
package moe.nea.firmament.features.events.carnival
import io.github.notenoughupdates.moulconfig.observer.ObservableList
-import io.github.notenoughupdates.moulconfig.platform.ModernItemStack
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform
import io.github.notenoughupdates.moulconfig.xml.Bind
import java.util.UUID
import net.minecraft.block.Blocks
@@ -120,7 +120,7 @@ object MinesweeperHelper {
.setSkyBlockFirmamentUiId("MINESWEEPER_$name")
@Bind
- fun getIcon() = ModernItemStack.of(itemStack)
+ fun getIcon() = MoulConfigPlatform.wrap(itemStack)
@Bind
fun pieceLabel() = fruitColor.formattingCode + fruitName
@@ -158,7 +158,7 @@ object MinesweeperHelper {
;
@Bind("itemType")
- fun getItemStack() = ModernItemStack.of(ItemStack(itemType))
+ fun getItemStack() = MoulConfigPlatform.wrap(ItemStack(itemType))
companion object {
val id = SkyblockId("CARNIVAL_SHOVEL")
@@ -222,7 +222,7 @@ object MinesweeperHelper {
fun onChat(event: ProcessChatEvent) {
if (CarnivalFeatures.TConfig.displayTutorials && event.unformattedString == startGameQuestion) {
MC.sendChat(Text.translatable("firmament.carnival.tutorial.minesweeper").styled {
- it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, "/firm minesweepertutorial"))
+ it.withClickEvent(ClickEvent.RunCommand("/firm minesweepertutorial"))
})
}
if (!CarnivalFeatures.TConfig.enableBombSolver) {
@@ -259,7 +259,7 @@ object MinesweeperHelper {
val boardPosition = BoardPosition.fromBlockPos(event.blockPos)
log.log { "Breaking block at ${event.blockPos} ($boardPosition)" }
gs.lastClickedPosition = boardPosition
- gs.lastDowsingMode = DowsingMode.fromItem(event.player.inventory.mainHandStack)
+ gs.lastDowsingMode = DowsingMode.fromItem(event.player.mainHandStack)
}
@Subscribe
diff --git a/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt b/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt
index 76f6ed4..55592c5 100644
--- a/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt
+++ b/src/main/kotlin/features/fixes/CompatibliltyFeatures.kt
@@ -4,22 +4,20 @@ import net.minecraft.particle.ParticleTypes
import net.minecraft.util.math.Vec3d
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ParticleSpawnEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.compatloader.CompatLoader
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
-object CompatibliltyFeatures : FirmamentFeature {
- override val identifier: String
+object CompatibliltyFeatures {
+ val identifier: String
get() = "compatibility"
+ @Config
object TConfig : ManagedConfig(identifier, Category.INTEGRATIONS) {
val enhancedExplosions by toggle("explosion-enabled") { false }
val explosionSize by integer("explosion-power", 10, 50) { 1 }
}
- override val config: ManagedConfig?
- get() = TConfig
-
interface ExplosiveApiWrapper {
fun spawnParticle(vec3d: Vec3d, power: Float)
diff --git a/src/main/kotlin/features/fixes/Fixes.kt b/src/main/kotlin/features/fixes/Fixes.kt
index 5d70b1a..e7027ac 100644
--- a/src/main/kotlin/features/fixes/Fixes.kt
+++ b/src/main/kotlin/features/fixes/Fixes.kt
@@ -1,71 +1,80 @@
-
-
package moe.nea.firmament.features.fixes
-import moe.nea.jarvis.api.Point
+import org.joml.Vector2i
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
import net.minecraft.client.MinecraftClient
import net.minecraft.client.option.KeyBinding
-import net.minecraft.entity.player.PlayerEntity
import net.minecraft.text.Text
-import net.minecraft.util.Arm
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HudRenderEvent
import moe.nea.firmament.events.WorldKeyboardEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
-import moe.nea.firmament.util.errorBoundary
-
-object Fixes : FirmamentFeature {
- override val identifier: String
- get() = "fixes"
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.tr
- object TConfig : ManagedConfig(identifier, Category.MISC) { // TODO: split this config
- val fixUnsignedPlayerSkins by toggle("player-skins") { true }
- var autoSprint by toggle("auto-sprint") { false }
- val autoSprintKeyBinding by keyBindingWithDefaultUnbound("auto-sprint-keybinding")
- val autoSprintHud by position("auto-sprint-hud", 80, 10) { Point(0.0, 1.0) }
- val peekChat by keyBindingWithDefaultUnbound("peek-chat")
- }
+object Fixes {
+ val identifier: String
+ get() = "fixes"
- override val config: ManagedConfig
- get() = TConfig
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.MISC) { // TODO: split this config
+ val fixUnsignedPlayerSkins by toggle("player-skins") { true }
+ var autoSprint by toggle("auto-sprint") { false }
+ val autoSprintKeyBinding by keyBindingWithDefaultUnbound("auto-sprint-keybinding")
+ val autoSprintUnderWater by toggle("auto-sprint-underwater") { true }
+ val autoSprintHud by position("auto-sprint-hud", 80, 10) { Vector2i() }
+ val peekChat by keyBindingWithDefaultUnbound("peek-chat")
+ val hidePotionEffects by toggle("hide-mob-effects") { false }
+ val hidePotionEffectsHud by toggle("hide-potion-effects-hud") { false }
+ val noHurtCam by toggle("disable-hurt-cam") { false }
+ val hideSlotHighlights by toggle("hide-slot-highlights") { false }
+ val hideRecipeBook by toggle("hide-recipe-book") { false }
+ val hideOffHand by toggle("hide-off-hand") { false }
+ }
- fun handleIsPressed(
- keyBinding: KeyBinding,
- cir: CallbackInfoReturnable<Boolean>
- ) {
- if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true)
- cir.returnValue = true
- }
+ fun handleIsPressed(
+ keyBinding: KeyBinding,
+ cir: CallbackInfoReturnable<Boolean>
+ ) {
+ if (keyBinding !== MinecraftClient.getInstance().options.sprintKey) return
+ if (!TConfig.autoSprint) return
+ val player = MC.player ?: return
+ if (player.isSprinting) return
+ if (!TConfig.autoSprintUnderWater && player.isTouchingWater) return
+ cir.returnValue = true
+ }
- @Subscribe
- fun onRenderHud(it: HudRenderEvent) {
- if (!TConfig.autoSprintKeyBinding.isBound) return
- it.context.matrices.push()
- TConfig.autoSprintHud.applyTransformations(it.context.matrices)
- it.context.drawText(
- MC.font, Text.translatable(
- if (TConfig.autoSprint)
- "firmament.fixes.auto-sprint.on"
- else if (MC.player?.isSprinting == true)
- "firmament.fixes.auto-sprint.sprinting"
- else
- "firmament.fixes.auto-sprint.not-sprinting"
- ), 0, 0, -1, false
- )
- it.context.matrices.pop()
- }
+ @Subscribe
+ fun onRenderHud(it: HudRenderEvent) {
+ if (!TConfig.autoSprintKeyBinding.isBound) return
+ it.context.matrices.pushMatrix()
+ TConfig.autoSprintHud.applyTransformations(it.context.matrices)
+ it.context.drawText(
+ MC.font, (
+ if (MC.player?.isSprinting == true) {
+ Text.translatable("firmament.fixes.auto-sprint.sprinting")
+ } else if (TConfig.autoSprint) {
+ if (!TConfig.autoSprintUnderWater && MC.player?.isTouchingWater == true)
+ tr("firmament.fixes.auto-sprint.under-water", "In Water")
+ else
+ Text.translatable("firmament.fixes.auto-sprint.on")
+ } else {
+ Text.translatable("firmament.fixes.auto-sprint.not-sprinting")
+ }
+ ), 0, 0, -1, true
+ )
+ it.context.matrices.popMatrix()
+ }
- @Subscribe
- fun onWorldKeyboard(it: WorldKeyboardEvent) {
- if (it.matches(TConfig.autoSprintKeyBinding)) {
- TConfig.autoSprint = !TConfig.autoSprint
- }
- }
+ @Subscribe
+ fun onWorldKeyboard(it: WorldKeyboardEvent) {
+ if (it.matches(TConfig.autoSprintKeyBinding)) {
+ TConfig.autoSprint = !TConfig.autoSprint
+ }
+ }
- fun shouldPeekChat(): Boolean {
- return TConfig.peekChat.isPressed(atLeast = true)
- }
+ fun shouldPeekChat(): Boolean {
+ return TConfig.peekChat.isPressed(atLeast = true)
+ }
}
diff --git a/src/main/kotlin/features/garden/HideComposterNoises.kt b/src/main/kotlin/features/garden/HideComposterNoises.kt
new file mode 100644
index 0000000..843e4f9
--- /dev/null
+++ b/src/main/kotlin/features/garden/HideComposterNoises.kt
@@ -0,0 +1,34 @@
+package moe.nea.firmament.features.garden
+
+import net.minecraft.entity.passive.WolfSoundVariants
+import net.minecraft.sound.SoundEvent
+import net.minecraft.sound.SoundEvents
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.SoundReceiveEvent
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.SkyBlockIsland
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+
+object HideComposterNoises {
+ @Config
+ object TConfig : ManagedConfig("composter", Category.GARDEN) {
+ val hideComposterNoises by toggle("no-more-noises") { false }
+ }
+
+ val composterSoundEvents: List<SoundEvent> = listOf(
+ SoundEvents.BLOCK_PISTON_EXTEND,
+ SoundEvents.BLOCK_WATER_AMBIENT,
+ SoundEvents.ENTITY_CHICKEN_EGG,
+ SoundEvents.WOLF_SOUNDS[WolfSoundVariants.Type.CLASSIC]!!.growlSound().value(),
+ )
+
+ @Subscribe
+ fun onNoise(event: SoundReceiveEvent) {
+ if (!TConfig.hideComposterNoises) return
+ if (SBData.skyblockLocation == SkyBlockIsland.GARDEN) {
+ if (event.sound.value() in composterSoundEvents)
+ event.cancel()
+ }
+ }
+}
diff --git a/src/main/kotlin/features/inventory/CraftingOverlay.kt b/src/main/kotlin/features/inventory/CraftingOverlay.kt
index d2c79fd..30d2c6b 100644
--- a/src/main/kotlin/features/inventory/CraftingOverlay.kt
+++ b/src/main/kotlin/features/inventory/CraftingOverlay.kt
@@ -7,12 +7,12 @@ import net.minecraft.util.Formatting
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.SlotRenderEvents
-import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.SBItemStack
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.skyblockId
-object CraftingOverlay : FirmamentFeature {
+object CraftingOverlay {
private var screen: GenericContainerScreen? = null
private var recipe: NEUCraftingRecipe? = null
@@ -42,9 +42,10 @@ object CraftingOverlay : FirmamentFeature {
}
}
- override val identifier: String
+ val identifier: String
get() = "crafting-overlay"
+ @OptIn(ExpensiveItemCacheApi::class)
@Subscribe
fun onSlotRender(event: SlotRenderEvents.After) {
val slot = event.slot
diff --git a/src/main/kotlin/features/inventory/ItemHotkeys.kt b/src/main/kotlin/features/inventory/ItemHotkeys.kt
index 4aa8202..e9d0631 100644
--- a/src/main/kotlin/features/inventory/ItemHotkeys.kt
+++ b/src/main/kotlin/features/inventory/ItemHotkeys.kt
@@ -2,22 +2,26 @@ package moe.nea.firmament.features.inventory
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.HypixelStaticData
-import moe.nea.firmament.repo.ItemCache
import moe.nea.firmament.repo.ItemCache.asItemStack
import moe.nea.firmament.repo.ItemCache.isBroken
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.asBazaarStock
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.focusedItemStack
import moe.nea.firmament.util.skyBlockId
import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName
object ItemHotkeys {
+ @Config
object TConfig : ManagedConfig("item-hotkeys", Category.INVENTORY) {
val openGlobalTradeInterface by keyBindingWithDefaultUnbound("global-trade-interface")
}
+ @OptIn(ExpensiveItemCacheApi::class)
@Subscribe
fun onHandledInventoryPress(event: HandledScreenKeyPressedEvent) {
if (!event.matches(TConfig.openGlobalTradeInterface)) {
@@ -26,7 +30,7 @@ object ItemHotkeys {
var item = event.screen.focusedItemStack ?: return
val skyblockId = item.skyBlockId ?: return
item = RepoManager.getNEUItem(skyblockId)?.asItemStack()?.takeIf { !it.isBroken } ?: item
- if (HypixelStaticData.hasBazaarStock(skyblockId)) {
+ if (HypixelStaticData.hasBazaarStock(skyblockId.asBazaarStock)) {
MC.sendCommand("bz ${item.getSearchName()}")
} else if (HypixelStaticData.hasAuctionHouseOffers(skyblockId)) {
MC.sendCommand("ahs ${item.getSearchName()}")
diff --git a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt
index d2c555b..7a474f9 100644
--- a/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt
+++ b/src/main/kotlin/features/inventory/ItemRarityCosmetics.kt
@@ -1,46 +1,28 @@
package moe.nea.firmament.features.inventory
import java.awt.Color
+import net.minecraft.client.gl.RenderPipelines
import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.render.RenderLayer
import net.minecraft.item.ItemStack
-import net.minecraft.util.Formatting
import net.minecraft.util.Identifier
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HotbarItemRenderEvent
import moe.nea.firmament.events.SlotRenderEvents
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
-import moe.nea.firmament.util.collections.lastNotNullOfOrNull
-import moe.nea.firmament.util.collections.memoizeIdentity
-import moe.nea.firmament.util.mc.loreAccordingToNbt
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.skyblock.Rarity
-import moe.nea.firmament.util.unformattedString
-object ItemRarityCosmetics : FirmamentFeature {
- override val identifier: String
+object ItemRarityCosmetics {
+ val identifier: String
get() = "item-rarity-cosmetics"
+ @Config
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
val showItemRarityBackground by toggle("background") { false }
val showItemRarityInHotbar by toggle("background-hotbar") { false }
}
- 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
}
@@ -49,7 +31,7 @@ object ItemRarityCosmetics : FirmamentFeature {
val rarity = Rarity.fromItem(item) ?: return
val rgb = rarityToColor[rarity] ?: 0xFF00FF80.toInt()
drawContext.drawGuiTexture(
- RenderLayer::getGuiTextured,
+ RenderPipelines.GUI_TEXTURED,
Identifier.of("firmament:item_rarity_background"),
x, y,
16, 16,
diff --git a/src/main/kotlin/features/inventory/JunkHighlighter.kt b/src/main/kotlin/features/inventory/JunkHighlighter.kt
new file mode 100644
index 0000000..45d265e
--- /dev/null
+++ b/src/main/kotlin/features/inventory/JunkHighlighter.kt
@@ -0,0 +1,30 @@
+package moe.nea.firmament.features.inventory
+
+import org.lwjgl.glfw.GLFW
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.SlotRenderEvents
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName
+import moe.nea.firmament.util.useMatch
+
+object JunkHighlighter {
+ val identifier: String
+ get() = "junk-highlighter"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
+ val junkRegex by string("regex") { "" }
+ val highlightBind by keyBinding("highlight") { GLFW.GLFW_KEY_LEFT_CONTROL }
+ }
+
+ @Subscribe
+ fun onDrawSlot(event: SlotRenderEvents.After) {
+ if (!TConfig.highlightBind.isPressed() || TConfig.junkRegex.isEmpty()) return
+ val junkRegex = TConfig.junkRegex.toPattern()
+ val slot = event.slot
+ junkRegex.useMatch(slot.stack.getSearchName()) {
+ event.context.fill(slot.x, slot.y, slot.x + 16, slot.y + 16, 0xffff0000.toInt())
+ }
+ }
+}
diff --git a/src/main/kotlin/features/inventory/PetFeatures.kt b/src/main/kotlin/features/inventory/PetFeatures.kt
index 5ca10f7..965e705 100644
--- a/src/main/kotlin/features/inventory/PetFeatures.kt
+++ b/src/main/kotlin/features/inventory/PetFeatures.kt
@@ -1,27 +1,42 @@
package moe.nea.firmament.features.inventory
-import net.minecraft.util.Identifier
+import org.joml.Vector2i
+import net.minecraft.item.ItemStack
+import net.minecraft.text.Text
+import net.minecraft.util.Formatting
+import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
import moe.nea.firmament.events.SlotRenderEvents
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.jarvis.JarvisIntegration
+import moe.nea.firmament.util.FirmFormatters.formatPercent
+import moe.nea.firmament.util.FirmFormatters.shortFormat
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.petData
import moe.nea.firmament.util.render.drawGuiTexture
+import moe.nea.firmament.util.skyblock.Rarity
+import moe.nea.firmament.util.titleCase
import moe.nea.firmament.util.useMatch
+import moe.nea.firmament.util.withColor
-object PetFeatures : FirmamentFeature {
- override val identifier: String
+object PetFeatures {
+ val identifier: String
get() = "pets"
- override val config: ManagedConfig?
- get() = TConfig
-
+ @Config
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
val highlightEquippedPet by toggle("highlight-pet") { true }
+ var petOverlay by toggle("pet-overlay") { false }
+ val petOverlayHud by position("pet-overlay-hud", 80, 10) {
+ Vector2i()
+ }
}
val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern()
+ var petItemStack: ItemStack? = null
@Subscribe
fun onSlotRender(event: SlotRenderEvents.Before) {
@@ -29,12 +44,67 @@ object PetFeatures : FirmamentFeature {
val stack = event.slot.stack
if (stack.petData?.active == true)
petMenuTitle.useMatch(MC.screenName ?: return) {
+ petItemStack = stack
event.context.drawGuiTexture(
- event.slot.x, event.slot.y, 0, 16, 16,
- Identifier.of("firmament:selected_pet_background")
+ Firmament.identifier("selected_pet_background"),
+ event.slot.x, event.slot.y, 16, 16,
)
}
}
+ @Subscribe
+ fun onRenderHud(it: HudRenderEvent) {
+ if (!TConfig.petOverlay || !SBData.isOnSkyblock) return
+ val itemStack = petItemStack ?: return
+ val petData = petItemStack?.petData ?: return
+ val rarity = Rarity.fromNeuRepo(petData.tier)
+ val rarityCode = Rarity.colourMap[rarity] ?: Formatting.WHITE
+ val xp = petData.level
+ val petType = titleCase(petData.type)
+ val heldItem = petData.heldItem?.let { item -> "Held Item: ${titleCase(item)}" }
+
+ it.context.matrices.pushMatrix()
+ TConfig.petOverlayHud.applyTransformations(JarvisIntegration.jarvis, it.context.matrices)
+
+ val lines = mutableListOf<Text>()
+ it.context.matrices.pushMatrix()
+ it.context.matrices.translate(-0.5F, -0.5F)
+ it.context.matrices.scale(2f, 2f)
+ it.context.drawItem(itemStack, 0, 0)
+ it.context.matrices.popMatrix()
+ lines.add(Text.literal("[Lvl ${xp.currentLevel}] ").append(Text.literal(petType).withColor(rarityCode)))
+ if (heldItem != null) lines.add(Text.literal(heldItem))
+ if (xp.currentLevel != xp.maxLevel) lines.add(
+ Text.literal(
+ "Required L${xp.currentLevel + 1}: ${shortFormat(xp.expInCurrentLevel.toDouble())}/${
+ shortFormat(
+ xp.expRequiredForNextLevel.toDouble()
+ )
+ } (${formatPercent(xp.percentageToNextLevel.toDouble())})"
+ )
+ )
+ lines.add(
+ Text.literal(
+ "Required L100: ${shortFormat(xp.expTotal.toDouble())}/${shortFormat(xp.expRequiredForMaxLevel.toDouble())} (${
+ formatPercent(
+ xp.percentageToMaxLevel.toDouble()
+ )
+ })"
+ )
+ )
+
+ for ((index, line) in lines.withIndex()) {
+ it.context.drawText(
+ MC.font,
+ line.copy().withColor(Formatting.GRAY),
+ 36,
+ MC.font.fontHeight * index,
+ -1,
+ true
+ )
+ }
+
+ it.context.matrices.popMatrix()
+ }
}
diff --git a/src/main/kotlin/features/inventory/PriceData.kt b/src/main/kotlin/features/inventory/PriceData.kt
index 4477203..5f9268e 100644
--- a/src/main/kotlin/features/inventory/PriceData.kt
+++ b/src/main/kotlin/features/inventory/PriceData.kt
@@ -1,51 +1,120 @@
-
-
package moe.nea.firmament.features.inventory
+import org.lwjgl.glfw.GLFW
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.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.repo.HypixelStaticData
-import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.FirmFormatters.formatCommas
+import moe.nea.firmament.util.asBazaarStock
+import moe.nea.firmament.util.bold
+import moe.nea.firmament.util.darkGrey
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.getLogicalStackSize
+import moe.nea.firmament.util.gold
import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.tr
+import moe.nea.firmament.util.yellow
+
+object PriceData {
+ val identifier: String
+ get() = "price-data"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
+ val tooltipEnabled by toggle("enable-always") { true }
+ val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind")
+ val stackSizeKey by keyBinding("stack-size-keybind") { GLFW.GLFW_KEY_LEFT_SHIFT }
+ val avgLowestBin by choice(
+ "avg-lowest-bin-days",
+ ) {
+ AvgLowestBin.THREEDAYAVGLOWESTBIN
+ }
+ }
-object PriceData : FirmamentFeature {
- override val identifier: String
- get() = "price-data"
+ enum class AvgLowestBin : StringIdentifiable {
+ OFF,
+ ONEDAYAVGLOWESTBIN,
+ THREEDAYAVGLOWESTBIN,
+ SEVENDAYAVGLOWESTBIN;
- object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
- val tooltipEnabled by toggle("enable-always") { true }
- val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind")
- }
+ override fun asString(): String {
+ return name
+ }
+ }
- override val config get() = TConfig
+ fun formatPrice(label: Text, price: Double): Text {
+ return Text.literal("")
+ .yellow()
+ .bold()
+ .append(label)
+ .append(": ")
+ .append(
+ Text.literal(formatCommas(price, fractionalDigits = 1))
+ .append(if (price != 1.0) " coins" else " coin")
+ .gold()
+ .bold()
+ )
+ }
- @Subscribe
- fun onItemTooltip(it: ItemTooltipEvent) {
- if (!TConfig.tooltipEnabled && !TConfig.enableKeybinding.isPressed()) {
- return
- }
- val sbId = it.stack.skyBlockId
- val bazaarData = HypixelStaticData.bazaarData[sbId]
- val lowestBin = HypixelStaticData.lowestBin[sbId]
- if (bazaarData != null) {
- it.lines.add(Text.literal(""))
- it.lines.add(
- Text.stringifiedTranslatable("firmament.tooltip.bazaar.sell-order",
- FirmFormatters.formatCommas(bazaarData.quickStatus.sellPrice, 1))
- )
- it.lines.add(
- Text.stringifiedTranslatable("firmament.tooltip.bazaar.buy-order",
- FirmFormatters.formatCommas(bazaarData.quickStatus.buyPrice, 1))
- )
- } else if (lowestBin != null) {
- it.lines.add(Text.literal(""))
- it.lines.add(
- Text.stringifiedTranslatable("firmament.tooltip.ah.lowestbin",
- FirmFormatters.formatCommas(lowestBin, 1))
- )
- }
- }
+ @Subscribe
+ fun onItemTooltip(it: ItemTooltipEvent) {
+ if (!TConfig.tooltipEnabled) return
+ if (TConfig.enableKeybinding.isBound && !TConfig.enableKeybinding.isPressed()) return
+ val sbId = it.stack.skyBlockId
+ val stackSize = it.stack.getLogicalStackSize()
+ val isShowingStack = TConfig.stackSizeKey.isPressed()
+ val multiplier = if (isShowingStack) stackSize else 1
+ val multiplierText =
+ if (isShowingStack)
+ tr("firmament.tooltip.multiply", "Showing prices for x${stackSize}").darkGrey()
+ else
+ tr(
+ "firmament.tooltip.multiply.hint",
+ "[${TConfig.stackSizeKey.format()}] to show x${stackSize}"
+ ).darkGrey()
+ val bazaarData = HypixelStaticData.bazaarData[sbId?.asBazaarStock]
+ val lowestBin = HypixelStaticData.lowestBin[sbId]
+ val avgBinValue: Double? = when (TConfig.avgLowestBin) {
+ AvgLowestBin.ONEDAYAVGLOWESTBIN -> HypixelStaticData.avg1dlowestBin[sbId]
+ AvgLowestBin.THREEDAYAVGLOWESTBIN -> HypixelStaticData.avg3dlowestBin[sbId]
+ AvgLowestBin.SEVENDAYAVGLOWESTBIN -> HypixelStaticData.avg7dlowestBin[sbId]
+ AvgLowestBin.OFF -> null
+ }
+ if (bazaarData != null) {
+ it.lines.add(Text.literal(""))
+ it.lines.add(multiplierText)
+ it.lines.add(
+ formatPrice(
+ tr("firmament.tooltip.bazaar.buy-order", "Bazaar Buy Order"),
+ bazaarData.quickStatus.sellPrice * multiplier
+ )
+ )
+ it.lines.add(
+ formatPrice(
+ tr("firmament.tooltip.bazaar.sell-order", "Bazaar Sell Order"),
+ bazaarData.quickStatus.buyPrice * multiplier
+ )
+ )
+ } else if (lowestBin != null) {
+ it.lines.add(Text.literal(""))
+ it.lines.add(multiplierText)
+ it.lines.add(
+ formatPrice(
+ tr("firmament.tooltip.ah.lowestbin", "Lowest BIN"),
+ lowestBin * multiplier
+ )
+ )
+ if (avgBinValue != null) {
+ it.lines.add(
+ formatPrice(
+ tr("firmament.tooltip.ah.avg-lowestbin", "AVG Lowest BIN"),
+ avgBinValue * multiplier
+ )
+ )
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/features/inventory/REIDependencyWarner.kt b/src/main/kotlin/features/inventory/REIDependencyWarner.kt
index 1e9b1b8..9e8a4db 100644
--- a/src/main/kotlin/features/inventory/REIDependencyWarner.kt
+++ b/src/main/kotlin/features/inventory/REIDependencyWarner.kt
@@ -1,5 +1,6 @@
package moe.nea.firmament.features.inventory
+import java.net.URI
import net.fabricmc.loader.api.FabricLoader
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
@@ -30,7 +31,7 @@ object REIDependencyWarner {
var sentWarning = false
fun modrinthLink(slug: String) =
- "https://modrinth.com/mod/$slug/versions?g=${SharedConstants.getGameVersion().name}&l=fabric"
+ "https://modrinth.com/mod/$slug/versions?g=${SharedConstants.getGameVersion().name()}&l=fabric"
fun downloadButton(modName: String, modId: String, slug: String): Text {
val alreadyDownloaded = FabricLoader.getInstance().isModLoaded(modId)
@@ -38,7 +39,7 @@ object REIDependencyWarner {
.white()
.append(Text.literal("[").aqua())
.append(Text.translatable("firmament.download", modName)
- .styled { it.withClickEvent(ClickEvent(ClickEvent.Action.OPEN_URL, modrinthLink(slug))) }
+ .styled { it.withClickEvent(ClickEvent.OpenUrl(URI (modrinthLink(slug)))) }
.yellow()
.also {
if (alreadyDownloaded)
@@ -51,6 +52,7 @@ object REIDependencyWarner {
@Subscribe
fun checkREIDependency(event: SkyblockServerUpdateEvent) {
if (!SBData.isOnSkyblock) return
+ if (!RepoManager.TConfig.warnForMissingItemListMod) return
if (hasREI) return
if (sentWarning) return
sentWarning = true
@@ -74,8 +76,8 @@ object REIDependencyWarner {
if (hasREI) return
event.subcommand("disablereiwarning") {
thenExecute {
- RepoManager.Config.warnForMissingItemListMod = false
- RepoManager.Config.save()
+ RepoManager.TConfig.warnForMissingItemListMod = false
+ RepoManager.TConfig.markDirty()
MC.sendChat(Text.translatable("firmament.reiwarning.disabled").yellow())
}
}
diff --git a/src/main/kotlin/features/inventory/SaveCursorPosition.kt b/src/main/kotlin/features/inventory/SaveCursorPosition.kt
index c47867b..3e55d02 100644
--- a/src/main/kotlin/features/inventory/SaveCursorPosition.kt
+++ b/src/main/kotlin/features/inventory/SaveCursorPosition.kt
@@ -1,66 +1,57 @@
-
-
package moe.nea.firmament.features.inventory
+import org.lwjgl.glfw.GLFW
import kotlin.math.absoluteValue
import kotlin.time.Duration.Companion.milliseconds
-import net.minecraft.client.util.InputUtil
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.assertNotNullOr
-
-object SaveCursorPosition : FirmamentFeature {
- override val identifier: String
- get() = "save-cursor-position"
-
- object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
- val enable by toggle("enable") { true }
- val tolerance by duration("tolerance", 10.milliseconds, 5000.milliseconds) { 500.milliseconds }
- }
-
- override val config: TConfig
- get() = TConfig
-
- var savedPositionedP1: Pair<Double, Double>? = null
- var savedPosition: SavedPosition? = null
-
- data class SavedPosition(
- val middle: Pair<Double, Double>,
- val cursor: Pair<Double, Double>,
- val savedAt: TimeMark = TimeMark.now()
- )
-
- @JvmStatic
- fun saveCursorOriginal(positionedX: Double, positionedY: Double) {
- savedPositionedP1 = Pair(positionedX, positionedY)
- }
-
- @JvmStatic
- fun loadCursor(middleX: Double, middleY: Double): Pair<Double, Double>? {
- if (!TConfig.enable) return null
- val lastPosition = savedPosition?.takeIf { it.savedAt.passedTime() < TConfig.tolerance }
- savedPosition = null
- if (lastPosition != null &&
- (lastPosition.middle.first - middleX).absoluteValue < 1 &&
- (lastPosition.middle.second - middleY).absoluteValue < 1
- ) {
- InputUtil.setCursorParameters(
- MC.window.handle,
- InputUtil.GLFW_CURSOR_NORMAL,
- lastPosition.cursor.first,
- lastPosition.cursor.second
- )
- return lastPosition.cursor
- }
- return null
- }
-
- @JvmStatic
- fun saveCursorMiddle(middleX: Double, middleY: Double) {
- if (!TConfig.enable) return
- val cursorPos = assertNotNullOr(savedPositionedP1) { return }
- savedPosition = SavedPosition(Pair(middleX, middleY), cursorPos)
- }
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+
+object SaveCursorPosition {
+ val identifier: String
+ get() = "save-cursor-position"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
+ val enable by toggle("enable") { true }
+ val tolerance by duration("tolerance", 10.milliseconds, 5000.milliseconds) { 500.milliseconds }
+ }
+
+ var savedPositionedP1: Pair<Double, Double>? = null
+ var savedPosition: SavedPosition? = null
+
+ data class SavedPosition(
+ val middle: Pair<Double, Double>,
+ val cursor: Pair<Double, Double>,
+ val savedAt: TimeMark = TimeMark.now()
+ )
+
+ @JvmStatic
+ fun saveCursorOriginal(positionedX: Double, positionedY: Double) {
+ savedPositionedP1 = Pair(positionedX, positionedY)
+ }
+
+ @JvmStatic
+ fun loadCursor(middleX: Double, middleY: Double): Pair<Double, Double>? {
+ if (!TConfig.enable) return null
+ val lastPosition = savedPosition?.takeIf { it.savedAt.passedTime() < TConfig.tolerance }
+ savedPosition = null
+ if (lastPosition != null &&
+ (lastPosition.middle.first - middleX).absoluteValue < 1 &&
+ (lastPosition.middle.second - middleY).absoluteValue < 1
+ ) {
+ GLFW.glfwSetCursorPos(MC.window.handle, lastPosition.cursor.first, lastPosition.cursor.second);
+ return lastPosition.cursor
+ }
+ return null
+ }
+
+ @JvmStatic
+ fun saveCursorMiddle(middleX: Double, middleY: Double) {
+ if (!TConfig.enable) return
+ val cursorPos = assertNotNullOr(savedPositionedP1) { return }
+ savedPosition = SavedPosition(Pair(middleX, middleY), cursorPos)
+ }
}
diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt
index fc09476..bae6a5e 100644
--- a/src/main/kotlin/features/inventory/SlotLocking.kt
+++ b/src/main/kotlin/features/inventory/SlotLocking.kt
@@ -2,78 +2,176 @@
package moe.nea.firmament.features.inventory
-import com.mojang.blaze3d.systems.RenderSystem
import java.util.UUID
import org.lwjgl.glfw.GLFW
+import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.int
import kotlinx.serialization.serializer
+import net.minecraft.client.gl.RenderPipelines
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.entity.player.PlayerInventory
+import net.minecraft.item.ItemStack
import net.minecraft.screen.GenericContainerScreenHandler
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
import net.minecraft.util.Identifier
+import net.minecraft.util.StringIdentifiable
import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ClientInitEvent
import moe.nea.firmament.events.HandledScreenForegroundEvent
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
import moe.nea.firmament.events.HandledScreenKeyReleasedEvent
import moe.nea.firmament.events.IsSlotProtectedEvent
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.SlotRenderEvents
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.keybindings.InputModifiers
import moe.nea.firmament.keybindings.SavedKeyBinding
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.CommonSoundEffects
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SkyBlockIsland
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
+import moe.nea.firmament.util.extraAttributes
import moe.nea.firmament.util.json.DashlessUUIDSerializer
+import moe.nea.firmament.util.lime
import moe.nea.firmament.util.mc.ScreenUtil.getSlotByIndex
import moe.nea.firmament.util.mc.SlotUtils.swapWithHotBar
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
-import moe.nea.firmament.util.render.GuiRenderLayers
+import moe.nea.firmament.util.red
import moe.nea.firmament.util.render.drawLine
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.DungeonUtil
+import moe.nea.firmament.util.skyblock.SkyBlockItems
import moe.nea.firmament.util.skyblockUUID
+import moe.nea.firmament.util.tr
import moe.nea.firmament.util.unformattedString
-object SlotLocking : FirmamentFeature {
- override val identifier: String
+object SlotLocking {
+ val identifier: String
get() = "slot-locking"
@Serializable
- data class Data(
+ data class DimensionData(
val lockedSlots: MutableSet<Int> = mutableSetOf(),
- val lockedSlotsRift: MutableSet<Int> = mutableSetOf(),
+ val boundSlots: BoundSlots = BoundSlots(),
+ )
+
+ @Serializable
+ data class Data(
val lockedUUIDs: MutableSet<UUID> = mutableSetOf(),
- val boundSlots: MutableMap<Int, Int> = mutableMapOf()
+ val rift: DimensionData = DimensionData(),
+ val overworld: DimensionData = DimensionData(),
+ )
+
+
+ val currentWorldData
+ get() = if (SBData.skyblockLocation == SkyBlockIsland.RIFT)
+ DConfig.data?.rift
+ else
+ DConfig.data?.overworld
+
+ @Serializable
+ data class BoundSlot(
+ val hotbar: Int,
+ val inventory: Int,
)
+ @Serializable(with = BoundSlots.Serializer::class)
+ data class BoundSlots(
+ val pairs: MutableSet<BoundSlot> = mutableSetOf()
+ ) {
+ fun findMatchingSlots(index: Int): List<BoundSlot> {
+ return pairs.filter { it.hotbar == index || it.inventory == index }
+ }
+
+ fun removeDuplicateForInventory(index: Int) {
+ pairs.removeIf { it.inventory == index }
+ }
+
+ fun removeAllInvolving(index: Int): Boolean {
+ return pairs.removeIf { it.inventory == index || it.hotbar == index }
+ }
+
+ fun insert(hotbar: Int, inventory: Int) {
+ if (!TConfig.allowMultiBinding) {
+ removeAllInvolving(hotbar)
+ removeAllInvolving(inventory)
+ }
+ pairs.add(BoundSlot(hotbar, inventory))
+ }
+
+ object Serializer : KSerializer<BoundSlots> {
+ override val descriptor: SerialDescriptor
+ get() = serializer<JsonElement>().descriptor
+
+ override fun serialize(
+ encoder: Encoder,
+ value: BoundSlots
+ ) {
+ serializer<MutableSet<BoundSlot>>()
+ .serialize(encoder, value.pairs)
+ }
+
+ override fun deserialize(decoder: Decoder): BoundSlots {
+ decoder as JsonDecoder
+ val json = decoder.decodeJsonElement()
+ if (json is JsonObject) {
+ return BoundSlots(json.entries.map {
+ BoundSlot(it.key.toInt(), (it.value as JsonPrimitive).int)
+ }.toMutableSet())
+ }
+ return BoundSlots(decoder.json.decodeFromJsonElement(serializer<MutableSet<BoundSlot>>(), json))
+
+ }
+ }
+ }
+
+
+ @Config
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
val lockSlot by keyBinding("lock") { GLFW.GLFW_KEY_L }
val lockUUID by keyBindingWithOutDefaultModifiers("lock-uuid") {
- SavedKeyBinding(GLFW.GLFW_KEY_L, shift = true)
+ SavedKeyBinding.keyWithMods(GLFW.GLFW_KEY_L, InputModifiers.of(shift = true))
}
val slotBind by keyBinding("bind") { GLFW.GLFW_KEY_L }
val slotBindRequireShift by toggle("require-quick-move") { true }
+ val slotRenderLines by choice("bind-render") { SlotRenderLinesMode.ONLY_BOXES }
+ val slotBindOnlyInInv by toggle("bind-only-in-inv") { false }
+ val allowMultiBinding by toggle("multi-bind") { true } // TODO: filter based on this option
+ val protectAllHuntingBoxes by toggle("hunting-box") { false }
+ val allowDroppingInDungeons by toggle("drop-in-dungeons") { true }
}
- override val config: TConfig
- get() = TConfig
+ enum class SlotRenderLinesMode : StringIdentifiable {
+ EVERYTHING,
+ ONLY_BOXES,
+ NOTHING;
+
+ override fun asString(): String {
+ return name
+ }
+ }
+ @Config
object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "locked-slots", ::Data)
val lockedUUIDs get() = DConfig.data?.lockedUUIDs
val lockedSlots
- get() = when (SBData.skyblockLocation) {
- SkyBlockIsland.RIFT -> DConfig.data?.lockedSlotsRift
- null -> null
- else -> DConfig.data?.lockedSlots
- }
+ get() = currentWorldData?.lockedSlots
fun isSalvageScreen(screen: HandledScreen<*>?): Boolean {
if (screen == null) return false
@@ -95,7 +193,7 @@ object SlotLocking : FirmamentFeature {
if (handler.inventory.size() < 9) return false
val sellItem = handler.inventory.getStack(handler.inventory.size() - 5)
if (sellItem == null) return false
- if (sellItem.displayNameAccordingToNbt?.unformattedString == "Sell Item") return true
+ if (sellItem.displayNameAccordingToNbt.unformattedString == "Sell Item") return true
val lore = sellItem.loreAccordingToNbt
return (lore.lastOrNull() ?: return false).unformattedString == "Click to buyback!"
}
@@ -104,12 +202,18 @@ object SlotLocking : FirmamentFeature {
fun onSalvageProtect(event: IsSlotProtectedEvent) {
if (event.slot == null) return
if (!event.slot.hasStack()) return
- if (event.slot.stack.displayNameAccordingToNbt?.unformattedString != "Salvage Items") return
+ if (event.slot.stack.displayNameAccordingToNbt.unformattedString != "Salvage Items") return
val inv = event.slot.inventory
var anyBlocked = false
for (i in 0 until event.slot.index) {
val stack = inv.getStack(i)
- if (IsSlotProtectedEvent.shouldBlockInteraction(null, SlotActionType.THROW, stack))
+ if (IsSlotProtectedEvent.shouldBlockInteraction(
+ null,
+ SlotActionType.THROW,
+ IsSlotProtectedEvent.MoveOrigin.SALVAGE,
+ stack
+ )
+ )
anyBlocked = true
}
if (anyBlocked) {
@@ -131,12 +235,20 @@ object SlotLocking : FirmamentFeature {
&& doesNotDeleteItem
) return
val stack = event.itemStack ?: return
+ if (TConfig.protectAllHuntingBoxes && (stack.isHuntingBox())) {
+ event.protect()
+ return
+ }
val uuid = stack.skyblockUUID ?: return
if (uuid in (lockedUUIDs ?: return)) {
event.protect()
}
}
+ fun ItemStack.isHuntingBox(): Boolean {
+ return skyBlockId == SkyBlockItems.HUNTING_TOOLKIT || extraAttributes.get("tool_kit") != null
+ }
+
@Subscribe
fun onProtectSlot(it: IsSlotProtectedEvent) {
if (it.slot != null && it.slot.inventory is PlayerInventory && it.slot.index in (lockedSlots ?: setOf())) {
@@ -145,20 +257,35 @@ object SlotLocking : FirmamentFeature {
}
@Subscribe
+ fun onEvent(event: ClientInitEvent) {
+ IsSlotProtectedEvent.subscribe(receivesCancelled = true, "SlotLocking:unlockInDungeons") {
+ if (it.isProtected
+ && it.origin == IsSlotProtectedEvent.MoveOrigin.DROP_FROM_HOTBAR
+ && DungeonUtil.isInActiveDungeon
+ && TConfig.allowDroppingInDungeons
+ ) {
+ it.isProtected = false
+ }
+ }
+ }
+
+ @Subscribe
fun onQuickMoveBoundSlot(it: IsSlotProtectedEvent) {
- val boundSlots = DConfig.data?.boundSlots ?: mapOf()
+ val boundSlots = currentWorldData?.boundSlots ?: BoundSlots()
val isValidAction =
it.actionType == SlotActionType.QUICK_MOVE || (it.actionType == SlotActionType.PICKUP && !TConfig.slotBindRequireShift)
if (!isValidAction) return
val handler = MC.handledScreen?.screenHandler ?: return
+ if (TConfig.slotBindOnlyInInv && handler.slots.any { it.inventory !is PlayerInventory })
+ return
val slot = it.slot
if (slot != null && it.slot.inventory is PlayerInventory) {
- val boundSlot = boundSlots.entries.find {
- it.value == slot.index || it.key == slot.index
- } ?: return
+ val matchingSlots = boundSlots.findMatchingSlots(slot.index)
+ if (matchingSlots.isEmpty()) return
it.protectSilent()
- val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.value, true)
- inventorySlot?.swapWithHotBar(handler, boundSlot.key)
+ val boundSlot = matchingSlots.singleOrNull() ?: return
+ val inventorySlot = MC.handledScreen?.getSlotByIndex(boundSlot.inventory, true)
+ inventorySlot?.swapWithHotBar(handler, boundSlot.hotbar)
}
}
@@ -170,6 +297,21 @@ object SlotLocking : FirmamentFeature {
val slot = inventory.focusedSlot_Firmament ?: return
val stack = slot.stack ?: return
+ if (stack.isHuntingBox()) {
+ MC.sendChat(
+ tr(
+ "firmament.slot-locking.hunting-box-unbindable-hint",
+ "The hunting box cannot be UUID bound reliably. It changes its own UUID frequently when switching tools. "
+ ).red().append(
+ tr(
+ "firmament.slot-locking.hunting-box-unbindable-hint.solution",
+ "Use the Firmament config option for locking all hunting boxes instead."
+ ).lime()
+ )
+ )
+ CommonSoundEffects.playFailure()
+ return
+ }
val uuid = stack.skyblockUUID ?: return
val lockedUUIDs = lockedUUIDs ?: return
if (uuid in lockedUUIDs) {
@@ -194,13 +336,11 @@ object SlotLocking : FirmamentFeature {
storedLockingSlot = null
val hotBarSlot = if (slot.isHotbar()) slot else storedSlot
val invSlot = if (slot.isHotbar()) storedSlot else slot
- val boundSlots = DConfig.data?.boundSlots ?: return
+ val boundSlots = currentWorldData?.boundSlots ?: return
lockedSlots?.remove(hotBarSlot.index)
lockedSlots?.remove(invSlot.index)
- boundSlots.entries.removeIf {
- it.value == invSlot.index
- }
- boundSlots[hotBarSlot.index] = invSlot.index
+ boundSlots.removeDuplicateForInventory(invSlot.index)
+ boundSlots.insert(hotBarSlot.index, invSlot.index)
DConfig.markDirty()
CommonSoundEffects.playSuccess()
return
@@ -212,38 +352,55 @@ object SlotLocking : FirmamentFeature {
}
if (it.matches(TConfig.slotBind)) {
storedLockingSlot = null
- val boundSlots = DConfig.data?.boundSlots ?: return
+ val boundSlots = currentWorldData?.boundSlots ?: return
if (slot != null)
- boundSlots.entries.removeIf {
- it.value == slot.index || it.key == slot.index
- }
+ boundSlots.removeAllInvolving(slot.index)
}
}
@Subscribe
fun onRenderAllBoundSlots(event: HandledScreenForegroundEvent) {
- val boundSlots = DConfig.data?.boundSlots ?: return
+ val boundSlots = currentWorldData?.boundSlots ?: return
fun findByIndex(index: Int) = event.screen.getSlotByIndex(index, true)
val accScreen = event.screen as AccessorHandledScreen
val sx = accScreen.x_Firmament
val sy = accScreen.y_Firmament
- boundSlots.entries.forEach {
- val hotbarSlot = findByIndex(it.key) ?: return@forEach
- val inventorySlot = findByIndex(it.value) ?: return@forEach
+ val highlitSlots = mutableSetOf<Slot>()
+ for (it in boundSlots.pairs) {
+ val hotbarSlot = findByIndex(it.hotbar) ?: continue
+ val inventorySlot = findByIndex(it.inventory) ?: continue
val (hotX, hotY) = hotbarSlot.lineCenter()
val (invX, invY) = inventorySlot.lineCenter()
- event.context.drawLine(
- invX + sx, invY + sy,
- hotX + sx, hotY + sy,
- me.shedaniel.math.Color.ofOpaque(0x00FF00)
+ val anyHovered = accScreen.focusedSlot_Firmament === hotbarSlot
+ || accScreen.focusedSlot_Firmament === inventorySlot
+ if (!anyHovered && TConfig.slotRenderLines == SlotRenderLinesMode.NOTHING)
+ continue
+ if (anyHovered) {
+ highlitSlots.add(hotbarSlot)
+ highlitSlots.add(inventorySlot)
+ }
+ fun color(highlit: Boolean) =
+ if (highlit)
+ me.shedaniel.math.Color.ofOpaque(0x00FF00)
+ else
+ me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt())
+ if (TConfig.slotRenderLines == SlotRenderLinesMode.EVERYTHING || anyHovered)
+ event.context.drawLine(
+ invX + sx, invY + sy,
+ hotX + sx, hotY + sy,
+ color(anyHovered)
+ )
+ event.context.drawBorder(
+ hotbarSlot.x + sx,
+ hotbarSlot.y + sy,
+ 16, 16, color(hotbarSlot in highlitSlots).color
+ )
+ event.context.drawBorder(
+ inventorySlot.x + sx,
+ inventorySlot.y + sy,
+ 16, 16, color(inventorySlot in highlitSlots).color
)
- event.context.drawBorder(hotbarSlot.x + sx,
- hotbarSlot.y + sy,
- 16, 16, 0xFF00FF00u.toInt())
- event.context.drawBorder(inventorySlot.x + sx,
- inventorySlot.y + sy,
- 16, 16, 0xFF00FF00u.toInt())
}
}
@@ -271,9 +428,11 @@ object SlotLocking : FirmamentFeature {
hovX + sx, hovY + sy,
me.shedaniel.math.Color.ofOpaque(0x00FF00)
)
- event.context.drawBorder(hoveredSlot.x + sx,
- hoveredSlot.y + sy,
- 16, 16, 0xFF00FF00u.toInt())
+ event.context.drawBorder(
+ hoveredSlot.x + sx,
+ hoveredSlot.y + sy,
+ 16, 16, 0xFF00FF00u.toInt()
+ )
}
}
@@ -281,7 +440,7 @@ object SlotLocking : FirmamentFeature {
return if (isHotbar()) {
x + 9 to y
} else {
- x + 9 to y + 17
+ x + 9 to y + 16
}
}
@@ -299,11 +458,9 @@ object SlotLocking : FirmamentFeature {
fun toggleSlotLock(slot: Slot) {
val lockedSlots = lockedSlots ?: return
- val boundSlots = DConfig.data?.boundSlots ?: mutableMapOf()
+ val boundSlots = currentWorldData?.boundSlots ?: BoundSlots()
if (slot.inventory is PlayerInventory) {
- if (boundSlots.entries.removeIf {
- it.value == slot.index || it.key == slot.index
- }) {
+ if (boundSlots.removeAllInvolving(slot.index)) {
// intentionally do nothing
} else if (slot.index in lockedSlots) {
lockedSlots.remove(slot.index)
@@ -338,7 +495,7 @@ object SlotLocking : FirmamentFeature {
val isUUIDLocked = (it.slot.stack?.skyblockUUID) in (lockedUUIDs ?: setOf())
if (isSlotLocked || isUUIDLocked) {
it.context.drawGuiTexture(
- GuiRenderLayers.GUI_TEXTURED_NO_DEPTH,
+ RenderPipelines.GUI_TEXTURED,
when {
isSlotLocked ->
(Identifier.of("firmament:slot_locked"))
diff --git a/src/main/kotlin/features/inventory/TimerInLore.kt b/src/main/kotlin/features/inventory/TimerInLore.kt
new file mode 100644
index 0000000..d8eebda
--- /dev/null
+++ b/src/main/kotlin/features/inventory/TimerInLore.kt
@@ -0,0 +1,152 @@
+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.util.SBData
+import moe.nea.firmament.util.aqua
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.grey
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.timestamp
+import moe.nea.firmament.util.tr
+import moe.nea.firmament.util.unformattedString
+
+object TimerInLore {
+ @Config
+ object TConfig : ManagedConfig("lore-timers", Category.INVENTORY) {
+ val showTimers by toggle("show") { true }
+ val showCreationTimestamp by toggle("show-creation") { 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"),
+ RFCPrecise(DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss.SSS Z")),
+ ;
+
+ 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"),
+ TIMEREMAININGS("Time Remaining:", "Ends at"),
+ COOLDOWN("Cooldown:", "Come back at"),
+ ONCOOLDOWN("On cooldown:", "Available at"),
+ EVENTENDING("Event ends in:", "Ends at");
+ }
+
+ val regex =
+ "(?i)(?:(?<years>[0-9]+) ?(y|years?) )?(?:(?<days>[0-9]+) ?(d|days?))? ?(?:(?<hours>[0-9]+) ?(h|hours?))? ?(?:(?<minutes>[0-9]+) ?(m|minutes?))? ?(?:(?<seconds>[0-9]+) ?(s|seconds?))?\\b".toRegex()
+
+ @Subscribe
+ fun creationInLore(event: ItemTooltipEvent) {
+ if (!TConfig.showCreationTimestamp) return
+ val timestamp = event.stack.timestamp ?: return
+ val formattedTimestamp = TConfig.timerFormat.formatter.format(ZonedDateTime.ofInstant(timestamp, ZoneId.systemDefault()))
+ event.lines.add(tr("firmament.lore.creationtimestamp", "Created at: $formattedTimestamp").grey())
+ }
+
+ @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/features/inventory/WardrobeKeybinds.kt b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt
new file mode 100644
index 0000000..cdd646e
--- /dev/null
+++ b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt
@@ -0,0 +1,78 @@
+package moe.nea.firmament.features.inventory
+
+import org.lwjgl.glfw.GLFW
+import net.minecraft.item.Items
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.mc.SlotUtils.clickLeftMouseButton
+
+object WardrobeKeybinds {
+ @Config
+ object TConfig : ManagedConfig("wardrobe-keybinds", Category.INVENTORY) {
+ val wardrobeKeybinds by toggle("wardrobe-keybinds") { false }
+ val changePageKeybind by keyBinding("change-page") { GLFW.GLFW_KEY_ENTER }
+ val nextPage by keyBinding("next-page") { GLFW.GLFW_KEY_D }
+ val previousPage by keyBinding("previous-page") { GLFW.GLFW_KEY_A }
+ val slotKeybinds = (1..9).map {
+ keyBinding("slot-$it") { GLFW.GLFW_KEY_0 + it }
+ }
+ val allowUnequipping by toggle("allow-unequipping") { true }
+ }
+
+ val slotKeybindsWithSlot = TConfig.slotKeybinds.withIndex().map { (index, keybinding) ->
+ index + 36 to keybinding
+ }
+
+ @Subscribe
+ fun switchSlot(event: HandledScreenKeyPressedEvent) {
+ if (MC.player == null || MC.world == null || MC.interactionManager == null) return
+
+ val regex = Regex("Wardrobe \\([12]/2\\)")
+ if (!regex.matches(event.screen.title.string)) return
+ if (!TConfig.wardrobeKeybinds) return
+
+ if (
+ event.matches(TConfig.changePageKeybind) ||
+ event.matches(TConfig.previousPage) ||
+ event.matches(TConfig.nextPage)
+ ) {
+ event.cancel()
+
+ val handler = event.screen.screenHandler
+ val previousSlot = handler.getSlot(45)
+ val nextSlot = handler.getSlot(53)
+
+ val backPressed = event.matches(TConfig.changePageKeybind) || event.matches(TConfig.previousPage)
+ val nextPressed = event.matches(TConfig.changePageKeybind) || event.matches(TConfig.nextPage)
+
+ if (backPressed && previousSlot.stack.item == Items.ARROW) {
+ previousSlot.clickLeftMouseButton(handler)
+ } else if (nextPressed && nextSlot.stack.item == Items.ARROW) {
+ nextSlot.clickLeftMouseButton(handler)
+ }
+ }
+
+
+ val slot =
+ slotKeybindsWithSlot
+ .find { event.matches(it.second.get()) }
+ ?.first ?: return
+
+ event.cancel()
+
+ val handler = event.screen.screenHandler
+ val invSlot = handler.getSlot(slot)
+
+ val itemStack = invSlot.stack
+ val isSelected = itemStack.item == Items.LIME_DYE
+ val isSelectable = itemStack.item == Items.PINK_DYE
+ if (!isSelectable && !isSelected) return
+ if (!TConfig.allowUnequipping && isSelected) return
+
+ invSlot.clickLeftMouseButton(handler)
+ }
+
+}
diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt
index a46bd76..955ae88 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButton.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButton.kt
@@ -1,5 +1,3 @@
-
-
package moe.nea.firmament.features.inventory.buttons
import com.mojang.brigadier.StringReader
@@ -13,74 +11,93 @@ import net.minecraft.command.argument.ItemStackArgumentType
import net.minecraft.item.ItemStack
import net.minecraft.resource.featuretoggle.FeatureFlags
import net.minecraft.util.Identifier
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.ItemCache.asItemStack
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.collections.memoize
+import moe.nea.firmament.util.mc.arbitraryUUID
+import moe.nea.firmament.util.mc.createSkullItem
import moe.nea.firmament.util.render.drawGuiTexture
@Serializable
data class InventoryButton(
- var x: Int,
- var y: Int,
- var anchorRight: Boolean,
- var anchorBottom: Boolean,
- var icon: String? = "",
- var command: String? = "",
+ var x: Int,
+ var y: Int,
+ var anchorRight: Boolean,
+ var anchorBottom: Boolean,
+ var icon: String? = "",
+ var command: String? = "",
) {
- companion object {
- val itemStackParser by lazy {
- ItemStackArgumentType.itemStack(CommandRegistryAccess.of(MC.defaultRegistries,
- FeatureFlags.VANILLA_FEATURES))
- }
- val dimensions = Dimension(18, 18)
- val getItemForName = ::getItemForName0.memoize(1024)
- fun getItemForName0(icon: String): ItemStack {
- val repoItem = RepoManager.getNEUItem(SkyblockId(icon))
- var itemStack = repoItem.asItemStack(idHint = SkyblockId(icon))
- if (repoItem == null) {
- val giveSyntaxItem = if (icon.startsWith("/give") || icon.startsWith("give"))
- icon.split(" ", limit = 3).getOrNull(2) ?: icon
- else icon
- val componentItem =
- runCatching {
- itemStackParser.parse(StringReader(giveSyntaxItem)).createStack(1, false)
- }.getOrNull()
- if (componentItem != null)
- itemStack = componentItem
- }
- return itemStack
- }
- }
+ companion object {
+ val itemStackParser by lazy {
+ ItemStackArgumentType.itemStack(
+ CommandRegistryAccess.of(
+ MC.defaultRegistries,
+ FeatureFlags.VANILLA_FEATURES
+ )
+ )
+ }
+ val dimensions = Dimension(18, 18)
+ val getItemForName = ::getItemForName0.memoize(1024)
+ @OptIn(ExpensiveItemCacheApi::class)
+ fun getItemForName0(icon: String): ItemStack {
+ val repoItem = RepoManager.getNEUItem(SkyblockId(icon))
+ var itemStack = repoItem.asItemStack(idHint = SkyblockId(icon))
+ if (repoItem == null) {
+ when {
+ icon.startsWith("skull:") -> {
+ itemStack = createSkullItem(
+ arbitraryUUID,
+ "https://textures.minecraft.net/texture/${icon.substring("skull:".length)}"
+ )
+ }
+
+ else -> {
+ val giveSyntaxItem = if (icon.startsWith("/give") || icon.startsWith("give"))
+ icon.split(" ", limit = 3).getOrNull(2) ?: icon
+ else icon
+ val componentItem =
+ runCatching {
+ itemStackParser.parse(StringReader(giveSyntaxItem)).createStack(1, false)
+ }.getOrNull()
+ if (componentItem != null)
+ itemStack = componentItem
+ }
+ }
+ }
+ return itemStack
+ }
+ }
- fun render(context: DrawContext) {
- context.drawGuiTexture(
- 0,
- 0,
- 0,
- dimensions.width,
- dimensions.height,
- Identifier.of("firmament:inventory_button_background")
- )
- context.drawItem(getItem(), 1, 1)
- }
+ fun render(context: DrawContext) {
+ context.drawGuiTexture(
+ 0,
+ 0,
+ 0,
+ dimensions.width,
+ dimensions.height,
+ Identifier.of("firmament:inventory_button_background")
+ )
+ context.drawItem(getItem(), 1, 1)
+ }
- fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank()
+ fun isValid() = !icon.isNullOrBlank() && !command.isNullOrBlank()
- fun getPosition(guiRect: Rectangle): Point {
- return Point(
- (if (anchorRight) guiRect.maxX else guiRect.minX) + x,
- (if (anchorBottom) guiRect.maxY else guiRect.minY) + y,
- )
- }
+ fun getPosition(guiRect: Rectangle): Point {
+ return Point(
+ (if (anchorRight) guiRect.maxX else guiRect.minX) + x,
+ (if (anchorBottom) guiRect.maxY else guiRect.minY) + y,
+ )
+ }
- fun getBounds(guiRect: Rectangle): Rectangle {
- return Rectangle(getPosition(guiRect), dimensions)
- }
+ fun getBounds(guiRect: Rectangle): Rectangle {
+ return Rectangle(getPosition(guiRect), dimensions)
+ }
- fun getItem(): ItemStack {
- return getItemForName(icon ?: "")
- }
+ fun getItem(): ItemStack {
+ return getItemForName(icon ?: "")
+ }
}
diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt
index ee3ae8b..7334c82 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt
@@ -1,13 +1,17 @@
package moe.nea.firmament.features.inventory.buttons
import io.github.notenoughupdates.moulconfig.common.IItemStack
-import io.github.notenoughupdates.moulconfig.platform.ModernItemStack
+import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigRenderContext
import io.github.notenoughupdates.moulconfig.xml.Bind
import me.shedaniel.math.Point
import me.shedaniel.math.Rectangle
import org.lwjgl.glfw.GLFW
+import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.widget.ButtonWidget
+import net.minecraft.client.gui.widget.TextWidget
import net.minecraft.client.util.InputUtil
import net.minecraft.text.Text
import net.minecraft.util.math.MathHelper
@@ -31,7 +35,7 @@ class InventoryButtonEditor(
@Bind
fun getItemIcon(): IItemStack {
save()
- return ModernItemStack.of(InventoryButton.getItemForName(icon))
+ return MoulConfigPlatform.wrap(InventoryButton.getItemForName(icon))
}
@Bind
@@ -55,9 +59,47 @@ class InventoryButtonEditor(
super.close()
}
+ override fun resize(client: MinecraftClient, width: Int, height: Int) {
+ lastGuiRect.move(
+ MC.window.scaledWidth / 2 - lastGuiRect.width / 2,
+ MC.window.scaledHeight / 2 - lastGuiRect.height / 2
+ )
+ super.resize(client, width, height)
+ }
+
override fun init() {
super.init()
addDrawableChild(
+ TextWidget(
+ lastGuiRect.minX,
+ 25,
+ lastGuiRect.width,
+ 9,
+ Text.translatable("firmament.inventory-buttons.delete"),
+ MC.font
+ ).alignCenter()
+ )
+ addDrawableChild(
+ TextWidget(
+ lastGuiRect.minX,
+ 40,
+ lastGuiRect.width,
+ 9,
+ Text.translatable("firmament.inventory-buttons.info"),
+ MC.font
+ ).alignCenter()
+ )
+ addDrawableChild(
+ ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.reset")) {
+ val newButtons = InventoryButtonTemplates.loadTemplate("TkVVQlVUVE9OUy9bXQ==")
+ if (newButtons != null)
+ buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) })
+ }
+ .position(lastGuiRect.minX + 10, lastGuiRect.minY + 10)
+ .width(lastGuiRect.width - 20)
+ .build()
+ )
+ addDrawableChild(
ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.load-preset")) {
val t = ClipboardUtils.getTextContents()
val newButtons = InventoryButtonTemplates.loadTemplate(t)
@@ -76,6 +118,34 @@ class InventoryButtonEditor(
.width(lastGuiRect.width - 20)
.build()
)
+ addDrawableChild(
+ ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.simple-preset")) {
+ // Preset from NEU
+ // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/resources/assets/notenoughupdates/invbuttons/presets.json#L900-L1348
+ val newButtons = InventoryButtonTemplates.loadTemplate(
+ "TkVVQlVUVE9OUy9bIntcblx0XCJ4XCI6IDE2MCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcImJvbmVcIixcblx0XCJjb21tYW5kXCI6IFwicGV0c1wiXG59Iiwie1xuXHRcInhcIjogMTQwLFxuXHRcInlcIjogLTIwLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwiYXJtb3Jfc3RhbmRcIixcblx0XCJjb21tYW5kXCI6IFwid2FyZHJvYmVcIlxufSIsIntcblx0XCJ4XCI6IDEyMCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcImVuZGVyX2NoZXN0XCIsXG5cdFwiY29tbWFuZFwiOiBcInN0b3JhZ2VcIlxufSIsIntcblx0XCJ4XCI6IDEwMCxcblx0XCJ5XCI6IC0yMCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcInNrdWxsOmQ3Y2M2Njg3NDIzZDA1NzBkNTU2YWM1M2UwNjc2Y2I1NjNiYmRkOTcxN2NkODI2OWJkZWJlZDZmNmQ0ZTdiZjhcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBpc2xhbmRcIlxufSIsIntcblx0XCJ4XCI6IDgwLFxuXHRcInlcIjogLTIwLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwic2t1bGw6MzVmNGI0MGNlZjllMDE3Y2Q0MTEyZDI2YjYyNTU3ZjhjMWQ1YjE4OWRhMmU5OTUzNDIyMmJjOGNlYzdkOTE5NlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGh1YlwiXG59Il0="
+ )
+ if (newButtons != null)
+ buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) })
+ }
+ .position(lastGuiRect.minX + 10, lastGuiRect.minY + 85)
+ .width(lastGuiRect.width - 20)
+ .build()
+ )
+ addDrawableChild(
+ ButtonWidget.builder(Text.translatable("firmament.inventory-buttons.all-warps-preset")) {
+ // Preset from NEU
+ // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/resources/assets/notenoughupdates/invbuttons/presets.json#L1817-L2276
+ val newButtons = InventoryButtonTemplates.loadTemplate(
+ "TkVVQlVUVE9OUy9bIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtODQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6YzljODg4MWU0MjkxNWE5ZDI5YmI2MWExNmZiMjZkMDU5OTEzMjA0ZDI2NWRmNWI0MzliM2Q3OTJhY2Q1NlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGhvbWVcIlxufSIsIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtNjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6ZDdjYzY2ODc0MjNkMDU3MGQ1NTZhYzUzZTA2NzZjYjU2M2JiZGQ5NzE3Y2Q4MjY5YmRlYmVkNmY2ZDRlN2JmOFwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGh1YlwiXG59Iiwie1xuXHRcInhcIjogMixcblx0XCJ5XCI6IC00NCxcblx0XCJhbmNob3JSaWdodFwiOiB0cnVlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo5YjU2ODk1Yjk2NTk4OTZhZDY0N2Y1ODU5OTIzOGFmNTMyZDQ2ZGI5YzFiMDM4OWI4YmJlYjcwOTk5ZGFiMzNkXCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZHVuZ2Vvbl9odWJcIlxufSIsIntcblx0XCJ4XCI6IDIsXG5cdFwieVwiOiAtMjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogdHJ1ZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6Nzg0MGI4N2Q1MjI3MWQyYTc1NWRlZGM4Mjg3N2UwZWQzZGY2N2RjYzQyZWE0NzllYzE0NjE3NmIwMjc3OWE1XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZW5kXCJcbn0iLCJ7XG5cdFwieFwiOiAxMDksXG5cdFwieVwiOiAtMTksXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IGZhbHNlLFxuXHRcImljb25cIjogXCJza3VsbDo4NmYwNmVhYTMwMDRhZWVkMDliM2Q1YjQ1ZDk3NmRlNTg0ZTY5MWMwZTljYWRlMTMzNjM1ZGU5M2QyM2I5ZWRiXCIsXG5cdFwiY29tbWFuZFwiOiBcImhvdG1cIlxufSIsIntcblx0XCJ4XCI6IDEzMCxcblx0XCJ5XCI6IC0xOSxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkVOREVSX0NIRVNUXCIsXG5cdFwiY29tbWFuZFwiOiBcInN0b3JhZ2VcIlxufSIsIntcblx0XCJ4XCI6IDE1MSxcblx0XCJ5XCI6IC0xOSxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkJPTkVcIixcblx0XCJjb21tYW5kXCI6IFwicGV0c1wiXG59Iiwie1xuXHRcInhcIjogLTE5LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogZmFsc2UsXG5cdFwiaWNvblwiOiBcIkdPTERfQkxPQ0tcIixcblx0XCJjb21tYW5kXCI6IFwiYWhcIlxufSIsIntcblx0XCJ4XCI6IC0xOSxcblx0XCJ5XCI6IDIyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiBmYWxzZSxcblx0XCJpY29uXCI6IFwiR09MRF9CQVJESU5HXCIsXG5cdFwiY29tbWFuZFwiOiBcImJ6XCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtODQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjQzOGNmM2Y4ZTU0YWZjM2IzZjkxZDIwYTQ5ZjMyNGRjYTE0ODYwMDdmZTU0NTM5OTA1NTUyNGMxNzk0MWY0ZGNcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBtdXNldW1cIlxufSIsIntcblx0XCJ4XCI6IC0xOSxcblx0XCJ5XCI6IC02NCxcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6ZjQ4ODBkMmMxZTdiODZlODc1MjJlMjA4ODI2NTZmNDViYWZkNDJmOTQ5MzJiMmM1ZTBkNmVjYWE0OTBjYjRjXCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZ2FyZGVuXCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtNDQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjRkM2E2YmQ5OGFjMTgzM2M2NjRjNDkwOWZmOGQyZGM2MmNlODg3YmRjZjNjYzViMzg0ODY1MWFlNWFmNmJcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBiYXJuXCJcbn0iLCJ7XG5cdFwieFwiOiAtMTksXG5cdFwieVwiOiAtMjQsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjUxNTM5ZGRkZjllZDI1NWVjZTYzNDgxOTNjZDc1MDEyYzgyYzkzYWVjMzgxZjA1NTcyY2VjZjczNzk3MTFiM2JcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBkZXNlcnRcIlxufSIsIntcblx0XCJ4XCI6IDQsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo3M2JjOTY1ZDU3OWMzYzYwMzlmMGExN2ViN2MyZTZmYWY1MzhjN2E1ZGU4ZTYwZWM3YTcxOTM2MGQwYTg1N2E5XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgZ29sZFwiXG59Iiwie1xuXHRcInhcIjogMjUsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo1NjlhMWYxMTQxNTFiNDUyMTM3M2YzNGJjMTRjMjk2M2E1MDExY2RjMjVhNjU1NGM0OGM3MDhjZDk2ZWJmY1wiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGRlZXBcIlxufSIsIntcblx0XCJ4XCI6IDQ2LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6MjFkYmUzMGIwMjdhY2JjZWI2MTI1NjNiZDg3N2NkN2ViYjcxOWVhNmVkMTM5OTAyN2RjZWU1OGJiOTA0OWQ0YVwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGNyeXN0YWxzXCJcbn0iLCJ7XG5cdFwieFwiOiA2Nyxcblx0XCJ5XCI6IDIsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjVjYmQ5ZjVlYzFlZDAwNzI1OTk5NjQ5MWU2OWZmNjQ5YTMxMDZjZjkyMDIyN2IxYmIzYTcxZWU3YTg5ODYzZlwiLFxuXHRcImNvbW1hbmRcIjogXCJ3YXJwIGZvcmdlXCJcbn0iLCJ7XG5cdFwieFwiOiA4OCxcblx0XCJ5XCI6IDIsXG5cdFwiYW5jaG9yUmlnaHRcIjogZmFsc2UsXG5cdFwiYW5jaG9yQm90dG9tXCI6IHRydWUsXG5cdFwiaWNvblwiOiBcInNrdWxsOjZiMjBiMjNjMWFhMmJlMDI3MGYwMTZiNGM5MGQ2ZWU2YjgzMzBhMTdjZmVmODc4NjlkNmFkNjBiMmZmYmYzYjVcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBtaW5lc1wiXG59Iiwie1xuXHRcInhcIjogMTA5LFxuXHRcInlcIjogMixcblx0XCJhbmNob3JSaWdodFwiOiBmYWxzZSxcblx0XCJhbmNob3JCb3R0b21cIjogdHJ1ZSxcblx0XCJpY29uXCI6IFwic2t1bGw6YTIyMWY4MTNkYWNlZTBmZWY4YzU5Zjc2ODk0ZGJiMjY0MTU0NzhkOWRkZmM0NGMyZTcwOGE2ZDNiNzU0OWJcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBwYXJrXCJcbn0iLCJ7XG5cdFwieFwiOiAxMzAsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDo5ZDdlM2IxOWFjNGYzZGVlOWM1Njc3YzEzNTMzM2I5ZDM1YTdmNTY4YjYzZDFlZjRhZGE0YjA2OGI1YTI1XCIsXG5cdFwiY29tbWFuZFwiOiBcIndhcnAgc3BpZGVyXCJcbn0iLCJ7XG5cdFwieFwiOiAxNTEsXG5cdFwieVwiOiAyLFxuXHRcImFuY2hvclJpZ2h0XCI6IGZhbHNlLFxuXHRcImFuY2hvckJvdHRvbVwiOiB0cnVlLFxuXHRcImljb25cIjogXCJza3VsbDpjMzY4N2UyNWM2MzJiY2U4YWE2MWUwZDY0YzI0ZTY5NGMzZWVhNjI5ZWE5NDRmNGNmMzBkY2ZiNGZiY2UwNzFcIixcblx0XCJjb21tYW5kXCI6IFwid2FycCBuZXRoZXJcIlxufSJd"
+ )
+ if (newButtons != null)
+ buttons = moveButtons(newButtons.map { it.copy(command = it.command?.removePrefix("/")) })
+ }
+ .position(lastGuiRect.minX + 10, lastGuiRect.minY + 110)
+ .width(lastGuiRect.width - 20)
+ .build()
+ )
}
private fun moveButtons(buttons: List<InventoryButton>): MutableList<InventoryButton> {
@@ -83,14 +153,20 @@ class InventoryButtonEditor(
val movedButtons = mutableListOf<InventoryButton>()
for (button in buttons) {
if ((!button.anchorBottom && !button.anchorRight && button.x > 0 && button.y > 0)) {
- MC.sendChat(tr("firmament.inventory-buttons.button-moved",
- "One of your imported buttons intersects with the inventory and has been moved to the top left."))
- movedButtons.add(button.copy(
- x = 0,
- y = -InventoryButton.dimensions.width,
- anchorRight = false,
- anchorBottom = false
- ))
+ MC.sendChat(
+ tr(
+ "firmament.inventory-buttons.button-moved",
+ "One of your imported buttons intersects with the inventory and has been moved to the top left."
+ )
+ )
+ movedButtons.add(
+ button.copy(
+ x = 0,
+ y = -InventoryButton.dimensions.width,
+ anchorRight = false,
+ anchorBottom = false
+ )
+ )
} else {
newButtons.add(button)
}
@@ -99,9 +175,11 @@ class InventoryButtonEditor(
val zeroRect = Rectangle(0, 0, 1, 1)
for (movedButton in movedButtons) {
fun getPosition(button: InventoryButton, index: Int) =
- button.copy(x = (index % 10) * InventoryButton.dimensions.width,
- y = (index / 10) * -InventoryButton.dimensions.height,
- anchorRight = false, anchorBottom = false)
+ button.copy(
+ x = (index % 10) * InventoryButton.dimensions.width,
+ y = (index / 10) * -InventoryButton.dimensions.height,
+ anchorRight = false, anchorBottom = false
+ )
while (true) {
val newPos = getPosition(movedButton, i++)
val newBounds = newPos.getBounds(zeroRect)
@@ -116,17 +194,22 @@ class InventoryButtonEditor(
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
- context.matrices.push()
- context.matrices.translate(0f, 0f, -10f)
- context.fill(lastGuiRect.minX, lastGuiRect.minY, lastGuiRect.maxX, lastGuiRect.maxY, -1)
- context.matrices.pop()
+ context.matrices.pushMatrix()
+ PanelComponent.DefaultBackgroundRenderer.VANILLA
+ .render(
+ MoulConfigRenderContext(context),
+ lastGuiRect.minX, lastGuiRect.minY,
+ lastGuiRect.width, lastGuiRect.height,
+ )
+ context.matrices.popMatrix()
for (button in buttons) {
val buttonPosition = button.getBounds(lastGuiRect)
- context.matrices.push()
- context.matrices.translate(buttonPosition.minX.toFloat(), buttonPosition.minY.toFloat(), 0F)
+ context.matrices.pushMatrix()
+ context.matrices.translate(buttonPosition.minX.toFloat(), buttonPosition.minY.toFloat())
button.render(context)
- context.matrices.pop()
+ context.matrices.popMatrix()
}
+ renderPopup(context, mouseX, mouseY, delta)
}
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
@@ -142,7 +225,15 @@ class InventoryButtonEditor(
if (super.mouseReleased(mouseX, mouseY, button)) return true
val clickedButton = buttons.firstOrNull { it.getBounds(lastGuiRect).contains(Point(mouseX, mouseY)) }
if (clickedButton != null && !justPerformedAClickAction) {
- createPopup(MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)), Point(mouseX, mouseY))
+ if (InputUtil.isKeyPressed(
+ MC.window.handle,
+ InputUtil.GLFW_KEY_LEFT_CONTROL
+ )
+ ) Editor(clickedButton).delete()
+ else createPopup(
+ MoulConfigUtils.loadGui("button_editor_fragment", Editor(clickedButton)),
+ Point(mouseX, mouseY)
+ )
return true
}
justPerformedAClickAction = false
@@ -180,14 +271,6 @@ class InventoryButtonEditor(
)
fun getCoordsForMouse(mx: Int, my: Int): AnchoredCoords? {
- if (lastGuiRect.contains(mx, my) || lastGuiRect.contains(
- Point(
- mx + InventoryButton.dimensions.width,
- my + InventoryButton.dimensions.height,
- )
- )
- ) return null
-
val anchorRight = mx > lastGuiRect.maxX
val anchorBottom = my > lastGuiRect.maxY
var offsetX = mx - if (anchorRight) lastGuiRect.maxX else lastGuiRect.minX
@@ -196,7 +279,10 @@ class InventoryButtonEditor(
offsetX = MathHelper.floor(offsetX / 20F) * 20
offsetY = MathHelper.floor(offsetY / 20F) * 20
}
- return AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY)
+ val rect = InventoryButton(offsetX, offsetY, anchorRight, anchorBottom).getBounds(lastGuiRect)
+ if (rect.intersects(lastGuiRect)) return null
+ val anchoredCoords = AnchoredCoords(anchorRight, anchorBottom, offsetX, offsetY)
+ return anchoredCoords
}
override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean {
diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt
index d282157..082673e 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonTemplates.kt
@@ -1,6 +1,5 @@
package moe.nea.firmament.features.inventory.buttons
-import kotlinx.serialization.encodeToString
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
import moe.nea.firmament.util.ErrorUtil
diff --git a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
index d5b5417..47fdbe9 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
@@ -1,88 +1,115 @@
-
-
package moe.nea.firmament.features.inventory.buttons
import me.shedaniel.math.Rectangle
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.client.gui.screen.ingame.HandledScreen
+import net.minecraft.client.gui.screen.ingame.InventoryScreen
+import net.minecraft.text.Text
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HandledScreenClickEvent
import moe.nea.firmament.events.HandledScreenForegroundEvent
import moe.nea.firmament.events.HandledScreenPushREIEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.ScreenUtil
-import moe.nea.firmament.util.data.DataHolder
+import moe.nea.firmament.util.TimeMark
import moe.nea.firmament.util.accessors.getRectangle
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.DataHolder
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.gold
+
+object InventoryButtons {
+
+ @Config
+ object TConfig : ManagedConfig("inventory-buttons-config", Category.INVENTORY) {
+ val _openEditor by button("open-editor") {
+ openEditor()
+ }
+ val hoverText by toggle("hover-text") { true }
+ val onlyInv by toggle("only-inv") { false }
+ }
-object InventoryButtons : FirmamentFeature {
- override val identifier: String
- get() = "inventory-buttons"
+ @Config
+ object DConfig : DataHolder<Data>(serializer(), "inventory-buttons", ::Data)
- object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
- val _openEditor by button("open-editor") {
- openEditor()
- }
- }
+ @Serializable
+ data class Data(
+ var buttons: MutableList<InventoryButton> = mutableListOf()
+ )
- object DConfig : DataHolder<Data>(serializer(), identifier, ::Data)
+ fun getValidButtons(screen: HandledScreen<*>): Sequence<InventoryButton> {
+ return DConfig.data.buttons.asSequence().filter { button ->
+ button.isValid() && (!TConfig.onlyInv || screen is InventoryScreen)
+ }
+ }
- @Serializable
- data class Data(
- var buttons: MutableList<InventoryButton> = mutableListOf()
- )
+ @Subscribe
+ fun onRectangles(it: HandledScreenPushREIEvent) {
+ val bounds = it.screen.getRectangle()
+ for (button in getValidButtons(it.screen)) {
+ val buttonBounds = button.getBounds(bounds)
+ it.block(buttonBounds)
+ }
+ }
- override val config: ManagedConfig
- get() = TConfig
+ @Subscribe
+ fun onClickScreen(it: HandledScreenClickEvent) {
+ val bounds = it.screen.getRectangle()
+ for (button in getValidButtons(it.screen)) {
+ val buttonBounds = button.getBounds(bounds)
+ if (buttonBounds.contains(it.mouseX, it.mouseY)) {
+ MC.sendCommand(button.command!! /* non null invariant covered by getValidButtons */)
+ break
+ }
+ }
+ }
- fun getValidButtons() = DConfig.data.buttons.asSequence().filter { it.isValid() }
+ var lastHoveredComponent: InventoryButton? = null
+ var lastMouseMove = TimeMark.farPast()
- @Subscribe
- fun onRectangles(it: HandledScreenPushREIEvent) {
- val bounds = it.screen.getRectangle()
- for (button in getValidButtons()) {
- val buttonBounds = button.getBounds(bounds)
- it.block(buttonBounds)
- }
- }
+ @Subscribe
+ fun onRenderForeground(it: HandledScreenForegroundEvent) {
+ val bounds = it.screen.getRectangle()
- @Subscribe
- fun onClickScreen(it: HandledScreenClickEvent) {
- val bounds = it.screen.getRectangle()
- for (button in getValidButtons()) {
- val buttonBounds = button.getBounds(bounds)
- if (buttonBounds.contains(it.mouseX, it.mouseY)) {
- MC.sendCommand(button.command!! /* non null invariant covered by getValidButtons */)
- break
- }
- }
- }
+ var hoveredComponent: InventoryButton? = null
+ for (button in getValidButtons(it.screen)) {
+ val buttonBounds = button.getBounds(bounds)
+ it.context.matrices.pushMatrix()
+ it.context.matrices.translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat())
+ button.render(it.context)
+ it.context.matrices.popMatrix()
- @Subscribe
- fun onRenderForeground(it: HandledScreenForegroundEvent) {
- val bounds = it.screen.getRectangle()
- for (button in getValidButtons()) {
- val buttonBounds = button.getBounds(bounds)
- it.context.matrices.push()
- it.context.matrices.translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat(), 0F)
- button.render(it.context)
- it.context.matrices.pop()
- }
- lastRectangle = bounds
- }
+ if (buttonBounds.contains(it.mouseX, it.mouseY) && TConfig.hoverText && hoveredComponent == null) {
+ hoveredComponent = button
+ if (lastMouseMove.passedTime() > 0.6.seconds && lastHoveredComponent === button) {
+ it.context.drawTooltip(
+ MC.font,
+ listOf(Text.literal(button.command).gold()),
+ buttonBounds.minX - 15,
+ buttonBounds.maxY + 20,
+ )
+ }
+ }
+ }
+ if (hoveredComponent !== lastHoveredComponent)
+ lastMouseMove = TimeMark.now()
+ lastHoveredComponent = hoveredComponent
+ lastRectangle = bounds
+ }
- var lastRectangle: Rectangle? = null
- fun openEditor() {
- ScreenUtil.setScreenLater(
- InventoryButtonEditor(
- lastRectangle ?: Rectangle(
- MC.window.scaledWidth / 2 - 100,
- MC.window.scaledHeight / 2 - 100,
- 200, 200,
- )
- )
- )
- }
+ var lastRectangle: Rectangle? = null
+ fun openEditor() {
+ ScreenUtil.setScreenLater(
+ InventoryButtonEditor(
+ lastRectangle ?: Rectangle(
+ MC.window.scaledWidth / 2 - 88,
+ MC.window.scaledHeight / 2 - 83,
+ 176, 166,
+ )
+ )
+ )
+ }
}
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt
index 8fad4df..d7346c2 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt
@@ -32,8 +32,8 @@ sealed interface StorageBackingHandle {
StorageBackingHandle, HasBackingScreen
companion object {
- private val enderChestName = "^Ender Chest \\(([1-9])/[1-9]\\)$".toRegex()
- private val backPackName = "^.+Backpack \\(Slot #([0-9]+)\\)$".toRegex()
+ private val enderChestName = "^Ender Chest (?:✦ )?\\(([1-9])/[1-9]\\)$".toRegex()
+ private val backPackName = "^.+Backpack (?:✦ )?\\(Slot #([0-9]+)\\)$".toRegex()
/**
* Parse a screen into a [StorageBackingHandle]. If this returns null it means that the screen is not
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt
index 2e807de..6043335 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt
@@ -1,5 +1,6 @@
package moe.nea.firmament.features.inventory.storageoverlay
+import io.github.notenoughupdates.moulconfig.ChromaColour
import java.util.SortedMap
import kotlinx.serialization.serializer
import net.minecraft.client.gui.screen.ingame.GenericContainerScreen
@@ -10,38 +11,80 @@ import net.minecraft.network.packet.c2s.play.CloseHandledScreenC2SPacket
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.SlotClickEvent
+import moe.nea.firmament.events.SlotRenderEvents
import moe.nea.firmament.events.TickEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.customgui.customGui
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
-object StorageOverlay : FirmamentFeature {
-
+object StorageOverlay {
+ @Config
object Data : ProfileSpecificDataHolder<StorageData>(serializer(), "storage-data", ::StorageData)
- override val identifier: String
+ val identifier: String
get() = "storage-overlay"
+ @Config
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
val alwaysReplace by toggle("always-replace") { true }
+ val outlineActiveStoragePage by toggle("outline-active-page") { false }
+ val outlineActiveStoragePageColour by colour("outline-active-page-colour") {
+ ChromaColour.fromRGB(
+ 255,
+ 255,
+ 0,
+ 0,
+ 255
+ )
+ }
val columns by integer("rows", 1, 10) { 3 }
val height by integer("height", 80, 3000) { 3 * 18 * 6 }
+ val retainScroll by toggle("retain-scroll") { true }
val scrollSpeed by integer("scroll-speed", 1, 50) { 10 }
val inverseScroll by toggle("inverse-scroll") { false }
val padding by integer("padding", 1, 20) { 5 }
val margin by integer("margin", 1, 60) { 20 }
+ val itemsBlockScrolling by toggle("block-item-scrolling") { true }
+ val highlightSearchResults by toggle("highlight-search-results") { true }
+ val highlightSearchResultsColour by colour("highlight-search-results-colour") {
+ ChromaColour.fromRGB(
+ 0,
+ 176,
+ 0,
+ 0,
+ 255
+ )
+ }
}
+ @Subscribe
+ fun highlightSlots(event: SlotRenderEvents.Before) {
+ if (!TConfig.highlightSearchResults) return
+ val storageOverlayScreen =
+ (MC.screen as? StorageOverlayScreen)
+ ?: (MC.handledScreen?.customGui as? StorageOverlayCustom)?.overview
+ ?: return
+ val stack = event.slot.stack ?: return
+ val search = storageOverlayScreen.searchText.get().takeIf { it.isNotBlank() } ?: return
+ if (storageOverlayScreen.matchesSearch(stack, search)) {
+ event.context.fill(
+ event.slot.x,
+ event.slot.y,
+ event.slot.x + 16,
+ event.slot.y + 16,
+ TConfig.highlightSearchResultsColour.getEffectiveColourRGB()
+ )
+ }
+ }
+
+
fun adjustScrollSpeed(amount: Double): Double {
return amount * TConfig.scrollSpeed * (if (TConfig.inverseScroll) 1 else -1)
}
- override val config: TConfig
- get() = TConfig
-
var lastStorageOverlay: StorageOverviewScreen? = null
var skipNextStorageOverlayBackflip = false
var currentHandler: StorageBackingHandle? = null
@@ -100,7 +143,8 @@ object StorageOverlay : FirmamentFeature {
screen.customGui = StorageOverlayCustom(
currentHandler ?: return,
screen,
- storageOverlayScreen ?: (if (TConfig.alwaysReplace) StorageOverlayScreen() else return))
+ storageOverlayScreen ?: (if (TConfig.alwaysReplace) StorageOverlayScreen() else return)
+ )
}
fun rememberContent(handler: StorageBackingHandle?) {
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt
index 6092e26..e4d4e42 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt
@@ -9,6 +9,7 @@ import net.minecraft.entity.player.PlayerInventory
import net.minecraft.screen.slot.Slot
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.customgui.CustomGui
+import moe.nea.firmament.util.focusedItemStack
class StorageOverlayCustom(
val handler: StorageBackingHandle,
@@ -17,6 +18,7 @@ class StorageOverlayCustom(
) : CustomGui() {
override fun onVoluntaryExit(): Boolean {
overview.isExiting = true
+ StorageOverlayScreen.resetScroll()
return super.onVoluntaryExit()
}
@@ -113,6 +115,8 @@ class StorageOverlayCustom(
horizontalAmount: Double,
verticalAmount: Double
): Boolean {
+ if (screen.focusedItemStack != null && StorageOverlay.TConfig.itemsBlockScrolling)
+ return false
return overview.mouseScrolled(mouseX, mouseY, horizontalAmount, verticalAmount)
}
}
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
index cf1cf1d..a023ce6 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
@@ -35,6 +35,7 @@ import moe.nea.firmament.util.mc.FakeSlot
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.render.drawGuiTexture
+import moe.nea.firmament.util.render.enableScissorWithoutTranslation
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.unformattedString
@@ -46,19 +47,28 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
val PLAYER_Y_INSET = 3
val SLOT_SIZE = 18
val PADDING = 10
- val PAGE_WIDTH = SLOT_SIZE * 9
+ val PAGE_SLOTS_WIDTH = SLOT_SIZE * 9
+ val PAGE_WIDTH = PAGE_SLOTS_WIDTH + 4
val HOTBAR_X = 12
val HOTBAR_Y = 67
val MAIN_INVENTORY_Y = 9
val SCROLL_BAR_WIDTH = 8
val SCROLL_BAR_HEIGHT = 16
+ val CONTROL_X_INSET = 3
+ val CONTROL_Y_INSET = 5
val CONTROL_WIDTH = 70
- val CONTROL_BACKGROUND_WIDTH = CONTROL_WIDTH + PLAYER_Y_INSET
- val CONTROL_HEIGHT = 100
+ val CONTROL_BACKGROUND_WIDTH = CONTROL_WIDTH + CONTROL_X_INSET + 1
+ val CONTROL_HEIGHT = 50
+
+ var scroll: Float = 0F
+ var lastRenderedInnerHeight = 0
+
+ fun resetScroll() {
+ if (!StorageOverlay.TConfig.retainScroll) scroll = 0F
+ }
}
var isExiting: Boolean = false
- var scroll: Float = 0F
var pageWidthCount = StorageOverlay.TConfig.columns
inner class Measurements {
@@ -67,20 +77,20 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
val x = width / 2 - overviewWidth / 2
val overviewHeight = minOf(
height - PLAYER_HEIGHT - minOf(80, height / 10),
- StorageOverlay.TConfig.height)
+ StorageOverlay.TConfig.height
+ )
val innerScrollPanelHeight = overviewHeight - PADDING * 2
val y = height / 2 - (overviewHeight + PLAYER_HEIGHT) / 2
val playerX = width / 2 - PLAYER_WIDTH / 2
val playerY = y + overviewHeight - PLAYER_Y_INSET
- val controlX = x - CONTROL_WIDTH
- val controlY = y + overviewHeight / 2 - CONTROL_HEIGHT / 2
+ val controlX = playerX - CONTROL_WIDTH + CONTROL_X_INSET
+ val controlY = playerY - CONTROL_Y_INSET
val totalWidth = overviewWidth
val totalHeight = overviewHeight - PLAYER_Y_INSET + PLAYER_HEIGHT
}
var measurements = Measurements()
- var lastRenderedInnerHeight = 0
public override fun init() {
super.init()
pageWidthCount = StorageOverlay.TConfig.columns
@@ -99,6 +109,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
coerceScroll(StorageOverlay.adjustScrollSpeed(verticalAmount).toFloat())
return true
}
+
fun coerceScroll(offset: Float) {
scroll = (scroll + offset)
.coerceAtMost(getMaxScroll())
@@ -116,6 +127,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
override fun close() {
isExiting = true
+ resetScroll()
super.close()
}
@@ -148,21 +160,29 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
fun editPages() {
isExiting = true
- val hs = MC.screen as? HandledScreen<*>
- if (StorageBackingHandle.fromScreen(hs) is StorageBackingHandle.Overview) {
- hs.customGui = null
- } else {
- MC.sendCommand("storage")
+ MC.instance.send {
+ val hs = MC.screen as? HandledScreen<*>
+ if (StorageBackingHandle.fromScreen(hs) is StorageBackingHandle.Overview) {
+ hs.customGui = null
+ hs.init(MC.instance, width, height)
+ } else {
+ MC.sendCommand("storage")
+ }
}
}
val guiContext = GuiContext(EmptyComponent())
private val knobStub = EmptyComponent()
- val editButton = FirmButtonComponent(TextComponent(tr("firmament.storage-overlay.edit-pages", "Edit Pages").string), action = ::editPages)
+ val editButton = FirmButtonComponent(
+ TextComponent(tr("firmament.storage-overlay.edit-pages", "Edit Pages").string),
+ action = ::editPages
+ )
val searchText = Property.of("") // TODO: sync with REI
- val searchField = TextFieldComponent(searchText, 100, GetSetter.constant(true),
- tr("firmament.storage-overlay.search.suggestion", "Search...").string,
- IMinecraft.instance.defaultFontRenderer)
+ val searchField = TextFieldComponent(
+ searchText, 100, GetSetter.constant(true),
+ tr("firmament.storage-overlay.search.suggestion", "Search...").string,
+ IMinecraft.INSTANCE.defaultFontRenderer
+ )
val controlComponent = PanelComponent(
ColumnComponent(
searchField,
@@ -185,25 +205,31 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
controllerBackground,
measurements.controlX,
measurements.controlY,
- CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT)
+ CONTROL_BACKGROUND_WIDTH, CONTROL_HEIGHT
+ )
context.drawMCComponentInPlace(
controlComponent,
measurements.controlX, measurements.controlY,
CONTROL_WIDTH, CONTROL_HEIGHT,
- mouseX, mouseY)
+ mouseX, mouseY
+ )
}
fun drawBackgrounds(context: DrawContext) {
- context.drawGuiTexture(upperBackgroundSprite,
- measurements.x,
- measurements.y,
- measurements.overviewWidth,
- measurements.overviewHeight)
- context.drawGuiTexture(playerInventorySprite,
- measurements.playerX,
- measurements.playerY,
- PLAYER_WIDTH,
- PLAYER_HEIGHT)
+ context.drawGuiTexture(
+ upperBackgroundSprite,
+ measurements.x,
+ measurements.y,
+ measurements.overviewWidth,
+ measurements.overviewHeight
+ )
+ context.drawGuiTexture(
+ playerInventorySprite,
+ measurements.playerX,
+ measurements.playerY,
+ PLAYER_WIDTH,
+ PLAYER_HEIGHT
+ )
}
fun getPlayerInventorySlotPosition(int: Int): Pair<Int, Int> {
@@ -217,7 +243,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
}
fun drawPlayerInventory(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
- val items = MC.player?.inventory?.main ?: return
+ val items = MC.player?.inventory?.mainStacks ?: return
items.withIndex().forEach { (index, item) ->
val (x, y) = getPlayerInventorySlotPosition(index)
context.drawItem(item, x, y, 0)
@@ -226,24 +252,28 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
}
fun getScrollBarRect(): Rectangle {
- return Rectangle(measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING,
- measurements.y + PADDING,
- SCROLL_BAR_WIDTH,
- measurements.innerScrollPanelHeight)
+ return Rectangle(
+ measurements.x + PADDING + measurements.innerScrollPanelWidth + PADDING,
+ measurements.y + PADDING,
+ SCROLL_BAR_WIDTH,
+ measurements.innerScrollPanelHeight
+ )
}
fun getScrollPanelInner(): Rectangle {
- return Rectangle(measurements.x + PADDING,
- measurements.y + PADDING,
- measurements.innerScrollPanelWidth,
- measurements.innerScrollPanelHeight)
+ return Rectangle(
+ measurements.x + PADDING,
+ measurements.y + PADDING,
+ measurements.innerScrollPanelWidth,
+ measurements.innerScrollPanelHeight
+ )
}
fun createScissors(context: DrawContext) {
val rect = getScrollPanelInner()
- context.enableScissor(
- rect.minX, rect.minY,
- rect.maxX, rect.maxY
+ context.enableScissorWithoutTranslation(
+ rect.minX.toFloat(), rect.minY.toFloat(),
+ rect.maxX.toFloat(), rect.maxY.toFloat(),
)
}
@@ -256,12 +286,13 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
createScissors(context)
val data = StorageOverlay.Data.data ?: StorageData()
layoutedForEach(data) { rect, page, inventory ->
- drawPage(context,
- rect.x,
- rect.y,
- page, inventory,
- if (excluding == page) slots else null,
- slotOffset
+ drawPage(
+ context,
+ rect.x,
+ rect.y,
+ page, inventory,
+ if (excluding == page) slots else null,
+ slotOffset
)
}
context.disableScissor()
@@ -281,11 +312,13 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
knobGrabbed = false
return true
}
- if (clickMCComponentInPlace(controlComponent,
- measurements.controlX, measurements.controlY,
- CONTROL_WIDTH, CONTROL_HEIGHT,
- mouseX.toInt(), mouseY.toInt(),
- MouseEvent.Click(button, false))
+ if (clickMCComponentInPlace(
+ controlComponent,
+ measurements.controlX, measurements.controlY,
+ CONTROL_WIDTH, CONTROL_HEIGHT,
+ mouseX.toInt(), mouseY.toInt(),
+ MouseEvent.Click(button, false)
+ )
) return true
return super.mouseReleased(mouseX, mouseY, button)
}
@@ -302,6 +335,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
}
fun mouseClicked(mouseX: Double, mouseY: Double, button: Int, activePage: StoragePageSlot?): Boolean {
+ guiContext.setFocusedElement(null) // Blur all elements. They will be refocused by clickMCComponentInPlace if in doubt, and we don't have any double click components.
if (getScrollPanelInner().contains(mouseX, mouseY)) {
val data = StorageOverlay.Data.data ?: StorageData()
layoutedForEach(data) { rect, page, _ ->
@@ -320,11 +354,13 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
knobGrabbed = true
return true
}
- if (clickMCComponentInPlace(controlComponent,
- measurements.controlX, measurements.controlY,
- CONTROL_WIDTH, CONTROL_HEIGHT,
- mouseX.toInt(), mouseY.toInt(),
- MouseEvent.Click(button, true))
+ if (clickMCComponentInPlace(
+ controlComponent,
+ measurements.controlX, measurements.controlY,
+ CONTROL_WIDTH, CONTROL_HEIGHT,
+ mouseX.toInt(), mouseY.toInt(),
+ MouseEvent.Click(button, true)
+ )
) return true
return false
}
@@ -347,7 +383,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
controlComponent,
measurements.controlX, measurements.controlY,
CONTROL_WIDTH, CONTROL_HEIGHT,
- KeyboardEvent.KeyPressed(keyCode, false)
+ KeyboardEvent.KeyPressed(keyCode, scanCode, false)
)
) {
return true
@@ -355,12 +391,16 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
return super.keyReleased(keyCode, scanCode, modifiers)
}
+ override fun shouldCloseOnEsc(): Boolean {
+ return this === MC.screen // Fixes this UI closing the handled screen on Escape press.
+ }
+
override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
if (typeMCComponentInPlace(
controlComponent,
measurements.controlX, measurements.controlY,
CONTROL_WIDTH, CONTROL_HEIGHT,
- KeyboardEvent.KeyPressed(keyCode, true)
+ KeyboardEvent.KeyPressed(keyCode, scanCode, true)
)
) {
return true
@@ -414,7 +454,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
val filter = getFilteredPages()
for ((page, inventory) in data.storageInventories.entries) {
if (page !in filter) continue
- val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 4 + textRenderer.fontHeight }
+ val currentHeight = inventory.inventory?.let { it.rows * SLOT_SIZE + 6 + textRenderer.fontHeight }
?: 18
maxHeight = maxOf(maxHeight, currentHeight)
val rect = Rectangle(
@@ -446,24 +486,43 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
val inv = inventory.inventory
if (inv == null) {
context.drawGuiTexture(upperBackgroundSprite, x, y, PAGE_WIDTH, 18)
- context.drawText(textRenderer,
- Text.literal("TODO: open this page"),
- x + 4,
- y + 4,
- -1,
- true)
+ context.drawText(
+ textRenderer,
+ Text.literal("TODO: open this page"),
+ x + 4,
+ y + 4,
+ -1,
+ true
+ )
return 18
}
assertTrueOr(slots == null || slots.size == inv.stacks.size) { return 0 }
- val name = page.defaultName()
- context.drawText(textRenderer, Text.literal(name), x + 4, y + 2,
- if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true)
- context.drawGuiTexture(slotRowSprite, x, y + 4 + textRenderer.fontHeight, PAGE_WIDTH, inv.rows * SLOT_SIZE)
+ val name = inventory.title
+ val pageHeight = inv.rows * SLOT_SIZE + 8 + textRenderer.fontHeight
+ if (slots != null && StorageOverlay.TConfig.outlineActiveStoragePage)
+ context.drawBorder(
+ x,
+ y + 3 + textRenderer.fontHeight,
+ PAGE_WIDTH,
+ inv.rows * SLOT_SIZE + 4,
+ StorageOverlay.TConfig.outlineActiveStoragePageColour.getEffectiveColourRGB()
+ )
+ context.drawText(
+ textRenderer, Text.literal(name), x + 6, y + 3,
+ if (slots == null) 0xFFFFFFFF.toInt() else 0xFFFFFF00.toInt(), true
+ )
+ context.drawGuiTexture(
+ slotRowSprite,
+ x + 2,
+ y + 5 + textRenderer.fontHeight,
+ PAGE_SLOTS_WIDTH,
+ inv.rows * SLOT_SIZE
+ )
inv.stacks.forEachIndexed { index, stack ->
- val slotX = (index % 9) * SLOT_SIZE + x + 1
- val slotY = (index / 9) * SLOT_SIZE + y + 4 + textRenderer.fontHeight + 1
- val fakeSlot = FakeSlot(stack, slotX, slotY)
+ val slotX = (index % 9) * SLOT_SIZE + x + 3
+ val slotY = (index / 9) * SLOT_SIZE + y + 5 + textRenderer.fontHeight + 1
if (slots == null) {
+ val fakeSlot = FakeSlot(stack, slotX, slotY)
SlotRenderEvents.Before.publish(SlotRenderEvents.Before(context, fakeSlot))
context.drawItem(stack, slotX, slotY)
context.drawStackOverlay(textRenderer, stack, slotX, slotY)
@@ -474,22 +533,29 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
slot.y = slotY - slotOffset.y
}
}
- return inv.rows * SLOT_SIZE + 4 + textRenderer.fontHeight
+ return pageHeight + 6
}
fun getBounds(): List<Rectangle> {
return listOf(
- Rectangle(measurements.x,
- measurements.y,
- measurements.overviewWidth,
- measurements.overviewHeight),
- Rectangle(measurements.playerX,
- measurements.playerY,
- PLAYER_WIDTH,
- PLAYER_HEIGHT),
- Rectangle(measurements.controlX,
- measurements.controlY,
- CONTROL_WIDTH,
- CONTROL_HEIGHT))
+ Rectangle(
+ measurements.x,
+ measurements.y,
+ measurements.overviewWidth,
+ measurements.overviewHeight
+ ),
+ Rectangle(
+ measurements.playerX,
+ measurements.playerY,
+ PLAYER_WIDTH,
+ PLAYER_HEIGHT
+ ),
+ Rectangle(
+ measurements.controlX,
+ measurements.controlY,
+ CONTROL_WIDTH,
+ CONTROL_HEIGHT
+ )
+ )
}
}
diff --git a/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt b/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt
index 9112fab..65d7e8c 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverviewScreen.kt
@@ -22,39 +22,49 @@ class StorageOverviewScreen() : Screen(Text.empty()) {
Items.GRAY_DYE
)
val pageWidth get() = 19 * 9
+
+ var scroll = 0
+ var lastRenderedHeight = 0
}
val content = StorageOverlay.Data.data ?: StorageData()
var isClosing = false
- var scroll = 0
- var lastRenderedHeight = 0
+ override fun init() {
+ super.init()
+ scroll = scroll.coerceAtMost(getMaxScroll()).coerceAtLeast(0)
+ }
+
+ override fun close() {
+ if (!StorageOverlay.TConfig.retainScroll) scroll = 0
+ super.close()
+ }
override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
super.render(context, mouseX, mouseY, delta)
context.fill(0, 0, width, height, 0x90000000.toInt())
layoutedForEach { (key, value), offsetX, offsetY ->
- context.matrices.push()
- context.matrices.translate(offsetX.toFloat(), offsetY.toFloat(), 0F)
+ context.matrices.pushMatrix()
+ context.matrices.translate(offsetX.toFloat(), offsetY.toFloat())
renderStoragePage(context, value, mouseX - offsetX, mouseY - offsetY)
- context.matrices.pop()
+ context.matrices.popMatrix()
}
}
inline fun layoutedForEach(onEach: (data: Pair<StoragePageSlot, StorageData.StorageInventory>, offsetX: Int, offsetY: Int) -> Unit) {
var offsetY = 0
- var currentMaxHeight = StorageOverlay.config.margin - StorageOverlay.config.padding - scroll
+ var currentMaxHeight = StorageOverlay.TConfig.margin - StorageOverlay.TConfig.padding - scroll
var totalHeight = -currentMaxHeight
content.storageInventories.onEachIndexed { index, (key, value) ->
- val pageX = (index % StorageOverlay.config.columns)
+ val pageX = (index % StorageOverlay.TConfig.columns)
if (pageX == 0) {
- currentMaxHeight += StorageOverlay.config.padding
+ currentMaxHeight += StorageOverlay.TConfig.padding
offsetY += currentMaxHeight
totalHeight += currentMaxHeight
currentMaxHeight = 0
}
val xPosition =
- width / 2 - (StorageOverlay.config.columns * (pageWidth + StorageOverlay.config.padding) - StorageOverlay.config.padding) / 2 + pageX * (pageWidth + StorageOverlay.config.padding)
+ width / 2 - (StorageOverlay.TConfig.columns * (pageWidth + StorageOverlay.TConfig.padding) - StorageOverlay.TConfig.padding) / 2 + pageX * (pageWidth + StorageOverlay.TConfig.padding)
onEach(Pair(key, value), xPosition, offsetY)
val height = getStorePageHeight(value)
currentMaxHeight = max(currentMaxHeight, height)
@@ -88,10 +98,12 @@ class StorageOverviewScreen() : Screen(Text.empty()) {
): Boolean {
scroll =
(scroll + StorageOverlay.adjustScrollSpeed(verticalAmount)).toInt()
- .coerceAtMost(lastRenderedHeight - height + 2 * StorageOverlay.config.margin).coerceAtLeast(0)
+ .coerceAtMost(getMaxScroll()).coerceAtLeast(0)
return true
}
+ private fun getMaxScroll() = lastRenderedHeight - height + 2 * StorageOverlay.TConfig.margin
+
private fun renderStoragePage(context: DrawContext, page: StorageData.StorageInventory, mouseX: Int, mouseY: Int) {
context.drawText(MC.font, page.title, 2, 2, -1, true)
val inventory = page.inventory
diff --git a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
index e07df8a..83e0d19 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
@@ -13,53 +11,65 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
+import kotlin.jvm.optionals.getOrNull
import net.minecraft.item.ItemStack
import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtIo
import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.NbtSizeTracker
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.mc.TolerantRegistriesOps
@Serializable(with = VirtualInventory.Serializer::class)
data class VirtualInventory(
- val stacks: List<ItemStack>
+ val stacks: List<ItemStack>
) {
- val rows = stacks.size / 9
+ val rows = stacks.size / 9
+
+ init {
+ assert(stacks.size % 9 == 0)
+ assert(stacks.size / 9 in 1..5)
+ }
- init {
- assert(stacks.size % 9 == 0)
- assert(stacks.size / 9 in 1..5)
- }
+ object Serializer : KSerializer<VirtualInventory> {
+ const val INVENTORY = "INVENTORY"
+ override val descriptor: SerialDescriptor
+ get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING)
- object Serializer : KSerializer<VirtualInventory> {
- const val INVENTORY = "INVENTORY"
- override val descriptor: SerialDescriptor
- get() = PrimitiveSerialDescriptor("VirtualInventory", PrimitiveKind.STRING)
+ override fun deserialize(decoder: Decoder): VirtualInventory {
+ val s = decoder.decodeString()
+ val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000))
+ val items = n.getList(INVENTORY).getOrNull()
+ 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 }
+ } ?: listOf())
+ }
- 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/features/items/BlockZapperOverlay.kt b/src/main/kotlin/features/items/BlockZapperOverlay.kt
new file mode 100644
index 0000000..a853012
--- /dev/null
+++ b/src/main/kotlin/features/items/BlockZapperOverlay.kt
@@ -0,0 +1,139 @@
+package moe.nea.firmament.features.items
+
+import io.github.notenoughupdates.moulconfig.ChromaColour
+import java.util.LinkedList
+import net.minecraft.block.Block
+import net.minecraft.block.BlockState
+import net.minecraft.block.Blocks
+import net.minecraft.util.hit.BlockHitResult
+import net.minecraft.util.hit.HitResult
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.WorldKeyboardEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.render.RenderInWorldContext
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+
+object BlockZapperOverlay {
+ val identifier: String
+ get() = "block-zapper-overlay"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.ITEMS) {
+ var blockZapperOverlay by toggle("block-zapper-overlay") { false }
+ val color by colour("color") { ChromaColour.fromStaticRGB(160, 0, 0, 60) }
+ var undoKey by keyBindingWithDefaultUnbound("undo-key")
+ }
+
+ val bannedZapper: List<Block> = listOf<Block>(
+ Blocks.WHEAT,
+ Blocks.CARROTS,
+ Blocks.POTATOES,
+ Blocks.PUMPKIN,
+ Blocks.PUMPKIN_STEM,
+ Blocks.MELON,
+ Blocks.MELON_STEM,
+ Blocks.CACTUS,
+ Blocks.SUGAR_CANE,
+ Blocks.NETHER_WART,
+ Blocks.TALL_GRASS,
+ Blocks.SUNFLOWER,
+ Blocks.FARMLAND,
+ Blocks.BREWING_STAND,
+ Blocks.SNOW,
+ Blocks.RED_MUSHROOM,
+ Blocks.BROWN_MUSHROOM,
+ )
+
+ private val zapperOffsets: List<BlockPos> = listOf(
+ BlockPos(0, 0, -1),
+ BlockPos(0, 0, 1),
+ BlockPos(-1, 0, 0),
+ BlockPos(1, 0, 0),
+ BlockPos(0, 1, 0),
+ BlockPos(0, -1, 0)
+ )
+
+ // Skidded from NEU
+ // Credit: https://github.com/NotEnoughUpdates/NotEnoughUpdates/blob/9b1fcfebc646e9fb69f99006327faa3e734e5f51/src/main/java/io/github/moulberry/notenoughupdates/miscfeatures/CustomItemEffects.java#L1281-L1355 (Modified)
+ @Subscribe
+ fun renderBlockZapperOverlay(event: WorldRenderLastEvent) {
+ if (!TConfig.blockZapperOverlay) return
+ val player = MC.player ?: return
+ val world = player.world ?: return
+ val heldItem = MC.stackInHand
+ if (heldItem.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return
+ val hitResult = MC.instance.crosshairTarget ?: return
+
+ val zapperBlocks: HashSet<BlockPos> = HashSet()
+ val returnablePositions = LinkedList<BlockPos>()
+
+ if (hitResult is BlockHitResult && hitResult.type == HitResult.Type.BLOCK) {
+ var pos: BlockPos = hitResult.blockPos
+ val firstBlockState: BlockState = world.getBlockState(pos)
+ val block = firstBlockState.block
+
+ val initialAboveBlock = world.getBlockState(pos.up()).block
+ if (!bannedZapper.contains(initialAboveBlock) && !bannedZapper.contains(block)) {
+ var i = 0
+ while (i < 164) {
+ zapperBlocks.add(pos)
+ returnablePositions.remove(pos)
+
+ val availableNeighbors: MutableList<BlockPos> = ArrayList()
+
+ for (offset in zapperOffsets) {
+ val newPos = pos.add(offset)
+
+ if (zapperBlocks.contains(newPos)) continue
+
+ val state: BlockState? = world.getBlockState(newPos)
+ if (state != null && state.block === block) {
+ val above = newPos.up()
+ val aboveBlock = world.getBlockState(above).block
+ if (!bannedZapper.contains(aboveBlock)) {
+ availableNeighbors.add(newPos)
+ }
+ }
+ }
+
+ if (availableNeighbors.size >= 2) {
+ returnablePositions.add(pos)
+ pos = availableNeighbors[0]
+ } else if (availableNeighbors.size == 1) {
+ pos = availableNeighbors[0]
+ } else if (returnablePositions.isEmpty()) {
+ break
+ } else {
+ i--
+ pos = returnablePositions.last()
+ }
+
+ i++
+ }
+ }
+
+ RenderInWorldContext.renderInWorld(event) {
+ if (MC.player?.isSneaking ?: false) {
+ zapperBlocks.forEach {
+ block(it, TConfig.color.getEffectiveColourRGB())
+ }
+ } else {
+ sharedVoxelSurface(zapperBlocks, TConfig.color.getEffectiveColourRGB())
+ }
+ }
+ }
+ }
+
+ @Subscribe
+ fun onWorldKeyboard(it: WorldKeyboardEvent) {
+ if (!TConfig.undoKey.isBound) return
+ if (!it.matches(TConfig.undoKey)) return
+ if (MC.stackInHand.skyBlockId != SkyBlockItems.BLOCK_ZAPPER) return
+ MC.sendCommand("undozap")
+ }
+}
diff --git a/src/main/kotlin/features/items/BonemerangOverlay.kt b/src/main/kotlin/features/items/BonemerangOverlay.kt
new file mode 100644
index 0000000..1310154
--- /dev/null
+++ b/src/main/kotlin/features/items/BonemerangOverlay.kt
@@ -0,0 +1,94 @@
+package moe.nea.firmament.features.items
+
+import me.shedaniel.math.Color
+import org.joml.Vector2i
+import net.minecraft.entity.LivingEntity
+import net.minecraft.entity.decoration.ArmorStandEntity
+import net.minecraft.entity.player.PlayerEntity
+import net.minecraft.util.Formatting
+import net.minecraft.util.math.Box
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.EntityRenderTintEvent
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.render.TintedOverlayTexture
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+import moe.nea.firmament.util.tr
+
+object BonemerangOverlay {
+ val identifier: String
+ get() = "bonemerang-overlay"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.ITEMS) {
+ var bonemerangOverlay by toggle("bonemerang-overlay") { false }
+ val bonemerangOverlayHud by position("bonemerang-overlay-hud", 80, 10) { Vector2i() }
+ var highlightHitEntities by toggle("highlight-hit-entities") { false }
+ }
+
+ fun getEntities(): MutableSet<LivingEntity> {
+ val entities = mutableSetOf<LivingEntity>()
+ val camera = MC.camera as? PlayerEntity ?: return entities
+ val player = MC.player ?: return entities
+ val world = player.world ?: return entities
+
+ val cameraPos = camera.eyePos
+ val rayDirection = camera.rotationVector.normalize()
+ val endPos = cameraPos.add(rayDirection.multiply(15.0))
+ val foundEntities = world.getOtherEntities(camera, Box(cameraPos, endPos).expand(1.0))
+
+ for (entity in foundEntities) {
+ if (entity !is LivingEntity || entity is ArmorStandEntity || entity.isInvisible) continue
+ val hitResult = entity.boundingBox.expand(0.35).raycast(cameraPos, endPos).orElse(null)
+ if (hitResult != null) entities.add(entity)
+ }
+
+ return entities
+ }
+
+
+ val throwableWeapons = listOf(
+ SkyBlockItems.BONE_BOOMERANG, SkyBlockItems.STARRED_BONE_BOOMERANG,
+ SkyBlockItems.TRIBAL_SPEAR,
+ )
+
+
+ @Subscribe
+ fun onEntityRender(event: EntityRenderTintEvent) {
+ if (!TConfig.highlightHitEntities) return
+ if (MC.stackInHand.skyBlockId !in throwableWeapons) return
+
+ val entities = getEntities()
+ if (entities.isEmpty()) return
+ if (event.entity !in entities) return
+
+ val tintOverlay by lazy {
+ TintedOverlayTexture().setColor(Color.ofOpaque(Formatting.BLUE.colorValue!!))
+ }
+
+ event.renderState.overlayTexture_firmament = tintOverlay
+ }
+
+
+ @Subscribe
+ fun onRenderHud(it: HudRenderEvent) {
+ if (!TConfig.bonemerangOverlay) return
+ if (MC.stackInHand.skyBlockId !in throwableWeapons) return
+
+ val entities = getEntities()
+
+ it.context.matrices.pushMatrix()
+ TConfig.bonemerangOverlayHud.applyTransformations(it.context.matrices)
+ it.context.drawText(
+ MC.font, String.format(
+ tr(
+ "firmament.bonemerang-overlay.bonemerang-overlay.display", "Bonemerang Targets: %s"
+ ).string, entities.size
+ ), 0, 0, -1, true
+ )
+ it.context.matrices.popMatrix()
+ }
+}
diff --git a/src/main/kotlin/features/items/EtherwarpOverlay.kt b/src/main/kotlin/features/items/EtherwarpOverlay.kt
new file mode 100644
index 0000000..ba712b3
--- /dev/null
+++ b/src/main/kotlin/features/items/EtherwarpOverlay.kt
@@ -0,0 +1,230 @@
+package moe.nea.firmament.features.items
+
+import io.github.notenoughupdates.moulconfig.ChromaColour
+import net.minecraft.block.Blocks
+import net.minecraft.registry.entry.RegistryEntry
+import net.minecraft.registry.tag.BlockTags
+import net.minecraft.registry.tag.TagKey
+import net.minecraft.text.Text
+import net.minecraft.util.hit.BlockHitResult
+import net.minecraft.util.hit.HitResult
+import net.minecraft.util.math.BlockPos
+import net.minecraft.util.math.Vec3d
+import net.minecraft.world.BlockView
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.extraAttributes
+import moe.nea.firmament.util.render.RenderInWorldContext
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+import moe.nea.firmament.util.tr
+
+object EtherwarpOverlay {
+ val identifier: String
+ get() = "etherwarp-overlay"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.ITEMS) {
+ var etherwarpOverlay by toggle("etherwarp-overlay") { false }
+ var onlyShowWhileSneaking by toggle("only-show-while-sneaking") { true }
+ var cube by toggle("cube") { true }
+ val cubeColour by colour("cube-colour") { ChromaColour.fromStaticRGB(172, 0, 255, 60) }
+ val failureCubeColour by colour("cube-colour-fail") { ChromaColour.fromStaticRGB(255, 0, 172, 60) }
+ val tooCloseCubeColour by colour("cube-colour-tooclose") { ChromaColour.fromStaticRGB(0, 255, 0, 60) }
+ val tooFarCubeColour by colour("cube-colour-toofar") { ChromaColour.fromStaticRGB(255, 255, 0, 60) }
+ var wireframe by toggle("wireframe") { false }
+ var failureText by toggle("failure-text") { false }
+ }
+
+ enum class EtherwarpResult(val label: Text?, val color: () -> ChromaColour) {
+ SUCCESS(null, TConfig::cubeColour),
+ INTERACTION_BLOCKED(
+ tr("firmament.etherwarp.fail.tooclosetointeractable", "Too close to interactable"),
+ TConfig::tooCloseCubeColour
+ ),
+ TOO_DISTANT(tr("firmament.etherwarp.fail.toofar", "Too far away"), TConfig::tooFarCubeColour),
+ OCCUPIED(tr("firmament.etherwarp.fail.occupied", "Occupied"), TConfig::failureCubeColour),
+ }
+
+ val interactionBlocked = Checker(
+ setOf(
+ Blocks.HOPPER,
+ Blocks.CHEST,
+ Blocks.ENDER_CHEST,
+ Blocks.FURNACE,
+ Blocks.CRAFTING_TABLE,
+ Blocks.CAULDRON,
+ Blocks.WATER_CAULDRON,
+ Blocks.ENCHANTING_TABLE,
+ Blocks.DISPENSER,
+ Blocks.DROPPER,
+ Blocks.BREWING_STAND,
+ Blocks.TRAPPED_CHEST,
+ ),
+ setOf(
+ BlockTags.DOORS,
+ BlockTags.TRAPDOORS,
+ BlockTags.ANVIL,
+ BlockTags.FENCE_GATES,
+ )
+ )
+
+ data class Checker<T>(
+ val direct: Set<T>,
+ val byTag: Set<TagKey<T>>,
+ ) {
+ fun matches(entry: RegistryEntry<T>): Boolean {
+ return entry.value() in direct || checkTags(entry, byTag)
+ }
+ }
+
+ val etherwarpHallpasses = Checker(
+ setOf(
+ Blocks.CREEPER_HEAD,
+ Blocks.CREEPER_WALL_HEAD,
+ Blocks.DRAGON_HEAD,
+ Blocks.DRAGON_WALL_HEAD,
+ Blocks.SKELETON_SKULL,
+ Blocks.SKELETON_WALL_SKULL,
+ Blocks.WITHER_SKELETON_SKULL,
+ Blocks.WITHER_SKELETON_WALL_SKULL,
+ Blocks.PIGLIN_HEAD,
+ Blocks.PIGLIN_WALL_HEAD,
+ Blocks.ZOMBIE_HEAD,
+ Blocks.ZOMBIE_WALL_HEAD,
+ Blocks.PLAYER_HEAD,
+ Blocks.PLAYER_WALL_HEAD,
+ Blocks.REPEATER,
+ Blocks.COMPARATOR,
+ Blocks.BIG_DRIPLEAF_STEM,
+ Blocks.MOSS_CARPET,
+ Blocks.PALE_MOSS_CARPET,
+ Blocks.COCOA,
+ Blocks.LADDER,
+ ),
+ setOf(
+ BlockTags.FLOWER_POTS,
+ BlockTags.WOOL_CARPETS,
+ ),
+ )
+ val etherwarpConsidersFat = Checker(
+ setOf(), setOf(
+ // Wall signs have a hitbox
+ BlockTags.ALL_SIGNS, BlockTags.ALL_HANGING_SIGNS,
+ BlockTags.BANNERS,
+ )
+ )
+
+
+ fun <T> checkTags(holder: RegistryEntry<out T>, set: Set<TagKey<out T>>) =
+ holder.streamTags()
+ .anyMatch(set::contains)
+
+
+ fun isEtherwarpTransparent(world: BlockView, blockPos: BlockPos): Boolean {
+ val blockState = world.getBlockState(blockPos)
+ val block = blockState.block
+ if (etherwarpConsidersFat.matches(blockState.registryEntry))
+ return false
+ if (block.defaultState.getCollisionShape(world, blockPos).isEmpty)
+ return true
+ if (etherwarpHallpasses.matches(blockState.registryEntry))
+ return true
+ return false
+ }
+
+ sealed interface EtherwarpBlockHit {
+ data class BlockHit(val blockPos: BlockPos) : EtherwarpBlockHit
+ data object Miss : EtherwarpBlockHit
+ }
+
+ fun raycastWithEtherwarpTransparency(world: BlockView, start: Vec3d, end: Vec3d): EtherwarpBlockHit {
+ return BlockView.raycast<EtherwarpBlockHit, Unit>(
+ start, end, Unit,
+ { _, blockPos ->
+ if (isEtherwarpTransparent(world, blockPos)) {
+ return@raycast null
+ }
+// val defaultedState = world.getBlockState(blockPos).block.defaultState
+// val hitShape = defaultedState.getCollisionShape(
+// world,
+// blockPos,
+// ShapeContext.absent()
+// )
+// if (world.raycastBlock(start, end, blockPos, hitShape, defaultedState) == null) {
+// return@raycast null
+// }
+ return@raycast EtherwarpBlockHit.BlockHit(blockPos)
+ },
+ { EtherwarpBlockHit.Miss })
+ }
+
+ enum class EtherwarpItemKind {
+ MERGED,
+ RAW
+ }
+
+ @Subscribe
+ fun renderEtherwarpOverlay(event: WorldRenderLastEvent) {
+ if (!TConfig.etherwarpOverlay) return
+ val player = MC.player ?: return
+ if (TConfig.onlyShowWhileSneaking && !player.isSneaking) return
+ val world = player.world
+ val heldItem = MC.stackInHand
+ val etherwarpTyp = run {
+ if (heldItem.extraAttributes.contains("ethermerge"))
+ EtherwarpItemKind.MERGED
+ else if (heldItem.skyBlockId == SkyBlockItems.ETHERWARP_CONDUIT)
+ EtherwarpItemKind.RAW
+ else
+ return
+ }
+ val playerEyeHeight = // Sneaking: 1.27 (1.21) 1.54 (1.8.9) / Upright: 1.62 (1.8.9,1.21)
+ if (player.isSneaking || etherwarpTyp == EtherwarpItemKind.MERGED)
+ (if (SBData.skyblockLocation?.isModernServer ?: false) 1.27 else 1.54)
+ else 1.62
+ val playerEyePos = player.pos.add(0.0, playerEyeHeight, 0.0)
+ val start = playerEyePos
+ val end = player.getRotationVec(0F).multiply(120.0).add(playerEyePos)
+ val hitResult = raycastWithEtherwarpTransparency(
+ world,
+ start,
+ end,
+ )
+ if (hitResult !is EtherwarpBlockHit.BlockHit) return
+ val blockPos = hitResult.blockPos
+ val success = run {
+ if (!isEtherwarpTransparent(world, blockPos.up()))
+ EtherwarpResult.OCCUPIED
+ else if (!isEtherwarpTransparent(world, blockPos.up(2)))
+ EtherwarpResult.OCCUPIED
+ else if (player.squaredDistanceTo(blockPos.toCenterPos()) > 61 * 61)
+ EtherwarpResult.TOO_DISTANT
+ else if ((MC.instance.crosshairTarget as? BlockHitResult)
+ ?.takeIf { it.type == HitResult.Type.BLOCK }
+ ?.let { interactionBlocked.matches(world.getBlockState(it.blockPos).registryEntry) }
+ ?: false
+ )
+ EtherwarpResult.INTERACTION_BLOCKED
+ else
+ EtherwarpResult.SUCCESS
+ }
+ RenderInWorldContext.renderInWorld(event) {
+ if (TConfig.cube)
+ block(
+ blockPos,
+ success.color().getEffectiveColourRGB()
+ )
+ if (TConfig.wireframe) wireframeCube(blockPos, 10f)
+ if (TConfig.failureText && success.label != null) {
+ withFacingThePlayer(blockPos.toCenterPos()) {
+ text(success.label)
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/macros/ComboProcessor.kt b/src/main/kotlin/features/macros/ComboProcessor.kt
new file mode 100644
index 0000000..a01f8b7
--- /dev/null
+++ b/src/main/kotlin/features/macros/ComboProcessor.kt
@@ -0,0 +1,103 @@
+package moe.nea.firmament.features.macros
+
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.text.Text
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.events.WorldKeyboardEvent
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.tr
+
+object ComboProcessor {
+
+ var rootTrie: Branch = Branch(mapOf())
+ private set
+
+ var activeTrie: Branch = rootTrie
+ private set
+
+ var isInputting = false
+ var lastInput = TimeMark.farPast()
+ val breadCrumbs = mutableListOf<SavedKeyBinding>()
+
+ fun setActions(actions: List<ComboKeyAction>) {
+ rootTrie = KeyComboTrie.fromComboList(actions)
+ reset()
+ }
+
+ fun reset() {
+ activeTrie = rootTrie
+ lastInput = TimeMark.now()
+ isInputting = false
+ breadCrumbs.clear()
+ }
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ if (isInputting && lastInput.passedTime() > 3.seconds)
+ reset()
+ }
+
+
+ @Subscribe
+ fun onRender(event: HudRenderEvent) {
+ if (!isInputting) return
+ if (!event.isRenderingHud) return
+ event.context.matrices.pushMatrix()
+ val width = 120
+ event.context.matrices.translate(
+ (MC.window.scaledWidth - width) / 2F,
+ (MC.window.scaledHeight) / 2F + 8
+ )
+ val breadCrumbText = breadCrumbs.joinToString(" > ")
+ event.context.drawText(
+ MC.font,
+ tr("firmament.combo.active", "Current Combo: ").append(breadCrumbText),
+ 0,
+ 0,
+ -1,
+ true
+ )
+ event.context.matrices.translate(0F, MC.font.fontHeight + 2F)
+ for ((key, value) in activeTrie.nodes) {
+ event.context.drawText(
+ MC.font,
+ Text.literal("$breadCrumbText > $key: ").append(value.label),
+ 0,
+ 0,
+ -1,
+ true
+ )
+ event.context.matrices.translate(0F, MC.font.fontHeight + 1F)
+ }
+ event.context.matrices.popMatrix()
+ }
+
+ @Subscribe
+ fun onKeyBinding(event: WorldKeyboardEvent) {
+ val nextEntry = activeTrie.nodes.entries
+ .find { event.matches(it.key) }
+ if (nextEntry == null) {
+ reset()
+ return
+ }
+ event.cancel()
+ breadCrumbs.add(nextEntry.key)
+ lastInput = TimeMark.now()
+ isInputting = true
+ val value = nextEntry.value
+ when (value) {
+ is Branch -> {
+ activeTrie = value
+ }
+
+ is Leaf -> {
+ value.execute()
+ reset()
+ }
+ }.let { }
+ }
+}
diff --git a/src/main/kotlin/features/macros/HotkeyAction.kt b/src/main/kotlin/features/macros/HotkeyAction.kt
new file mode 100644
index 0000000..011f797
--- /dev/null
+++ b/src/main/kotlin/features/macros/HotkeyAction.kt
@@ -0,0 +1,40 @@
+package moe.nea.firmament.features.macros
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import net.minecraft.text.Text
+import moe.nea.firmament.util.MC
+
+@Serializable
+sealed interface HotkeyAction {
+ // TODO: execute
+ val label: Text
+ fun execute()
+}
+
+@Serializable
+@SerialName("command")
+data class CommandAction(val command: String) : HotkeyAction {
+ override val label: Text
+ get() = Text.literal("/$command")
+
+ override fun execute() {
+ MC.sendCommand(command)
+ }
+}
+
+// Mit onscreen anzeige:
+// F -> 1 /equipment
+// F -> 2 /wardrobe
+// Bei Combos: Keys buffern! (für wardrobe hotkeys beispielsweiße)
+
+// Radial menu
+// Hold F
+// Weight (mach eins doppelt so groß)
+// /equipment
+// /wardrobe
+
+// Bei allen: Filter!
+// - Nur in Dungeons / andere Insel
+// - Nur wenn ich Item X im inventar habe (fishing rod)
+
diff --git a/src/main/kotlin/features/macros/KeyComboTrie.kt b/src/main/kotlin/features/macros/KeyComboTrie.kt
new file mode 100644
index 0000000..1983b2e
--- /dev/null
+++ b/src/main/kotlin/features/macros/KeyComboTrie.kt
@@ -0,0 +1,73 @@
+package moe.nea.firmament.features.macros
+
+import kotlinx.serialization.Serializable
+import net.minecraft.text.Text
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.ErrorUtil
+
+sealed interface KeyComboTrie {
+ val label: Text
+
+ companion object {
+ fun fromComboList(
+ combos: List<ComboKeyAction>,
+ ): Branch {
+ val root = Branch(mutableMapOf())
+ for (combo in combos) {
+ var p = root
+ if (combo.keySequence.isEmpty()) {
+ ErrorUtil.softUserError("Key Combo for ${combo.action.label.string} is empty")
+ continue
+ }
+ for ((index, key) in combo.keySequence.withIndex()) {
+ val m = (p.nodes as MutableMap)
+ if (index == combo.keySequence.lastIndex) {
+ if (key in m) {
+ ErrorUtil.softUserError("Overlapping actions found for ${combo.keySequence.joinToString(" > ")} (another action ${m[key]} already exists).")
+ break
+ }
+
+ m[key] = Leaf(combo.action)
+ } else {
+ val c = m.getOrPut(key) { Branch(mutableMapOf()) }
+ if (c !is Branch) {
+ ErrorUtil.softUserError("Overlapping actions found for ${combo.keySequence} (final node exists at index $index) through another action already")
+ break
+ } else {
+ p = c
+ }
+ }
+ }
+ }
+ return root
+ }
+ }
+}
+
+@Serializable
+data class MacroWheel(
+ val keyBinding: SavedKeyBinding = SavedKeyBinding.unbound(),
+ val options: List<HotkeyAction>
+)
+
+@Serializable
+data class ComboKeyAction(
+ val action: HotkeyAction,
+ val keySequence: List<SavedKeyBinding> = listOf(),
+)
+
+data class Leaf(val action: HotkeyAction) : KeyComboTrie {
+ override val label: Text
+ get() = action.label
+
+ fun execute() {
+ action.execute()
+ }
+}
+
+data class Branch(
+ val nodes: Map<SavedKeyBinding, KeyComboTrie>
+) : KeyComboTrie {
+ override val label: Text
+ get() = Text.literal("...") // TODO: better labels
+}
diff --git a/src/main/kotlin/features/macros/MacroData.kt b/src/main/kotlin/features/macros/MacroData.kt
new file mode 100644
index 0000000..af1b0e8
--- /dev/null
+++ b/src/main/kotlin/features/macros/MacroData.kt
@@ -0,0 +1,19 @@
+package moe.nea.firmament.features.macros
+
+import kotlinx.serialization.Serializable
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.DataHolder
+
+@Serializable
+data class MacroData(
+ var comboActions: List<ComboKeyAction> = listOf(),
+ var wheels: List<MacroWheel> = listOf(),
+) {
+ @Config
+ object DConfig : DataHolder<MacroData>(kotlinx.serialization.serializer(), "macros", ::MacroData) {
+ override fun onLoad() {
+ ComboProcessor.setActions(data.comboActions)
+ RadialMacros.setWheels(data.wheels)
+ }
+ }
+}
diff --git a/src/main/kotlin/features/macros/MacroUI.kt b/src/main/kotlin/features/macros/MacroUI.kt
new file mode 100644
index 0000000..869466d
--- /dev/null
+++ b/src/main/kotlin/features/macros/MacroUI.kt
@@ -0,0 +1,293 @@
+package moe.nea.firmament.features.macros
+
+import io.github.notenoughupdates.moulconfig.common.text.StructuredText
+import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.gui.config.AllConfigsGui.toObservableList
+import moe.nea.firmament.gui.config.KeyBindingStateManager
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.ScreenUtil
+
+class MacroUI {
+
+
+ companion object {
+ @Subscribe
+ fun onCommands(event: CommandEvent.SubCommand) {
+ // TODO: add button in config
+ event.subcommand("macros") {
+ thenExecute {
+ ScreenUtil.setScreenLater(MoulConfigUtils.loadScreen("config/macros/index", MacroUI(), null))
+ }
+ }
+ }
+
+ }
+
+ @field:Bind("combos")
+ val combos = Combos()
+
+ @field:Bind("wheels")
+ val wheels = Wheels()
+ var dontSave = false
+
+ @Bind
+ fun beforeClose(): CloseEventListener.CloseAction {
+ if (!dontSave)
+ save()
+ return CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE
+ }
+
+ fun save() {
+ MacroData.DConfig.data.comboActions = combos.actions.map { it.asSaveable() }
+ MacroData.DConfig.data.wheels = wheels.wheels.map { it.asSaveable() }
+ MacroData.DConfig.markDirty()
+ RadialMacros.setWheels(MacroData.DConfig.data.wheels)
+ ComboProcessor.setActions(MacroData.DConfig.data.comboActions)
+ }
+
+ fun discard() {
+ dontSave = true
+ MC.screen?.close()
+ }
+
+ class Command(
+ @field:Bind("text")
+ var text: String,
+ val parent: Wheel,
+ ) {
+ @Bind
+ fun textR() = StructuredText.of(text)
+
+ @Bind
+ fun delete() {
+ parent.editableCommands.removeIf { it === this }
+ parent.editableCommands.update()
+ parent.commands.update()
+ }
+
+ fun asCommandAction() = CommandAction(text)
+ }
+
+ inner class Wheel(
+ val parent: Wheels,
+ var binding: SavedKeyBinding,
+ commands: List<CommandAction>,
+ ) {
+
+ fun asSaveable(): MacroWheel {
+ return MacroWheel(binding, commands.map { it.asCommandAction() })
+ }
+
+ @Bind("keyCombo")
+ fun text() = MoulConfigPlatform.wrap(binding.format())
+
+ @field:Bind("commands")
+ val commands = commands.mapTo(ObservableList(mutableListOf())) { Command(it.command, this) }
+
+ @field:Bind("editableCommands")
+ val editableCommands = this.commands.toObservableList()
+
+ @Bind
+ fun addOption() {
+ editableCommands.add(Command("", this))
+ }
+
+ @Bind
+ fun back() {
+ MC.screen?.close()
+ }
+
+ @Bind
+ fun edit() {
+ MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_wheel", this, MC.screen)
+ }
+
+ @Bind
+ fun delete() {
+ parent.wheels.removeIf { it === this }
+ parent.wheels.update()
+ }
+
+ val sm = KeyBindingStateManager(
+ { binding },
+ { binding = it },
+ ::blur,
+ ::requestFocus
+ )
+
+ @field:Bind
+ val button = sm.createButton()
+
+ init {
+ sm.updateLabel()
+ }
+
+ fun blur() {
+ button.blur()
+ }
+
+
+ fun requestFocus() {
+ button.requestFocus()
+ }
+ }
+
+ inner class Wheels {
+ @field:Bind("wheels")
+ val wheels: ObservableList<Wheel> = MacroData.DConfig.data.wheels.mapTo(ObservableList(mutableListOf())) {
+ Wheel(this, it.keyBinding, it.options.map { CommandAction((it as CommandAction).command) })
+ }
+
+ @Bind
+ fun discard() {
+ this@MacroUI.discard()
+ }
+
+ @Bind
+ fun saveAndClose() {
+ this@MacroUI.saveAndClose()
+ }
+
+ @Bind
+ fun save() {
+ this@MacroUI.save()
+ }
+
+ @Bind
+ fun addWheel() {
+ wheels.add(Wheel(this, SavedKeyBinding.unbound(), listOf()))
+ }
+ }
+
+ fun saveAndClose() {
+ save()
+ MC.screen?.close()
+ }
+
+ inner class Combos {
+ @field:Bind("actions")
+ val actions: ObservableList<ActionEditor> = ObservableList(
+ MacroData.DConfig.data.comboActions.mapTo(mutableListOf()) {
+ ActionEditor(it, this)
+ }
+ )
+
+ @Bind
+ fun addCommand() {
+ actions.add(
+ ActionEditor(
+ ComboKeyAction(
+ CommandAction("ac Hello from a Firmament Hotkey"),
+ listOf()
+ ),
+ this
+ )
+ )
+ }
+
+ @Bind
+ fun discard() {
+ this@MacroUI.discard()
+ }
+
+ @Bind
+ fun saveAndClose() {
+ this@MacroUI.saveAndClose()
+ }
+
+ @Bind
+ fun save() {
+ this@MacroUI.save()
+ }
+ }
+
+ class KeyBindingEditor(var binding: SavedKeyBinding, val parent: ActionEditor) {
+ val sm = KeyBindingStateManager(
+ { binding },
+ { binding = it },
+ ::blur,
+ ::requestFocus
+ )
+
+ @field:Bind
+ val button = sm.createButton()
+
+ init {
+ sm.updateLabel()
+ }
+
+ fun blur() {
+ button.blur()
+ }
+
+
+ fun requestFocus() {
+ button.requestFocus()
+ }
+
+ @Bind
+ fun delete() {
+ parent.combo.removeIf { it === this }
+ parent.combo.update()
+ }
+ }
+
+ class ActionEditor(val action: ComboKeyAction, val parent: Combos) {
+ fun asSaveable(): ComboKeyAction {
+ return ComboKeyAction(
+ CommandAction(command),
+ combo.map { it.binding }
+ )
+ }
+
+ @field:Bind("command")
+ var command: String = (action.action as CommandAction).command
+
+ @Bind
+ fun commandR() = StructuredText.of(command)
+
+ @field:Bind("combo")
+ val combo = action.keySequence.map { KeyBindingEditor(it, this) }.toObservableList()
+
+ @Bind
+ fun formattedCombo() =
+ StructuredText.of(combo.joinToString(" > ") { it.binding.toString() }) // TODO: this can be joined without .toString()
+
+ @Bind
+ fun addStep() {
+ combo.add(KeyBindingEditor(SavedKeyBinding.unbound(), this))
+ }
+
+ @Bind
+ fun back() {
+ MC.screen?.close()
+ }
+
+ @Bind
+ fun delete() {
+ parent.actions.removeIf { it === this }
+ parent.actions.update()
+ }
+
+ @Bind
+ fun edit() {
+ MC.screen = MoulConfigUtils.loadScreen("config/macros/editor_combo", this, MC.screen)
+ }
+ }
+}
+
+private fun <T> ObservableList<T>.setAll(ts: Collection<T>) {
+ val observer = this.observer
+ this.clear()
+ this.addAll(ts)
+ this.observer = observer
+ this.update()
+}
diff --git a/src/main/kotlin/features/macros/RadialMenu.kt b/src/main/kotlin/features/macros/RadialMenu.kt
new file mode 100644
index 0000000..43e65a7
--- /dev/null
+++ b/src/main/kotlin/features/macros/RadialMenu.kt
@@ -0,0 +1,153 @@
+package moe.nea.firmament.features.macros
+
+import me.shedaniel.math.Color
+import org.joml.Vector2f
+import util.render.CustomRenderLayers
+import kotlin.math.atan2
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.math.sqrt
+import net.minecraft.client.gui.DrawContext
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.events.WorldKeyboardEvent
+import moe.nea.firmament.events.WorldMouseMoveEvent
+import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenu
+import moe.nea.firmament.features.macros.RadialMenuViewer.RadialMenuOption
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.render.RenderCircleProgress
+import moe.nea.firmament.util.render.drawLine
+import moe.nea.firmament.util.render.lerpAngle
+import moe.nea.firmament.util.render.wrapAngle
+import moe.nea.firmament.util.render.τ
+
+object RadialMenuViewer {
+ interface RadialMenu {
+ val key: SavedKeyBinding
+ val options: List<RadialMenuOption>
+ }
+
+ interface RadialMenuOption {
+ val isEnabled: Boolean
+ fun resolve()
+ fun renderSlice(drawContext: DrawContext)
+ }
+
+ var activeMenu: RadialMenu? = null
+ set(value) {
+ if (value?.options.isNullOrEmpty()) {
+ field = null
+ } else {
+ field = value
+ }
+ delta = Vector2f(0F, 0F)
+ }
+ var delta = Vector2f(0F, 0F)
+ val maxSelectionSize = 100F
+
+ @Subscribe
+ fun onMouseMotion(event: WorldMouseMoveEvent) {
+ val menu = activeMenu ?: return
+ event.cancel()
+ delta.add(event.deltaX.toFloat(), event.deltaY.toFloat())
+ val m = delta.lengthSquared()
+ if (m > maxSelectionSize * maxSelectionSize) {
+ delta.mul(maxSelectionSize / sqrt(m))
+ }
+ }
+
+ val INNER_CIRCLE_RADIUS = 16
+
+ @Subscribe
+ fun onRender(event: HudRenderEvent) {
+ val menu = activeMenu ?: return
+ val mat = event.context.matrices
+ mat.pushMatrix()
+ mat.translate(
+ (MC.window.scaledWidth) / 2F,
+ (MC.window.scaledHeight) / 2F,
+ )
+ val sliceWidth = (τ / menu.options.size).toFloat()
+ var selectedAngle = wrapAngle(atan2(delta.y, delta.x))
+ if (delta.lengthSquared() < INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS)
+ selectedAngle = Float.NaN
+ for ((idx, option) in menu.options.withIndex()) {
+ val range = (sliceWidth * idx)..(sliceWidth * (idx + 1))
+ mat.pushMatrix()
+ mat.scale(64F, 64F)
+ val cutout = INNER_CIRCLE_RADIUS / 64F / 2
+ RenderCircleProgress.renderCircularSlice(
+ event.context,
+ CustomRenderLayers.TRANSLUCENT_CIRCLE_GUI,
+ 0F, 1F, 0F, 1F,
+ range,
+ color = if (selectedAngle in range) 0x70A0A0A0 else 0x70FFFFFF,
+ innerCutoutRadius = cutout
+ )
+ mat.popMatrix()
+ mat.pushMatrix()
+ val centreAngle = lerpAngle(range.start, range.endInclusive, 0.5F)
+ val vec = Vector2f(cos(centreAngle), sin(centreAngle)).mul(40F)
+ mat.translate(vec.x, vec.y)
+ option.renderSlice(event.context)
+ mat.popMatrix()
+ }
+ event.context.drawLine(1, 1, delta.x.toInt(), delta.y.toInt(), Color.ofOpaque(0x00FF00))
+ mat.popMatrix()
+ }
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ val menu = activeMenu ?: return
+ if (!menu.key.isPressed(true)) {
+ val angle = atan2(delta.y, delta.x)
+
+ val choiceIndex = (wrapAngle(angle) * menu.options.size / τ).toInt()
+ val choice = menu.options[choiceIndex]
+ val selectedAny = delta.lengthSquared() > INNER_CIRCLE_RADIUS * INNER_CIRCLE_RADIUS
+ activeMenu = null
+ if (selectedAny)
+ choice.resolve()
+ }
+ }
+
+}
+
+object RadialMacros {
+ lateinit var wheels: List<MacroWheel>
+ private set
+
+ fun setWheels(wheels: List<MacroWheel>) {
+ this.wheels = wheels
+ RadialMenuViewer.activeMenu = null
+ }
+
+ @Subscribe
+ fun onOpen(event: WorldKeyboardEvent) {
+ if (RadialMenuViewer.activeMenu != null) return
+ wheels.forEach { wheel ->
+ if (event.matches(wheel.keyBinding, atLeast = true)) {
+ class R(val action: HotkeyAction) : RadialMenuOption {
+ override val isEnabled: Boolean
+ get() = true
+
+ override fun resolve() {
+ action.execute()
+ }
+
+ override fun renderSlice(drawContext: DrawContext) {
+ drawContext.drawCenteredTextWithShadow(MC.font, action.label, 0, 0, -1)
+ }
+ }
+ RadialMenuViewer.activeMenu = object : RadialMenu {
+ override val key: SavedKeyBinding
+ get() = wheel.keyBinding
+ override val options: List<RadialMenuOption> =
+ wheel.options.map { R(it) }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/mining/CommissionFeatures.kt b/src/main/kotlin/features/mining/CommissionFeatures.kt
index faba253..1041ae5 100644
--- a/src/main/kotlin/features/mining/CommissionFeatures.kt
+++ b/src/main/kotlin/features/mining/CommissionFeatures.kt
@@ -3,20 +3,22 @@ package moe.nea.firmament.features.mining
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.SlotRenderEvents
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.unformattedString
object CommissionFeatures {
- object Config : ManagedConfig("commissions", Category.MINING) {
+ @Config
+ object TConfig : ManagedConfig("commissions", Category.MINING) {
val highlightCompletedCommissions by toggle("highlight-completed") { true }
}
@Subscribe
fun onSlotRender(event: SlotRenderEvents.Before) {
- if (!Config.highlightCompletedCommissions) return
+ if (!TConfig.highlightCompletedCommissions) return
if (MC.screenName != "Commissions") return
val stack = event.slot.stack
if (stack.loreAccordingToNbt.any { it.unformattedString == "COMPLETED" }) {
diff --git a/src/main/kotlin/features/mining/Histogram.kt b/src/main/kotlin/features/mining/Histogram.kt
index ed48437..08ee893 100644
--- a/src/main/kotlin/features/mining/Histogram.kt
+++ b/src/main/kotlin/features/mining/Histogram.kt
@@ -1,7 +1,8 @@
package moe.nea.firmament.features.mining
-import java.util.*
+import java.util.NavigableMap
+import java.util.TreeMap
import kotlin.time.Duration
import moe.nea.firmament.util.TimeMark
diff --git a/src/main/kotlin/features/mining/HotmPresets.kt b/src/main/kotlin/features/mining/HotmPresets.kt
index 2241fee..aa45d82 100644
--- a/src/main/kotlin/features/mining/HotmPresets.kt
+++ b/src/main/kotlin/features/mining/HotmPresets.kt
@@ -18,7 +18,6 @@ import moe.nea.firmament.events.ChestInventoryUpdateEvent
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.ScreenChangeEvent
import moe.nea.firmament.events.SlotRenderEvents
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.MC
diff --git a/src/main/kotlin/features/mining/MiningBlockInfoUi.kt b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt
new file mode 100644
index 0000000..f4def44
--- /dev/null
+++ b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt
@@ -0,0 +1,53 @@
+package moe.nea.firmament.features.mining
+
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import net.minecraft.client.gui.screen.Screen
+import net.minecraft.item.ItemStack
+import moe.nea.firmament.repo.MiningRepoData
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.SkyBlockIsland
+
+object MiningBlockInfoUi {
+ class MiningInfo(miningData: MiningRepoData) {
+ @field:Bind("search")
+ @JvmField
+ var search = ""
+
+ @get:Bind("ores")
+ val blocks = miningData.customMiningBlocks.mapTo(ObservableList(mutableListOf())) { OreInfo(it, this) }
+ }
+
+ class OreInfo(block: MiningRepoData.CustomMiningBlock, info: MiningInfo) {
+ @get:Bind("oreName")
+ val oreName = block.name ?: "No Name"
+
+ @get:Bind("blocks")
+ val res = ObservableList(block.blocks189.map { BlockInfo(it, info) })
+ }
+
+ class BlockInfo(val block: MiningRepoData.Block189, val info: MiningInfo) {
+ @get:Bind("item")
+ val item = MoulConfigPlatform.wrap(block.block?.let { ItemStack(it) } ?: ItemStack.EMPTY)
+
+ @get:Bind("isSelected")
+ val isSelected get() = info.search.let { block.isActiveIn(SkyBlockIsland.forMode(it)) }
+
+ @get:Bind("itemName")
+ val itemName get() = item.getDisplayName()
+
+ @get:Bind("restrictions")
+ val res = ObservableList(
+ if (block.onlyIn != null)
+ block.onlyIn.map { " §r- §a${it.userFriendlyName}" }
+ else
+ listOf("Everywhere")
+ )
+ }
+
+ fun makeScreen(): Screen {
+ return MoulConfigUtils.loadScreen("mining_block_info/index", MiningInfo(RepoManager.miningData), null)
+ }
+}
diff --git a/src/main/kotlin/features/mining/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt
index 4fcf8a7..5bd85c5 100644
--- a/src/main/kotlin/features/mining/PickaxeAbility.kt
+++ b/src/main/kotlin/features/mining/PickaxeAbility.kt
@@ -1,20 +1,23 @@
package moe.nea.firmament.features.mining
import java.util.regex.Pattern
+import kotlin.jvm.optionals.getOrNull
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
+import net.minecraft.client.MinecraftClient
+import net.minecraft.client.toast.SystemToast
import net.minecraft.item.ItemStack
import net.minecraft.util.DyeColor
import net.minecraft.util.Hand
import net.minecraft.util.Identifier
+import net.minecraft.util.StringIdentifiable
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.HudRenderEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.ProfileSwitchEvent
import moe.nea.firmament.events.SlotClickEvent
+import moe.nea.firmament.events.UseItemEvent
import moe.nea.firmament.events.WorldReadyEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.DurabilityBarEvent
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
@@ -22,27 +25,66 @@ import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
import moe.nea.firmament.util.SkyBlockIsland
import moe.nea.firmament.util.TIME_PATTERN
import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.extraAttributes
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.parseShortNumber
import moe.nea.firmament.util.parseTimePattern
+import moe.nea.firmament.util.red
import moe.nea.firmament.util.render.RenderCircleProgress
import moe.nea.firmament.util.render.lerp
import moe.nea.firmament.util.skyblock.AbilityUtils
+import moe.nea.firmament.util.skyblock.DungeonUtil
+import moe.nea.firmament.util.skyblock.ItemType
import moe.nea.firmament.util.toShedaniel
+import moe.nea.firmament.util.tr
import moe.nea.firmament.util.unformattedString
import moe.nea.firmament.util.useMatch
-object PickaxeAbility : FirmamentFeature {
- override val identifier: String
+object PickaxeAbility {
+ val identifier: String
get() = "pickaxe-info"
+ enum class ShowOnTools(val label: String, val items: Set<ItemType>) : StringIdentifiable {
+ ALL("all", ItemType.DRILL, ItemType.PICKAXE, ItemType.SHOVEL, ItemType.AXE),
+ PICKAXES_AND_DRILLS("pick-and-drill", ItemType.PICKAXE, ItemType.DRILL),
+ DRILLS("drills", ItemType.DRILL),
+ ;
+ override fun asString(): String? {
+ return label
+ }
+
+ constructor(label: String, vararg items: ItemType) : this(label, items.toSet())
+
+ fun matches(type: ItemType) = items.contains(type)
+ }
+
+ @Config
object TConfig : ManagedConfig(identifier, Category.MINING) {
val cooldownEnabled by toggle("ability-cooldown") { false }
+ val disableInDungeons by toggle("disable-in-dungeons") { true }
+ val showOnTools by choice("show-on-tools") { ShowOnTools.PICKAXES_AND_DRILLS }
val cooldownScale by integer("ability-scale", 16, 64) { 16 }
+ val cooldownReadyToast by toggle("ability-cooldown-toast") { false }
val drillFuelBar by toggle("fuel-bar") { true }
+ val blockOnPrivateIsland by choice(
+ "block-on-dynamic",
+ ) {
+ BlockPickaxeAbility.ONLY_DESTRUCTIVE
+ }
+ }
+
+ enum class BlockPickaxeAbility : StringIdentifiable {
+ NEVER,
+ ALWAYS,
+ ONLY_DESTRUCTIVE;
+
+ override fun asString(): String {
+ return name
+ }
}
var lobbyJoinTime = TimeMark.farPast()
@@ -56,9 +98,8 @@ object PickaxeAbility : FirmamentFeature {
"Maniac Miner" to 59.seconds,
"Vein Seeker" to 60.seconds
)
-
- override val config: ManagedConfig
- get() = TConfig
+ val destructiveAbilities = setOf("Pickobulus")
+ val pickaxeTypes = setOf(ItemType.PICKAXE, ItemType.DRILL, ItemType.GAUNTLET)
fun getCooldownPercentage(name: String, cooldown: Duration): Double {
val sinceLastUsage = lastUsage[name]?.passedTime() ?: Duration.INFINITE
@@ -74,6 +115,30 @@ object PickaxeAbility : FirmamentFeature {
}
@Subscribe
+ fun onPickaxeRightClick(event: UseItemEvent) {
+ if (TConfig.blockOnPrivateIsland == BlockPickaxeAbility.NEVER) return
+ if (SBData.skyblockLocation != SkyBlockIsland.PRIVATE_ISLAND && SBData.skyblockLocation != SkyBlockIsland.GARDEN) return
+ val itemType = ItemType.fromItemStack(event.item)
+ if (itemType !in pickaxeTypes) return
+ val ability = AbilityUtils.getAbilities(event.item)
+ val shouldBlock = when (TConfig.blockOnPrivateIsland) {
+ BlockPickaxeAbility.NEVER -> false
+ BlockPickaxeAbility.ALWAYS -> ability.any()
+ BlockPickaxeAbility.ONLY_DESTRUCTIVE -> ability.any { it.name in destructiveAbilities }
+ }
+ if (shouldBlock) {
+ MC.sendChat(
+ tr(
+ "firmament.pickaxe.blocked",
+ "Firmament blocked a pickaxe ability from being used on a private island."
+ )
+ .red() // TODO: .clickCommand("firm confignavigate ${TConfig.identifier} block-on-dynamic")
+ )
+ event.cancel()
+ }
+ }
+
+ @Subscribe
fun onSlotClick(it: SlotClickEvent) {
if (MC.screen?.title?.unformattedString == "Heart of the Mountain") {
val name = it.stack.displayNameAccordingToNbt.unformattedString
@@ -97,9 +162,9 @@ object PickaxeAbility : FirmamentFeature {
}
} ?: return
val extra = it.item.extraAttributes
- if (!extra.contains("drill_fuel")) return
- val fuel = extra.getInt("drill_fuel")
- val percentage = fuel / maxFuel.toFloat()
+ val fuel = extra.getInt("drill_fuel").getOrNull() ?: return
+ var percentage = fuel / maxFuel.toFloat()
+ if (percentage > 1f) percentage = 1f
it.barOverride = DurabilityBarEvent.DurabilityBar(
lerp(
DyeColor.RED.toShedaniel(),
@@ -113,10 +178,31 @@ object PickaxeAbility : FirmamentFeature {
fun onChatMessage(it: ProcessChatEvent) {
abilityUsePattern.useMatch(it.unformattedString) {
lastUsage[group("name")] = TimeMark.now()
+ abilityOverride = group("name")
}
abilitySwitchPattern.useMatch(it.unformattedString) {
abilityOverride = group("ability")
}
+ pickaxeAbilityCooldownPattern.useMatch(it.unformattedString) {
+ val ability = abilityOverride ?: return@useMatch
+ val remainingCooldown = parseTimePattern(group("remainingCooldown"))
+ val length = defaultAbilityDurations[ability] ?: return@useMatch
+ lastUsage[ability] = TimeMark.ago(length - remainingCooldown)
+ }
+ nowAvailable.useMatch(it.unformattedString) {
+ val ability = group("name")
+ lastUsage[ability] = TimeMark.farPast()
+ if (!TConfig.cooldownReadyToast) return
+ val mc: MinecraftClient = MinecraftClient.getInstance()
+ mc.toastManager.add(
+ SystemToast.create(
+ mc,
+ SystemToast.Type.NARRATOR_TOGGLE,
+ tr("firmament.pickaxe.ability-ready", "Pickaxe Cooldown"),
+ tr("firmament.pickaxe.ability-ready.desc", "Pickaxe ability is ready!")
+ )
+ )
+ }
}
@Subscribe
@@ -136,6 +222,7 @@ object PickaxeAbility : FirmamentFeature {
val fuelPattern = Pattern.compile("Fuel: .*/(?<maxFuel>$SHORT_NUMBER_FORMAT)")
val pickaxeAbilityCooldownPattern =
Pattern.compile("Your pickaxe ability is on cooldown for (?<remainingCooldown>$TIME_PATTERN)\\.")
+ val nowAvailable = Pattern.compile("(?<name>[a-zA-Z0-9 ]+) is now available!")
data class PickaxeAbilityData(
val name: String,
@@ -157,21 +244,26 @@ object PickaxeAbility : FirmamentFeature {
@Subscribe
fun renderHud(event: HudRenderEvent) {
if (!TConfig.cooldownEnabled) return
+ if (TConfig.disableInDungeons && DungeonUtil.isInDungeonIsland) return
if (!event.isRenderingCursor) return
- var ability = getCooldownFromLore(MC.player?.getStackInHand(Hand.MAIN_HAND) ?: return) ?: return
- defaultAbilityDurations[ability.name] = ability.cooldown
+ val stack = MC.player?.getStackInHand(Hand.MAIN_HAND) ?: return
+ if (!TConfig.showOnTools.matches(ItemType.fromItemStack(stack) ?: ItemType.NIL))
+ return
+ var ability = getCooldownFromLore(stack)?.also { ability ->
+ defaultAbilityDurations[ability.name] = ability.cooldown
+ }
val ao = abilityOverride
- if (ao != ability.name && ao != null) {
- ability = PickaxeAbilityData(ao, defaultAbilityDurations[ao] ?: 120.seconds)
+ if (ability == null || (ao != ability.name && ao != null)) {
+ ability = PickaxeAbilityData(ao ?: return, defaultAbilityDurations[ao] ?: 120.seconds)
}
- event.context.matrices.push()
- event.context.matrices.translate(MC.window.scaledWidth / 2F, MC.window.scaledHeight / 2F, 0F)
- event.context.matrices.scale(TConfig.cooldownScale.toFloat(), TConfig.cooldownScale.toFloat(), 1F)
+ event.context.matrices.pushMatrix()
+ event.context.matrices.translate(MC.window.scaledWidth / 2F, MC.window.scaledHeight / 2F)
+ event.context.matrices.scale(TConfig.cooldownScale.toFloat(), TConfig.cooldownScale.toFloat())
RenderCircleProgress.renderCircle(
event.context, Identifier.of("firmament", "textures/gui/circle.png"),
getCooldownPercentage(ability.name, ability.cooldown).toFloat(),
0f, 1f, 0f, 1f
)
- event.context.matrices.pop()
+ event.context.matrices.popMatrix()
}
}
diff --git a/src/main/kotlin/features/mining/PristineProfitTracker.kt b/src/main/kotlin/features/mining/PristineProfitTracker.kt
index 377a470..ad864c1 100644
--- a/src/main/kotlin/features/mining/PristineProfitTracker.kt
+++ b/src/main/kotlin/features/mining/PristineProfitTracker.kt
@@ -1,26 +1,26 @@
package moe.nea.firmament.features.mining
import io.github.notenoughupdates.moulconfig.xml.Bind
-import moe.nea.jarvis.api.Point
+import org.joml.Vector2i
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
import kotlin.time.Duration.Companion.seconds
import net.minecraft.text.Text
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ProcessChatEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.gui.hud.MoulConfigHud
import moe.nea.firmament.util.BazaarPriceStrategy
import moe.nea.firmament.util.FirmFormatters.formatCommas
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.StringUtil.parseIntWithComma
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
import moe.nea.firmament.util.formattedString
import moe.nea.firmament.util.useMatch
-object PristineProfitTracker : FirmamentFeature {
- override val identifier: String
+object PristineProfitTracker {
+ val identifier: String
get() = "pristine-profit"
enum class GemstoneKind(
@@ -50,14 +50,13 @@ object PristineProfitTracker : FirmamentFeature {
var maxCollectionPerSecond: Double = 1.0,
)
+ @Config
object DConfig : ProfileSpecificDataHolder<Data>(serializer(), identifier, ::Data)
- override val config: ManagedConfig?
- get() = TConfig
-
+ @Config
object TConfig : ManagedConfig(identifier, Category.MINING) {
val timeout by duration("timeout", 0.seconds, 120.seconds) { 30.seconds }
- val gui by position("position", 100, 30) { Point(0.05, 0.2) }
+ val gui by position("position", 100, 30) { Vector2i() }
val useFineGemstones by toggle("fine-gemstones") { false }
}
@@ -114,20 +113,18 @@ object PristineProfitTracker : FirmamentFeature {
formatCommas(moneyPerSecond * SECONDS_PER_HOUR, 1))
.formattedString()
val data = DConfig.data
- if (data != null) {
- if (data.maxCollectionPerSecond < collectionPerSecond && collectionHistogram.oldestUpdate()
- .passedTime() > 30.seconds
- ) {
- data.maxCollectionPerSecond = collectionPerSecond
- DConfig.markDirty()
- }
- if (data.maxMoneyPerSecond < moneyPerSecond && moneyHistogram.oldestUpdate().passedTime() > 30.seconds) {
- data.maxMoneyPerSecond = moneyPerSecond
- DConfig.markDirty()
- }
- ProfitHud.collectionMax = maxOf(data.maxCollectionPerSecond, collectionPerSecond)
- ProfitHud.moneyMax = maxOf(data.maxMoneyPerSecond, moneyPerSecond)
+ if (data.maxCollectionPerSecond < collectionPerSecond && collectionHistogram.oldestUpdate()
+ .passedTime() > 30.seconds
+ ) {
+ data.maxCollectionPerSecond = collectionPerSecond
+ DConfig.markDirty()
+ }
+ if (data.maxMoneyPerSecond < moneyPerSecond && moneyHistogram.oldestUpdate().passedTime() > 30.seconds) {
+ data.maxMoneyPerSecond = moneyPerSecond
+ DConfig.markDirty()
}
+ ProfitHud.collectionMax = maxOf(data.maxCollectionPerSecond, collectionPerSecond)
+ ProfitHud.moneyMax = maxOf(data.maxMoneyPerSecond, moneyPerSecond)
}
diff --git a/src/main/kotlin/features/misc/CustomCapes.kt b/src/main/kotlin/features/misc/CustomCapes.kt
new file mode 100644
index 0000000..086f2fb
--- /dev/null
+++ b/src/main/kotlin/features/misc/CustomCapes.kt
@@ -0,0 +1,188 @@
+package moe.nea.firmament.features.misc
+
+import util.render.CustomRenderPipelines
+import kotlin.time.Duration
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.client.network.AbstractClientPlayerEntity
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.client.render.VertexConsumer
+import net.minecraft.client.render.VertexConsumerProvider
+import net.minecraft.client.render.entity.state.PlayerEntityRenderState
+import net.minecraft.client.util.SkinTextures
+import net.minecraft.client.util.math.MatrixStack
+import net.minecraft.util.Identifier
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.mc.CustomRenderPassHelper
+
+object CustomCapes {
+ val identifier: String
+ get() = "developer-capes"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.DEV) {
+ val showCapes by toggle("show-cape") { true }
+ }
+
+ interface CustomCapeRenderer {
+ fun replaceRender(
+ renderLayer: RenderLayer,
+ vertexConsumerProvider: VertexConsumerProvider,
+ matrixStack: MatrixStack,
+ model: (VertexConsumer) -> Unit
+ )
+ }
+
+ data class TexturedCapeRenderer(
+ val location: Identifier
+ ) : CustomCapeRenderer {
+ override fun replaceRender(
+ renderLayer: RenderLayer,
+ vertexConsumerProvider: VertexConsumerProvider,
+ matrixStack: MatrixStack,
+ model: (VertexConsumer) -> Unit
+ ) {
+ model(vertexConsumerProvider.getBuffer(RenderLayer.getEntitySolid(location)))
+ }
+ }
+
+ data class ParallaxedHighlightCapeRenderer(
+ val template: Identifier,
+ val background: Identifier,
+ val overlay: Identifier,
+ val animationSpeed: Duration,
+ ) : CustomCapeRenderer {
+ override fun replaceRender(
+ renderLayer: RenderLayer,
+ vertexConsumerProvider: VertexConsumerProvider,
+ matrixStack: MatrixStack,
+ model: (VertexConsumer) -> Unit
+ ) {
+ val animationValue = (startTime.passedTime() / animationSpeed).mod(1F)
+ CustomRenderPassHelper(
+ { "Firmament Cape Renderer" },
+ renderLayer.drawMode,
+ renderLayer.vertexFormat,
+ MC.instance.framebuffer,
+ true,
+ ).use { renderPass ->
+ renderPass.setPipeline(CustomRenderPipelines.PARALLAX_CAPE_SHADER)
+ renderPass.setAllDefaultUniforms()
+ renderPass.setUniform("Animation", 4) {
+ it.putFloat(animationValue.toFloat())
+ }
+ renderPass.bindSampler("Sampler0", template)
+ renderPass.bindSampler("Sampler1", background)
+ renderPass.bindSampler("Sampler3", overlay)
+ renderPass.uploadVertices(2048, model)
+ renderPass.draw()
+ }
+ }
+ }
+
+ interface CapeStorage {
+ companion object {
+ @JvmStatic
+ fun cast(playerEntityRenderState: PlayerEntityRenderState) =
+ playerEntityRenderState as CapeStorage
+
+ }
+
+ var cape_firmament: CustomCape?
+ }
+
+ data class CustomCape(
+ val id: String,
+ val label: String,
+ val render: CustomCapeRenderer,
+ )
+
+ enum class AllCapes(val label: String, val render: CustomCapeRenderer) {
+ FIRMAMENT_ANIMATED(
+ "Animated Firmament", ParallaxedHighlightCapeRenderer(
+ Firmament.identifier("textures/cape/parallax_template.png"),
+ Firmament.identifier("textures/cape/parallax_background.png"),
+ Firmament.identifier("textures/cape/firmament_star.png"),
+ 110.seconds
+ )
+ ),
+ UNPLEASANT_GRADIENT(
+ "unpleasant_gradient",
+ TexturedCapeRenderer(Firmament.identifier("textures/cape/unpleasant_gradient.png"))
+ ),
+ FURFSKY_STATIC(
+ "FurfSky",
+ TexturedCapeRenderer(Firmament.identifier("textures/cape/fsr_static.png"))
+ ),
+
+ FIRMAMENT_STATIC(
+ "Firmament",
+ TexturedCapeRenderer(Firmament.identifier("textures/cape/firm_static.png"))
+ ),
+ HYPIXEL_PLUS(
+ "Hypixel+",
+ TexturedCapeRenderer(Firmament.identifier("textures/cape/h_plus.png"))
+ ),
+ ;
+
+ val cape = CustomCape(name, label, render)
+ }
+
+ val byId = AllCapes.entries.associateBy { it.cape.id }
+ val byUuid =
+ listOf(
+ listOf(
+ Devs.nea to AllCapes.UNPLEASANT_GRADIENT,
+ Devs.kath to AllCapes.FIRMAMENT_STATIC,
+ Devs.jani to AllCapes.FIRMAMENT_ANIMATED,
+ Devs.HPlus.ic22487 to AllCapes.HYPIXEL_PLUS,
+ ),
+ Devs.FurfSky.all.map { it to AllCapes.FURFSKY_STATIC },
+ ).flatten().flatMap { (dev, cape) -> dev.uuids.map { it to cape.cape } }.toMap()
+
+ @JvmStatic
+ fun render(
+ playerEntityRenderState: PlayerEntityRenderState,
+ vertexConsumer: VertexConsumer,
+ renderLayer: RenderLayer,
+ vertexConsumerProvider: VertexConsumerProvider,
+ matrixStack: MatrixStack,
+ model: (VertexConsumer) -> Unit
+ ) {
+ val capeStorage = CapeStorage.cast(playerEntityRenderState)
+ val firmCape = capeStorage.cape_firmament
+ if (firmCape != null) {
+ firmCape.render.replaceRender(renderLayer, vertexConsumerProvider, matrixStack, model)
+ } else {
+ model(vertexConsumer)
+ }
+ }
+
+ @JvmStatic
+ fun addCapeData(
+ player: AbstractClientPlayerEntity,
+ playerEntityRenderState: PlayerEntityRenderState
+ ) {
+ val cape = if (TConfig.showCapes) byUuid[player.uuid] else null
+ val capeStorage = CapeStorage.cast(playerEntityRenderState)
+ if (cape == null) {
+ capeStorage.cape_firmament = null
+ } else {
+ capeStorage.cape_firmament = cape
+ playerEntityRenderState.skinTextures = SkinTextures(
+ playerEntityRenderState.skinTextures.texture,
+ playerEntityRenderState.skinTextures.textureUrl,
+ Firmament.identifier("placeholder/fake_cape"),
+ playerEntityRenderState.skinTextures.elytraTexture,
+ playerEntityRenderState.skinTextures.model,
+ playerEntityRenderState.skinTextures.secure,
+ )
+ playerEntityRenderState.capeVisible = true
+ }
+ }
+
+ val startTime = TimeMark.now()
+}
diff --git a/src/main/kotlin/features/misc/Devs.kt b/src/main/kotlin/features/misc/Devs.kt
new file mode 100644
index 0000000..91095c0
--- /dev/null
+++ b/src/main/kotlin/features/misc/Devs.kt
@@ -0,0 +1,41 @@
+package moe.nea.firmament.features.misc
+
+import java.util.UUID
+
+object Devs {
+ data class Dev(
+ val uuids: List<UUID>,
+ ) {
+ constructor(vararg uuid: UUID) : this(uuid.toList())
+ constructor(vararg uuid: String) : this(uuid.map { UUID.fromString(it) })
+ }
+
+ val nea = Dev("d3cb85e2-3075-48a1-b213-a9bfb62360c1", "842204e6-6880-487b-ae5a-0595394f9948")
+ val kath = Dev("add71246-c46e-455c-8345-c129ea6f146c", "b491990d-53fd-4c5f-a61e-19d58cc7eddf")
+ val jani = Dev("8a9f1841-48e9-48ed-b14f-76a124e6c9df")
+
+ object FurfSky {
+ val smolegit = Dev("02b38b96-eb19-405a-b319-d6bc00b26ab3")
+ val itsCen = Dev("ada70b5a-ac37-49d2-b18c-1351672f8051")
+ val webster = Dev("02166f1b-9e8d-4e48-9e18-ea7a4499492d")
+ val vrachel = Dev("22e98637-ba97-4b6b-a84f-fb57a461ce43")
+ val cunuduh = Dev("2a15e3b3-c46e-4718-b907-166e1ab2efdc")
+ val eiiies = Dev("2ae162f2-81a7-4f91-a4b2-104e78a0a7e1")
+ val june = Dev("2584a4e3-f917-4493-8ced-618391f3b44f")
+ val denasu = Dev("313cbd25-8ade-4e41-845c-5cab555a30c9")
+ val libyKiwii = Dev("4265c52e-bd6f-4d3c-9cf6-bdfc8fb58023")
+ val madeleaan = Dev("bcb119a3-6000-4324-bda1-744f00c44b31")
+ val turtleSP = Dev("f1ca1934-a582-4723-8283-89921d008657")
+ val papayamm = Dev("ae0eea30-f6a2-40fe-ac17-9c80b3423409")
+ val persuasiveViksy = Dev("ba7ac144-28e0-4f55-a108-1a72fe744c9e")
+ val all = listOf(
+ smolegit, itsCen, webster, vrachel, cunuduh, eiiies,
+ june, denasu, libyKiwii, madeleaan, turtleSP, papayamm,
+ persuasiveViksy
+ )
+ }
+ object HPlus {
+ val ic22487 = Dev("ab2be3b2-bb75-4aaa-892d-9fff5a7e3953")
+ }
+
+}
diff --git a/src/main/kotlin/features/misc/Hud.kt b/src/main/kotlin/features/misc/Hud.kt
new file mode 100644
index 0000000..fb7c6cd
--- /dev/null
+++ b/src/main/kotlin/features/misc/Hud.kt
@@ -0,0 +1,75 @@
+package moe.nea.firmament.features.misc
+
+import org.joml.Vector2i
+import net.minecraft.client.network.PlayerListEntry
+import net.minecraft.text.Text
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.tr
+
+object Hud {
+ val identifier: String
+ get() = "hud"
+
+ @Config
+ object TConfig : ManagedConfig(identifier, Category.MISC) {
+ var dayCount by toggle("day-count") { false }
+ val dayCountHud by position("day-count-hud", 80, 10) { Vector2i() }
+ var fpsCount by toggle("fps-count") { false }
+ val fpsCountHud by position("fps-count-hud", 80, 10) { Vector2i() }
+ var pingCount by toggle("ping-count") { false }
+ val pingCountHud by position("ping-count-hud", 80, 10) { Vector2i() }
+ }
+
+ @Subscribe
+ fun onRenderHud(it: HudRenderEvent) {
+ if (TConfig.dayCount) {
+ it.context.matrices.pushMatrix()
+ TConfig.dayCountHud.applyTransformations(it.context.matrices)
+ val day = (MC.world?.timeOfDay ?: 0L) / 24000
+ it.context.drawText(
+ MC.font,
+ Text.literal(String.format(tr("firmament.config.hud.day-count-hud.display", "Day: %s").string, day)),
+ 36,
+ MC.font.fontHeight,
+ -1,
+ true
+ )
+ it.context.matrices.popMatrix()
+ }
+
+ if (TConfig.fpsCount) {
+ it.context.matrices.pushMatrix()
+ TConfig.fpsCountHud.applyTransformations(it.context.matrices)
+ it.context.drawText(
+ MC.font, Text.literal(
+ String.format(
+ tr("firmament.config.hud.fps-count-hud.display", "FPS: %s").string, MC.instance.currentFps
+ )
+ ), 36, MC.font.fontHeight, -1, true
+ )
+ it.context.matrices.popMatrix()
+ }
+
+ if (TConfig.pingCount) {
+ it.context.matrices.pushMatrix()
+ TConfig.pingCountHud.applyTransformations(it.context.matrices)
+ val ping = MC.player?.let {
+ val entry: PlayerListEntry? = MC.networkHandler?.getPlayerListEntry(it.uuid)
+ entry?.latency ?: -1
+ } ?: -1
+ it.context.drawText(
+ MC.font, Text.literal(
+ String.format(
+ tr("firmament.config.hud.ping-count-hud.display", "Ping: %s ms").string, ping
+ )
+ ), 36, MC.font.fontHeight, -1, true
+ )
+
+ it.context.matrices.popMatrix()
+ }
+ }
+}
diff --git a/src/main/kotlin/features/misc/LicenseViewer.kt b/src/main/kotlin/features/misc/LicenseViewer.kt
new file mode 100644
index 0000000..4219177
--- /dev/null
+++ b/src/main/kotlin/features/misc/LicenseViewer.kt
@@ -0,0 +1,128 @@
+package moe.nea.firmament.features.misc
+
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.xml.Bind
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import kotlinx.serialization.json.decodeFromStream
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.ScreenUtil
+import moe.nea.firmament.util.tr
+
+object LicenseViewer {
+ @Serializable
+ data class Software(
+ val licenses: List<License> = listOf(),
+ val webPresence: String? = null,
+ val projectName: String,
+ val projectDescription: String? = null,
+ val developers: List<Developer> = listOf(),
+ ) {
+
+ @Bind
+ fun hasWebPresence() = webPresence != null
+
+ @Bind
+ fun webPresence() = webPresence ?: "<no web presence>"
+ @Bind
+ fun open() {
+ MC.openUrl(webPresence ?: return)
+ }
+
+ @Bind
+ fun projectName() = projectName
+
+ @Bind
+ fun projectDescription() = projectDescription ?: "<no project description>"
+
+ @get:Bind("developers")
+ @Transient
+ val developersO = ObservableList(developers)
+
+ @get:Bind("licenses")
+ @Transient
+ val licenses0 = ObservableList(licenses)
+ }
+
+ @Serializable
+ data class Developer(
+ @get:Bind("name") val name: String,
+ val webPresence: String? = null
+ ) {
+
+ @Bind
+ fun open() {
+ MC.openUrl(webPresence ?: return)
+ }
+
+ @Bind
+ fun hasWebPresence() = webPresence != null
+
+ @Bind
+ fun webPresence() = webPresence ?: "<no web presence>"
+ }
+
+ @Serializable
+ data class License(
+ @get:Bind("name") val licenseName: String,
+ val licenseUrl: String? = null
+ ) {
+ @Bind
+ fun open() {
+ MC.openUrl(licenseUrl ?: return)
+ }
+
+ @Bind
+ fun hasUrl() = licenseUrl != null
+
+ @Bind
+ fun url() = licenseUrl ?: "<no link to license text>"
+ }
+
+ data class LicenseList(
+ val softwares: List<Software>
+ ) {
+ @get:Bind("softwares")
+ val obs = ObservableList(softwares)
+ }
+
+ @OptIn(ExperimentalSerializationApi::class)
+ val licenses: LicenseList? = ErrorUtil.catch("Could not load licenses") {
+ Firmament.json.decodeFromStream<List<Software>?>(
+ javaClass.getResourceAsStream("/LICENSES-FIRMAMENT.json") ?: error("Could not find LICENSES-FIRMAMENT.json")
+ )?.let { LicenseList(it) }
+ }.orNull()
+
+ fun showLicenses() {
+ ErrorUtil.catch("Could not display licenses") {
+ ScreenUtil.setScreenLater(
+ MoulConfigUtils.loadScreen(
+ "license_viewer/index", licenses!!, null
+ )
+ )
+ }.or {
+ MC.sendChat(
+ tr(
+ "firmament.licenses.notfound",
+ "Could not load licenses. Please check the Firmament source code for information directly."
+ )
+ )
+ }
+ }
+
+ @Subscribe
+ fun onSubcommand(event: CommandEvent.SubCommand) {
+ event.subcommand("licenses") {
+ thenExecute {
+ showLicenses()
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/misc/ModAnnouncer.kt b/src/main/kotlin/features/misc/ModAnnouncer.kt
new file mode 100644
index 0000000..1047353
--- /dev/null
+++ b/src/main/kotlin/features/misc/ModAnnouncer.kt
@@ -0,0 +1,80 @@
+package moe.nea.firmament.features.misc
+
+import io.netty.buffer.ByteBuf
+import net.fabricmc.fabric.api.networking.v1.PacketByteBufs
+import net.fabricmc.fabric.api.networking.v1.PayloadTypeRegistry
+import net.fabricmc.loader.api.FabricLoader
+import net.minecraft.network.codec.PacketCodec
+import net.minecraft.network.codec.PacketCodecs
+import net.minecraft.network.packet.CustomPayload
+import net.minecraft.network.packet.c2s.common.CustomPayloadC2SPacket
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.JoinServerEvent
+
+/**
+ * This is a class that announces all mods to hypixel (or any other server you connect to).
+ *
+ * I don't mind cheat mods, i just think they should be honest about themselves.
+ *
+ * If you are a cheat mod (or think you could possibly be seen as one), you have two options:
+ *
+ * - risk getting your users banned from hypixel by exposing your modid + version number in a packet sent on first connect
+ * - hide yourself from this list by either a mixin, or (more easily) by specifying a tag in your fabric.mod.json:
+ *
+ * ```json
+ * {
+ * "schemaVersion": 1,
+ * "id": "my-cheat-mod",
+ * "custom": { "firmament:hide_from_modlist": true }
+ * }
+ * ```
+ */
+object ModAnnouncer {
+
+ data class ModEntry(
+ val modid: String,
+ val modVersion: String,
+ ) {
+ companion object {
+ val CODEC: PacketCodec<ByteBuf, ModEntry> = PacketCodec.tuple(
+ PacketCodecs.STRING, ModEntry::modid,
+ PacketCodecs.STRING, ModEntry::modVersion,
+ ::ModEntry
+ )
+ }
+ }
+
+ data class ModPacket(
+ val mods: List<ModEntry>,
+ ) : CustomPayload {
+ override fun getId(): CustomPayload.Id<out ModPacket> {
+ return ID
+ }
+
+ companion object {
+ val ID = CustomPayload.Id<ModPacket>(Firmament.identifier("mod_list"))
+ val CODEC: PacketCodec<ByteBuf, ModPacket> = ModEntry.CODEC.collect(PacketCodecs.toList())
+ .xmap(::ModPacket, ModPacket::mods)
+ }
+ }
+
+ @Subscribe
+ fun onServerJoin(event: JoinServerEvent) {
+ val packet = ModPacket(
+ FabricLoader.getInstance()
+ .allMods
+ .filter { !it.metadata.containsCustomValue("firmament:hide_from_modlist") }
+ .map { ModEntry(it.metadata.id, it.metadata.version.friendlyString) })
+ val pbb = PacketByteBufs.create()
+ ModPacket.CODEC.encode(pbb, packet)
+ if (pbb.writerIndex() > CustomPayloadC2SPacket.MAX_PAYLOAD_SIZE)
+ return
+
+ event.networkHandler.sendPacket(event.packetSender.createPacket(packet))
+ }
+
+ init {
+ PayloadTypeRegistry.playC2S().register(ModPacket.ID, ModPacket.CODEC)
+ }
+}
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<Timer>()
+
+ @Subscribe
+ fun tick(event: TickEvent) {
+ timers.removeAll {
+ if (it.isDone()) {
+ MC.sendChat(tr("firmament.timer.finished",
+ "The timer you set ${FirmFormatters.formatTimespan(it.duration)} ago just went off: ${it.message}")
+ .yellow())
+ Firmament.coroutineScope.launch {
+ withContext(MinecraftDispatcher) {
+ repeat(5) {
+ CommonSoundEffects.playSuccess()
+ delay(0.2.seconds)
+ }
+ }
+ }
+ true
+ } else {
+ false
+ }
+ }
+ }
+
+ fun startTimer(duration: Duration, message: String) {
+ val timerId = createTimerId++
+ timers.add(Timer(TimeMark.now(), duration, message, timerId))
+ MC.sendChat(
+ tr("firmament.timer.start",
+ "Timer started for $message in ${FirmFormatters.formatTimespan(duration)}.").lime()
+ .append(" ")
+ .append(
+ tr("firmament.timer.cancelbutton",
+ "Click here to cancel the timer."
+ ).clickCommand("/firm timer clear $timerId").red()
+ )
+ )
+ }
+
+ fun clearTimer(timerId: Int) {
+ val timer = timers.indexOfFirst { it.timerId == timerId }
+ if (timer < 0) {
+ MC.sendChat(tr("firmament.timer.cancel.fail",
+ "Could not cancel that timer. Maybe it was already cancelled?").red())
+ } else {
+ val timerData = timers[timer]
+ timers.removeAt(timer)
+ MC.sendChat(tr("firmament.timer.cancel.done",
+ "Cancelled timer ${timerData.message}. It would have been done in ${
+ FirmFormatters.formatTimespan(timerData.timeLeft())
+ }.").lime())
+ }
+ }
+
+ var createTimerId = 0
+
+ @Subscribe
+ fun onCommands(event: CommandEvent.SubCommand) {
+ event.subcommand("cleartimer") {
+ thenArgument("timerId", IntegerArgumentType.integer(0)) { timerId ->
+ thenExecute {
+ clearTimer(this[timerId])
+ }
+ }
+ thenExecute {
+ timers.map { it.timerId }.forEach {
+ clearTimer(it)
+ }
+ }
+ }
+ event.subcommand("timer") {
+ thenArgument("time", DurationArgumentType) { duration ->
+ thenExecute {
+ startTimer(this[duration], "no message")
+ }
+ thenArgument("message", RestArgumentType) { message ->
+ thenExecute {
+ startTimer(this[duration], this[message])
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/notifications/Notifications.kt b/src/main/kotlin/features/notifications/Notifications.kt
deleted file mode 100644
index 8d912d1..0000000
--- a/src/main/kotlin/features/notifications/Notifications.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-
-package moe.nea.firmament.features.notifications
-
-import moe.nea.firmament.features.FirmamentFeature
-
-object Notifications {
-}
diff --git a/src/main/kotlin/features/texturepack/BakedModelExtra.kt b/src/main/kotlin/features/texturepack/BakedModelExtra.kt
deleted file mode 100644
index 6305748..0000000
--- a/src/main/kotlin/features/texturepack/BakedModelExtra.kt
+++ /dev/null
@@ -1,30 +0,0 @@
-package moe.nea.firmament.features.texturepack
-
-import net.fabricmc.fabric.api.renderer.v1.model.WrapperBakedModel as WrapperBakedModelFabric
-import net.minecraft.client.render.model.BakedModel
-import net.minecraft.client.render.model.WrapperBakedModel
-import moe.nea.firmament.util.ErrorUtil
-
-interface BakedModelExtra {
- companion object {
- @JvmStatic
- fun cast(originalModel: BakedModel): BakedModelExtra? {
- var p = originalModel
- for (i in 0..256) {
- p = when (p) {
- is BakedModelExtra -> return p
- is WrapperBakedModel -> p.wrapped
- is WrapperBakedModelFabric -> WrapperBakedModelFabric.unwrap(p)
- else -> break
- }
- }
- ErrorUtil.softError("Could not find a baked model for $originalModel")
- return null
- }
- }
-
- var tintOverrides_firmament: TintOverrides?
-
- fun getHeadModel_firmament(): BakedModel?
- fun setHeadModel_firmament(headModel: BakedModel?)
-}
diff --git a/src/main/kotlin/features/texturepack/BakedOverrideData.kt b/src/main/kotlin/features/texturepack/BakedOverrideData.kt
deleted file mode 100644
index e9391f1..0000000
--- a/src/main/kotlin/features/texturepack/BakedOverrideData.kt
+++ /dev/null
@@ -1,14 +0,0 @@
-
-package moe.nea.firmament.features.texturepack
-
-import net.minecraft.client.render.model.json.ModelOverrideList
-
-interface BakedOverrideData {
- fun getFirmamentOverrides(): Array<FirmamentModelPredicate>?
- fun setFirmamentOverrides(overrides: Array<FirmamentModelPredicate>?)
- companion object{
- @Suppress("CAST_NEVER_SUCCEEDS")
- @JvmStatic
- fun cast(bakedOverride: ModelOverrideList.BakedOverride): BakedOverrideData = bakedOverride as BakedOverrideData
- }
-}
diff --git a/src/main/kotlin/features/texturepack/CustomBlockTextures.kt b/src/main/kotlin/features/texturepack/CustomBlockTextures.kt
deleted file mode 100644
index 2f7f084..0000000
--- a/src/main/kotlin/features/texturepack/CustomBlockTextures.kt
+++ /dev/null
@@ -1,301 +0,0 @@
-@file:UseSerializers(BlockPosSerializer::class, IdentifierSerializer::class)
-
-package moe.nea.firmament.features.texturepack
-
-import java.util.concurrent.CompletableFuture
-import net.fabricmc.loader.api.FabricLoader
-import kotlinx.serialization.ExperimentalSerializationApi
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.Transient
-import kotlinx.serialization.UseSerializers
-import kotlinx.serialization.descriptors.SerialDescriptor
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
-import kotlinx.serialization.json.JsonDecoder
-import kotlinx.serialization.json.JsonElement
-import kotlinx.serialization.json.JsonPrimitive
-import kotlinx.serialization.serializer
-import kotlin.jvm.optionals.getOrNull
-import net.minecraft.block.Block
-import net.minecraft.block.BlockState
-import net.minecraft.client.render.model.BakedModel
-import net.minecraft.client.util.ModelIdentifier
-import net.minecraft.registry.RegistryKey
-import net.minecraft.registry.RegistryKeys
-import net.minecraft.resource.ResourceManager
-import net.minecraft.resource.SinglePreparationResourceReloader
-import net.minecraft.util.Identifier
-import net.minecraft.util.math.BlockPos
-import net.minecraft.util.profiler.Profiler
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.annotations.Subscribe
-import moe.nea.firmament.events.BakeExtraModelsEvent
-import moe.nea.firmament.events.EarlyResourceReloadEvent
-import moe.nea.firmament.events.FinalizeResourceManagerEvent
-import moe.nea.firmament.events.SkyblockServerUpdateEvent
-import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger
-import moe.nea.firmament.util.IdentifierSerializer
-import moe.nea.firmament.util.MC
-import moe.nea.firmament.util.SBData
-import moe.nea.firmament.util.SkyBlockIsland
-import moe.nea.firmament.util.json.BlockPosSerializer
-import moe.nea.firmament.util.json.SingletonSerializableList
-
-
-object CustomBlockTextures {
- @Serializable
- data class CustomBlockOverride(
- val modes: @Serializable(SingletonSerializableList::class) List<String>,
- val area: List<Area>? = null,
- val replacements: Map<Identifier, Replacement>,
- )
-
- @Serializable(with = Replacement.Serializer::class)
- data class Replacement(
- val block: Identifier,
- val sound: Identifier?,
- ) {
-
- @Transient
- val blockModelIdentifier get() = ModelIdentifier(block.withPrefixedPath("block/"), "firmament")
-
- @Transient
- val bakedModel: BakedModel by lazy(LazyThreadSafetyMode.NONE) {
- MC.instance.bakedModelManager.getModel(blockModelIdentifier)
- }
-
- @OptIn(ExperimentalSerializationApi::class)
- @kotlinx.serialization.Serializer(Replacement::class)
- object DefaultSerializer : KSerializer<Replacement>
-
- object Serializer : KSerializer<Replacement> {
- val delegate = serializer<JsonElement>()
- override val descriptor: SerialDescriptor
- get() = delegate.descriptor
-
- override fun deserialize(decoder: Decoder): Replacement {
- val jsonElement = decoder.decodeSerializableValue(delegate)
- if (jsonElement is JsonPrimitive) {
- require(jsonElement.isString)
- return Replacement(Identifier.tryParse(jsonElement.content)!!, null)
- }
- return (decoder as JsonDecoder).json.decodeFromJsonElement(DefaultSerializer, jsonElement)
- }
-
- override fun serialize(encoder: Encoder, value: Replacement) {
- encoder.encodeSerializableValue(DefaultSerializer, value)
- }
- }
- }
-
- @Serializable
- data class Area(
- val min: BlockPos,
- val max: BlockPos,
- ) {
- @Transient
- val realMin = BlockPos(
- minOf(min.x, max.x),
- minOf(min.y, max.y),
- minOf(min.z, max.z),
- )
-
- @Transient
- val realMax = BlockPos(
- maxOf(min.x, max.x),
- maxOf(min.y, max.y),
- maxOf(min.z, max.z),
- )
-
- fun roughJoin(other: Area): Area {
- return Area(
- BlockPos(
- minOf(realMin.x, other.realMin.x),
- minOf(realMin.y, other.realMin.y),
- minOf(realMin.z, other.realMin.z),
- ),
- BlockPos(
- maxOf(realMax.x, other.realMax.x),
- maxOf(realMax.y, other.realMax.y),
- maxOf(realMax.z, other.realMax.z),
- )
- )
- }
-
- fun contains(blockPos: BlockPos): Boolean {
- return (blockPos.x in realMin.x..realMax.x) &&
- (blockPos.y in realMin.y..realMax.y) &&
- (blockPos.z in realMin.z..realMax.z)
- }
- }
-
- data class LocationReplacements(
- val lookup: Map<Block, List<BlockReplacement>>
- )
-
- data class BlockReplacement(
- val checks: List<Area>?,
- val replacement: Replacement,
- ) {
- val roughCheck by lazy(LazyThreadSafetyMode.NONE) {
- if (checks == null || checks.size < 3) return@lazy null
- checks.reduce { acc, next -> acc.roughJoin(next) }
- }
- }
-
- data class BakedReplacements(val data: Map<SkyBlockIsland, LocationReplacements>)
-
- var allLocationReplacements: BakedReplacements = BakedReplacements(mapOf())
- var currentIslandReplacements: LocationReplacements? = null
-
- fun refreshReplacements() {
- val location = SBData.skyblockLocation
- val replacements =
- if (CustomSkyBlockTextures.TConfig.enableBlockOverrides) location?.let(allLocationReplacements.data::get)
- else null
- val lastReplacements = currentIslandReplacements
- currentIslandReplacements = replacements
- if (lastReplacements != replacements) {
- MC.nextTick {
- MC.worldRenderer.chunks?.chunks?.forEach {
- // false schedules rebuilds outside a 27 block radius to happen async
- it.scheduleRebuild(false)
- }
- sodiumReloadTask?.run()
- }
- }
- }
-
- private val sodiumReloadTask = runCatching {
- val r = Class.forName("moe.nea.firmament.compat.sodium.SodiumChunkReloader")
- .getConstructor()
- .newInstance() as Runnable
- r.run()
- r
- }.getOrElse {
- if (FabricLoader.getInstance().isModLoaded("sodium"))
- logger.error("Could not create sodium chunk reloader")
- null
- }
-
-
- fun matchesPosition(replacement: BlockReplacement, blockPos: BlockPos?): Boolean {
- if (blockPos == null) return true
- val rc = replacement.roughCheck
- if (rc != null && !rc.contains(blockPos)) return false
- val areas = replacement.checks
- if (areas != null && !areas.any { it.contains(blockPos) }) return false
- return true
- }
-
- @JvmStatic
- fun getReplacementModel(block: BlockState, blockPos: BlockPos?): BakedModel? {
- return getReplacement(block, blockPos)?.bakedModel
- }
-
- @JvmStatic
- fun getReplacement(block: BlockState, blockPos: BlockPos?): Replacement? {
- if (isInFallback() && blockPos == null) {
- return null
- }
- val replacements = currentIslandReplacements?.lookup?.get(block.block) ?: return null
- for (replacement in replacements) {
- if (replacement.checks == null || matchesPosition(replacement, blockPos))
- return replacement.replacement
- }
- return null
- }
-
-
- @Subscribe
- fun onLocation(event: SkyblockServerUpdateEvent) {
- refreshReplacements()
- }
-
- @Volatile
- var preparationFuture: CompletableFuture<BakedReplacements> = CompletableFuture.completedFuture(BakedReplacements(
- mapOf()))
-
- val insideFallbackCall = ThreadLocal.withInitial { 0 }
-
- @JvmStatic
- fun enterFallbackCall() {
- insideFallbackCall.set(insideFallbackCall.get() + 1)
- }
-
- fun isInFallback() = insideFallbackCall.get() > 0
-
- @JvmStatic
- fun exitFallbackCall() {
- insideFallbackCall.set(insideFallbackCall.get() - 1)
- }
-
- @Subscribe
- fun onEarlyReload(event: EarlyResourceReloadEvent) {
- preparationFuture = CompletableFuture
- .supplyAsync(
- { prepare(event.resourceManager) }, event.preparationExecutor)
- }
-
- @Subscribe
- fun bakeExtraModels(event: BakeExtraModelsEvent) {
- preparationFuture.join().data.values
- .flatMap { it.lookup.values }
- .flatten()
- .mapTo(mutableSetOf()) { it.replacement.blockModelIdentifier }
- .forEach { event.addNonItemModel(it, it.id) }
- }
-
- private fun prepare(manager: ResourceManager): BakedReplacements {
- val resources = manager.findResources("overrides/blocks") {
- it.namespace == "firmskyblock" && it.path.endsWith(".json")
- }
- val map = mutableMapOf<SkyBlockIsland, MutableMap<Block, MutableList<BlockReplacement>>>()
- for ((file, resource) in resources) {
- val json =
- Firmament.tryDecodeJsonFromStream<CustomBlockOverride>(resource.inputStream)
- .getOrElse { ex ->
- logger.error("Failed to load block texture override at $file", ex)
- continue
- }
- for (mode in json.modes) {
- val island = SkyBlockIsland.forMode(mode)
- val islandMpa = map.getOrPut(island, ::mutableMapOf)
- for ((blockId, replacement) in json.replacements) {
- val block = MC.defaultRegistries.getOrThrow(RegistryKeys.BLOCK)
- .getOptional(RegistryKey.of(RegistryKeys.BLOCK, blockId))
- .getOrNull()
- if (block == null) {
- logger.error("Failed to load block texture override at ${file}: unknown block '$blockId'")
- continue
- }
- val replacements = islandMpa.getOrPut(block.value(), ::mutableListOf)
- replacements.add(BlockReplacement(json.area, replacement))
- }
- }
- }
-
- return BakedReplacements(map.mapValues { LocationReplacements(it.value) })
- }
-
- @JvmStatic
- fun patchIndigo(orig: BakedModel, pos: BlockPos, state: BlockState): BakedModel {
- return getReplacementModel(state, pos) ?: orig
- }
-
- @Subscribe
- fun onStart(event: FinalizeResourceManagerEvent) {
- event.resourceManager.registerReloader(object :
- SinglePreparationResourceReloader<BakedReplacements>() {
- override fun prepare(manager: ResourceManager, profiler: Profiler): BakedReplacements {
- return preparationFuture.join()
- }
-
- override fun apply(prepared: BakedReplacements, manager: ResourceManager, profiler: Profiler?) {
- allLocationReplacements = prepared
- refreshReplacements()
- }
- })
- }
-}
diff --git a/src/main/kotlin/features/texturepack/CustomGlobalArmorOverrides.kt b/src/main/kotlin/features/texturepack/CustomGlobalArmorOverrides.kt
deleted file mode 100644
index 54e9e11..0000000
--- a/src/main/kotlin/features/texturepack/CustomGlobalArmorOverrides.kt
+++ /dev/null
@@ -1,155 +0,0 @@
-@file:UseSerializers(IdentifierSerializer::class)
-
-package moe.nea.firmament.features.texturepack
-
-import java.util.Optional
-import java.util.concurrent.atomic.AtomicInteger
-import kotlinx.serialization.SerialName
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.Transient
-import kotlinx.serialization.UseSerializers
-import net.minecraft.item.ItemStack
-import net.minecraft.item.equipment.EquipmentModel
-import net.minecraft.resource.ResourceManager
-import net.minecraft.resource.SinglePreparationResourceReloader
-import net.minecraft.util.Identifier
-import net.minecraft.util.profiler.Profiler
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.annotations.Subscribe
-import moe.nea.firmament.events.FinalizeResourceManagerEvent
-import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger
-import moe.nea.firmament.util.IdentifierSerializer
-import moe.nea.firmament.util.collections.WeakCache
-import moe.nea.firmament.util.skyBlockId
-
-object CustomGlobalArmorOverrides {
- @Serializable
- data class ArmorOverride(
- @SerialName("item_ids")
- val itemIds: List<String>,
- val layers: List<ArmorOverrideLayer>? = null,
- val model: Identifier? = null,
- val overrides: List<ArmorOverrideOverride> = listOf(),
- ) {
- @Transient
- lateinit var modelIdentifier: Identifier
- fun bake() {
- modelIdentifier = bakeModel(model, layers)
- overrides.forEach { it.bake() }
- }
-
- init {
- require(layers != null || model != null) { "Either model or layers must be specified for armor override" }
- require(layers == null || model == null) { "Can't specify both model and layers for armor override" }
- }
- }
-
- @Serializable
- data class ArmorOverrideLayer(
- val tint: Boolean = false,
- val identifier: Identifier,
- val suffix: String = "",
- )
-
- @Serializable
- data class ArmorOverrideOverride(
- val predicate: FirmamentModelPredicate,
- val layers: List<ArmorOverrideLayer>? = null,
- val model: Identifier? = null,
- ) {
- init {
- require(layers != null || model != null) { "Either model or layers must be specified for armor override override" }
- require(layers == null || model == null) { "Can't specify both model and layers for armor override override" }
- }
-
- @Transient
- lateinit var modelIdentifier: Identifier
- fun bake() {
- modelIdentifier = bakeModel(model, layers)
- }
- }
-
-
- val overrideCache = WeakCache.memoize<ItemStack, Optional<Identifier>>("ArmorOverrides") { stack ->
- val id = stack.skyBlockId ?: return@memoize Optional.empty()
- val override = overrides[id.neuItem] ?: return@memoize Optional.empty()
- for (suboverride in override.overrides) {
- if (suboverride.predicate.test(stack)) {
- return@memoize Optional.of(suboverride.modelIdentifier)
- }
- }
- return@memoize Optional.of(override.modelIdentifier)
- }
-
- var overrides: Map<String, ArmorOverride> = mapOf()
- private var bakedOverrides: MutableMap<Identifier, EquipmentModel> = mutableMapOf()
- private val sentinelFirmRunning = AtomicInteger()
-
- private fun bakeModel(model: Identifier?, layers: List<ArmorOverrideLayer>?): Identifier {
- require(model == null || layers == null)
- if (model != null) {
- return model
- } else if (layers != null) {
- val idNumber = sentinelFirmRunning.incrementAndGet()
- val identifier = Identifier.of("firmament:sentinel/$idNumber")
- val equipmentLayers = layers.map {
- EquipmentModel.Layer(
- it.identifier, if (it.tint) {
- Optional.of(EquipmentModel.Dyeable(Optional.empty()))
- } else {
- Optional.empty()
- },
- false
- )
- }
- bakedOverrides[identifier] = EquipmentModel(
- mapOf(
- EquipmentModel.LayerType.HUMANOID to equipmentLayers,
- EquipmentModel.LayerType.HUMANOID_LEGGINGS to equipmentLayers,
- )
- )
- return identifier
- } else {
- error("Either model or layers must be non null")
- }
- }
-
-
- @Subscribe
- fun onStart(event: FinalizeResourceManagerEvent) {
- event.resourceManager.registerReloader(object :
- SinglePreparationResourceReloader<Map<String, ArmorOverride>>() {
- override fun prepare(manager: ResourceManager, profiler: Profiler): Map<String, ArmorOverride> {
- val overrideFiles = manager.findResources("overrides/armor_models") {
- it.namespace == "firmskyblock" && it.path.endsWith(".json")
- }
- val overrides = overrideFiles.mapNotNull {
- Firmament.tryDecodeJsonFromStream<ArmorOverride>(it.value.inputStream).getOrElse { ex ->
- logger.error("Failed to load armor texture override at ${it.key}", ex)
- null
- }
- }
- val associatedMap = overrides.flatMap { obj -> obj.itemIds.map { it to obj } }
- .toMap()
- return associatedMap
- }
-
- override fun apply(prepared: Map<String, ArmorOverride>, manager: ResourceManager, profiler: Profiler) {
- bakedOverrides.clear()
- prepared.forEach { it.value.bake() }
- overrides = prepared
- }
- })
- }
-
- @JvmStatic
- fun overrideArmor(itemStack: ItemStack): Optional<Identifier> {
- return overrideCache.invoke(itemStack)
- }
-
- @JvmStatic
- fun overrideArmorLayer(id: Identifier): EquipmentModel? {
- return bakedOverrides[id]
- }
-
-}
diff --git a/src/main/kotlin/features/texturepack/CustomGlobalTextures.kt b/src/main/kotlin/features/texturepack/CustomGlobalTextures.kt
deleted file mode 100644
index 9ad8bc1..0000000
--- a/src/main/kotlin/features/texturepack/CustomGlobalTextures.kt
+++ /dev/null
@@ -1,164 +0,0 @@
-@file:UseSerializers(IdentifierSerializer::class, CustomModelOverrideParser.FirmamentRootPredicateSerializer::class)
-
-package moe.nea.firmament.features.texturepack
-
-
-import java.util.Optional
-import java.util.concurrent.CompletableFuture
-import org.slf4j.LoggerFactory
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.UseSerializers
-import kotlin.jvm.optionals.getOrNull
-import net.minecraft.client.render.item.ItemModels
-import net.minecraft.client.render.model.BakedModel
-import net.minecraft.client.util.ModelIdentifier
-import net.minecraft.item.ItemStack
-import net.minecraft.resource.ResourceManager
-import net.minecraft.resource.SinglePreparationResourceReloader
-import net.minecraft.text.Text
-import net.minecraft.util.Identifier
-import net.minecraft.util.profiler.Profiler
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.annotations.Subscribe
-import moe.nea.firmament.events.BakeExtraModelsEvent
-import moe.nea.firmament.events.EarlyResourceReloadEvent
-import moe.nea.firmament.events.FinalizeResourceManagerEvent
-import moe.nea.firmament.events.ScreenChangeEvent
-import moe.nea.firmament.events.subscription.SubscriptionOwner
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.util.IdentifierSerializer
-import moe.nea.firmament.util.MC
-import moe.nea.firmament.util.collections.WeakCache
-import moe.nea.firmament.util.intoOptional
-import moe.nea.firmament.util.json.SingletonSerializableList
-import moe.nea.firmament.util.runNull
-
-object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalTextures.CustomGuiTextureOverride>(),
- SubscriptionOwner {
- override val delegateFeature: FirmamentFeature
- get() = CustomSkyBlockTextures
-
- class CustomGuiTextureOverride(
- val classes: List<ItemOverrideCollection>
- )
-
- @Serializable
- data class GlobalItemOverride(
- val screen: @Serializable(SingletonSerializableList::class) List<Identifier>,
- val model: Identifier,
- val predicate: FirmamentModelPredicate,
- )
-
- @Serializable
- data class ScreenFilter(
- val title: StringMatcher,
- )
-
- data class ItemOverrideCollection(
- val screenFilter: ScreenFilter,
- val overrides: List<GlobalItemOverride>,
- )
-
- @Subscribe
- fun onStart(event: FinalizeResourceManagerEvent) {
- MC.resourceManager.registerReloader(this)
- }
-
- @Subscribe
- fun onEarlyReload(event: EarlyResourceReloadEvent) {
- preparationFuture = CompletableFuture
- .supplyAsync(
- {
- prepare(event.resourceManager)
- }, event.preparationExecutor)
- }
-
- @Subscribe
- fun onBakeModels(event: BakeExtraModelsEvent) {
- for (guiClassOverride in preparationFuture.join().classes) {
- for (override in guiClassOverride.overrides) {
- event.addItemModel(ModelIdentifier(override.model, "inventory"))
- }
- }
- }
-
- @Volatile
- var preparationFuture: CompletableFuture<CustomGuiTextureOverride> = CompletableFuture.completedFuture(
- CustomGuiTextureOverride(listOf()))
-
- override fun prepare(manager: ResourceManager?, profiler: Profiler?): CustomGuiTextureOverride {
- return preparationFuture.join()
- }
-
- override fun apply(prepared: CustomGuiTextureOverride, manager: ResourceManager?, profiler: Profiler?) {
- this.guiClassOverrides = prepared
- }
-
- val logger = LoggerFactory.getLogger(CustomGlobalTextures::class.java)
- fun prepare(manager: ResourceManager): CustomGuiTextureOverride {
- val overrideResources =
- manager.findResources("overrides/item") { it.namespace == "firmskyblock" && it.path.endsWith(".json") }
- .mapNotNull {
- Firmament.tryDecodeJsonFromStream<GlobalItemOverride>(it.value.inputStream).getOrElse { ex ->
- logger.error("Failed to load global item override at ${it.key}", ex)
- null
- }
- }
-
- val byGuiClass = overrideResources.flatMap { override -> override.screen.toSet().map { it to override } }
- .groupBy { it.first }
- val guiClasses = byGuiClass.entries
- .mapNotNull {
- val key = it.key
- val guiClassResource =
- manager.getResource(Identifier.of(key.namespace, "filters/screen/${key.path}.json"))
- .getOrNull()
- ?: return@mapNotNull runNull {
- logger.error("Failed to locate screen filter at $key")
- }
- val screenFilter =
- Firmament.tryDecodeJsonFromStream<ScreenFilter>(guiClassResource.inputStream)
- .getOrElse { ex ->
- logger.error("Failed to load screen filter at $key", ex)
- return@mapNotNull null
- }
- ItemOverrideCollection(screenFilter, it.value.map { it.second })
- }
- logger.info("Loaded ${overrideResources.size} global item overrides")
- return CustomGuiTextureOverride(guiClasses)
- }
-
- var guiClassOverrides = CustomGuiTextureOverride(listOf())
-
- var matchingOverrides: Set<ItemOverrideCollection> = setOf()
-
- @Subscribe
- fun onOpenGui(event: ScreenChangeEvent) {
- val newTitle = event.new?.title ?: Text.empty()
- matchingOverrides = guiClassOverrides.classes
- .filterTo(mutableSetOf()) { it.screenFilter.title.matches(newTitle) }
- }
-
- val overrideCache =
- WeakCache.memoize<ItemStack, ItemModels, Optional<BakedModel>>("CustomGlobalTextureModelOverrides") { stack, models ->
- matchingOverrides
- .firstNotNullOfOrNull {
- it.overrides
- .asSequence()
- .filter { it.predicate.test(stack) }
- .map { models.getModel(it.model) }
- .firstOrNull()
- }
- .intoOptional()
- }
-
- @JvmStatic
- fun replaceGlobalModel(
- models: ItemModels,
- stack: ItemStack,
- ): BakedModel? {
- return overrideCache.invoke(stack, models).getOrNull()
- }
-
-
-}
diff --git a/src/main/kotlin/features/texturepack/CustomModelOverrideParser.kt b/src/main/kotlin/features/texturepack/CustomModelOverrideParser.kt
deleted file mode 100644
index c5fc20b..0000000
--- a/src/main/kotlin/features/texturepack/CustomModelOverrideParser.kt
+++ /dev/null
@@ -1,82 +0,0 @@
-
-package moe.nea.firmament.features.texturepack
-
-import com.google.gson.JsonObject
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.descriptors.SerialDescriptor
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
-import moe.nea.firmament.features.texturepack.predicates.AndPredicate
-import moe.nea.firmament.features.texturepack.predicates.DisplayNamePredicate
-import moe.nea.firmament.features.texturepack.predicates.ExtraAttributesPredicate
-import moe.nea.firmament.features.texturepack.predicates.ItemPredicate
-import moe.nea.firmament.features.texturepack.predicates.LorePredicate
-import moe.nea.firmament.features.texturepack.predicates.NotPredicate
-import moe.nea.firmament.features.texturepack.predicates.OrPredicate
-import moe.nea.firmament.features.texturepack.predicates.PetPredicate
-import net.minecraft.item.ItemStack
-import net.minecraft.util.Identifier
-
-object CustomModelOverrideParser {
- object FirmamentRootPredicateSerializer : KSerializer<FirmamentModelPredicate> {
- val delegateSerializer = kotlinx.serialization.json.JsonObject.serializer()
- override val descriptor: SerialDescriptor
- get() = SerialDescriptor("FirmamentModelRootPredicate", delegateSerializer.descriptor)
-
- override fun deserialize(decoder: Decoder): FirmamentModelPredicate {
- val json = decoder.decodeSerializableValue(delegateSerializer).intoGson() as JsonObject
- return AndPredicate(parsePredicates(json).toTypedArray())
- }
-
- override fun serialize(encoder: Encoder, value: FirmamentModelPredicate) {
- TODO("Cannot serialize firmament predicates")
- }
- }
-
- val predicateParsers = mutableMapOf<Identifier, FirmamentModelPredicateParser>()
-
-
- fun registerPredicateParser(name: String, parser: FirmamentModelPredicateParser) {
- predicateParsers[Identifier.of("firmament", name)] = parser
- }
-
- init {
- registerPredicateParser("display_name", DisplayNamePredicate.Parser)
- registerPredicateParser("lore", LorePredicate.Parser)
- registerPredicateParser("all", AndPredicate.Parser)
- registerPredicateParser("any", OrPredicate.Parser)
- registerPredicateParser("not", NotPredicate.Parser)
- registerPredicateParser("item", ItemPredicate.Parser)
- registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser)
- registerPredicateParser("pet", PetPredicate.Parser)
- }
-
- private val neverPredicate = listOf(
- object : FirmamentModelPredicate {
- override fun test(stack: ItemStack): Boolean {
- return false
- }
- }
- )
-
- fun parsePredicates(predicates: JsonObject): List<FirmamentModelPredicate> {
- val parsedPredicates = mutableListOf<FirmamentModelPredicate>()
- for (predicateName in predicates.keySet()) {
- if (!predicateName.startsWith("firmament:")) continue
- val identifier = Identifier.of(predicateName)
- val parser = predicateParsers[identifier] ?: return neverPredicate
- val parsedPredicate = parser.parse(predicates[predicateName]) ?: return neverPredicate
- parsedPredicates.add(parsedPredicate)
- }
- return parsedPredicates
- }
-
- @JvmStatic
- fun parseCustomModelOverrides(jsonObject: JsonObject): Array<FirmamentModelPredicate>? {
- val predicates = (jsonObject["predicate"] as? JsonObject) ?: return null
- val parsedPredicates = parsePredicates(predicates)
- if (parsedPredicates.isEmpty())
- return null
- return parsedPredicates.toTypedArray()
- }
-}
diff --git a/src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt b/src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt
deleted file mode 100644
index 627d39a..0000000
--- a/src/main/kotlin/features/texturepack/CustomSkyBlockTextures.kt
+++ /dev/null
@@ -1,135 +0,0 @@
-package moe.nea.firmament.features.texturepack
-
-import com.mojang.authlib.minecraft.MinecraftProfileTexture
-import com.mojang.authlib.properties.Property
-import java.util.Optional
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
-import kotlin.jvm.optionals.getOrNull
-import net.minecraft.block.SkullBlock
-import net.minecraft.client.MinecraftClient
-import net.minecraft.client.render.RenderLayer
-import net.minecraft.client.util.ModelIdentifier
-import net.minecraft.component.type.ProfileComponent
-import net.minecraft.util.Identifier
-import moe.nea.firmament.annotations.Subscribe
-import moe.nea.firmament.events.BakeExtraModelsEvent
-import moe.nea.firmament.events.CustomItemModelEvent
-import moe.nea.firmament.events.FinalizeResourceManagerEvent
-import moe.nea.firmament.events.TickEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
-import moe.nea.firmament.util.collections.WeakCache
-import moe.nea.firmament.util.mc.decodeProfileTextureProperty
-import moe.nea.firmament.util.skyBlockId
-
-object CustomSkyBlockTextures : FirmamentFeature {
- override val identifier: String
- get() = "custom-skyblock-textures"
-
- object TConfig : ManagedConfig(identifier, Category.INTEGRATIONS) { // TODO: should this be its own thing?
- val enabled by toggle("enabled") { true }
- val skullsEnabled by toggle("skulls-enabled") { true }
- val cacheForever by toggle("cache-forever") { true }
- val cacheDuration by integer("cache-duration", 0, 100) { 1 }
- val enableModelOverrides by toggle("model-overrides") { true }
- val enableArmorOverrides by toggle("armor-overrides") { true }
- val enableBlockOverrides by toggle("block-overrides") { true }
- val enableLegacyCIT by toggle("legacy-cit") { true }
- val allowRecoloringUiText by toggle("recolor-text") { true }
- }
-
- override val config: ManagedConfig
- get() = TConfig
-
- val allItemCaches by lazy {
- listOf(
- CustomItemModelEvent.cache.cache,
- skullTextureCache.cache,
- CustomGlobalTextures.overrideCache.cache,
- CustomGlobalArmorOverrides.overrideCache.cache
- )
- }
-
- fun clearAllCaches() {
- allItemCaches.forEach(WeakCache<*, *, *>::clear)
- }
-
- @Subscribe
- fun onTick(it: TickEvent) {
- if (TConfig.cacheForever) return
- if (TConfig.cacheDuration < 1 || it.tickCount % TConfig.cacheDuration == 0) {
- clearAllCaches()
- }
- }
-
- @Subscribe
- fun onStart(event: FinalizeResourceManagerEvent) {
- event.registerOnApply("Clear firmament CIT caches") {
- clearAllCaches()
- }
- }
-
- @Subscribe
- fun bakeCustomFirmModels(event: BakeExtraModelsEvent) {
- val resources =
- MinecraftClient.getInstance().resourceManager.findResources("models/item"
- ) { it: Identifier ->
- "firmskyblock" == it.namespace && it.path
- .endsWith(".json")
- }
- for (identifier in resources.keys) {
- val modelId = ModelIdentifier.ofInventoryVariant(
- Identifier.of(
- "firmskyblock",
- identifier.path.substring(
- "models/item/".length,
- identifier.path.length - ".json".length),
- ))
- event.addItemModel(modelId)
- }
- }
-
- @Subscribe
- fun onCustomModelId(it: CustomItemModelEvent) {
- if (!TConfig.enabled) return
- val id = it.itemStack.skyBlockId ?: return
- it.overrideModel = ModelIdentifier.ofInventoryVariant(Identifier.of("firmskyblock", id.identifier.path))
- }
-
- private val skullTextureCache =
- WeakCache.memoize<ProfileComponent, Optional<Identifier>>("SkullTextureCache") { component ->
- val id = getSkullTexture(component) ?: return@memoize Optional.empty()
- if (!MinecraftClient.getInstance().resourceManager.getResource(id).isPresent) {
- return@memoize Optional.empty()
- }
- return@memoize Optional.of(id)
- }
-
- private val mcUrlRegex = "https?://textures.minecraft.net/texture/([a-fA-F0-9]+)".toRegex()
-
- fun getSkullId(textureProperty: Property): String? {
- val texture = decodeProfileTextureProperty(textureProperty) ?: return null
- val textureUrl =
- texture.textures[MinecraftProfileTexture.Type.SKIN]?.url ?: return null
- val mcUrlData = mcUrlRegex.matchEntire(textureUrl) ?: return null
- return mcUrlData.groupValues[1]
- }
-
- fun getSkullTexture(profile: ProfileComponent): Identifier? {
- val id = getSkullId(profile.properties["textures"].firstOrNull() ?: return null) ?: return null
- return Identifier.of("firmskyblock", "textures/placedskull/$id.png")
- }
-
- fun modifySkullTexture(
- type: SkullBlock.SkullType?,
- component: ProfileComponent?,
- cir: CallbackInfoReturnable<RenderLayer>
- ) {
- if (type != SkullBlock.Type.PLAYER) return
- if (!TConfig.skullsEnabled) return
- if (component == null) return
-
- val n = skullTextureCache.invoke(component).getOrNull() ?: return
- cir.returnValue = RenderLayer.getEntityTranslucent(n)
- }
-}
diff --git a/src/main/kotlin/features/texturepack/CustomTextColors.kt b/src/main/kotlin/features/texturepack/CustomTextColors.kt
deleted file mode 100644
index 4ca1796..0000000
--- a/src/main/kotlin/features/texturepack/CustomTextColors.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-package moe.nea.firmament.features.texturepack
-
-import java.util.Optional
-import kotlinx.serialization.Serializable
-import kotlin.jvm.optionals.getOrNull
-import net.minecraft.resource.ResourceManager
-import net.minecraft.resource.SinglePreparationResourceReloader
-import net.minecraft.text.Text
-import net.minecraft.util.Identifier
-import net.minecraft.util.profiler.Profiler
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.annotations.Subscribe
-import moe.nea.firmament.events.FinalizeResourceManagerEvent
-import moe.nea.firmament.util.collections.WeakCache
-
-object CustomTextColors : SinglePreparationResourceReloader<CustomTextColors.TextOverrides?>() {
- @Serializable
- data class TextOverrides(
- val defaultColor: Int,
- val overrides: List<TextOverride> = listOf()
- )
-
- @Serializable
- data class TextOverride(
- val predicate: StringMatcher,
- val override: Int,
- )
-
- @Subscribe
- fun registerTextColorReloader(event: FinalizeResourceManagerEvent) {
- event.resourceManager.registerReloader(this)
- }
-
- val cache = WeakCache.memoize<Text, Optional<Int>>("CustomTextColor") { text ->
- val override = textOverrides ?: return@memoize Optional.empty()
- Optional.of(override.overrides.find { it.predicate.matches(text) }?.override ?: override.defaultColor)
- }
-
- fun mapTextColor(text: Text, oldColor: Int): Int {
- if (textOverrides == null) return oldColor
- return cache(text).getOrNull() ?: oldColor
- }
-
- override fun prepare(
- manager: ResourceManager,
- profiler: Profiler
- ): TextOverrides? {
- val resource = manager.getResource(Identifier.of("firmskyblock", "overrides/text_colors.json")).getOrNull()
- ?: return null
- return Firmament.tryDecodeJsonFromStream<TextOverrides>(resource.inputStream)
- .getOrElse {
- Firmament.logger.error("Could not parse text_colors.json", it)
- null
- }
- }
-
- var textOverrides: TextOverrides? = null
-
- override fun apply(
- prepared: TextOverrides?,
- manager: ResourceManager,
- profiler: Profiler
- ) {
- textOverrides = prepared
- }
-}
diff --git a/src/main/kotlin/features/texturepack/FirmamentModelPredicate.kt b/src/main/kotlin/features/texturepack/FirmamentModelPredicate.kt
deleted file mode 100644
index d11fec0..0000000
--- a/src/main/kotlin/features/texturepack/FirmamentModelPredicate.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-
-package moe.nea.firmament.features.texturepack
-
-import net.minecraft.item.ItemStack
-
-interface FirmamentModelPredicate {
- fun test(stack: ItemStack): Boolean
-}
diff --git a/src/main/kotlin/features/texturepack/FirmamentModelPredicateParser.kt b/src/main/kotlin/features/texturepack/FirmamentModelPredicateParser.kt
deleted file mode 100644
index 3ed0c67..0000000
--- a/src/main/kotlin/features/texturepack/FirmamentModelPredicateParser.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-
-package moe.nea.firmament.features.texturepack
-
-import com.google.gson.JsonElement
-
-interface FirmamentModelPredicateParser {
- fun parse(jsonElement: JsonElement): FirmamentModelPredicate?
-}
diff --git a/src/main/kotlin/features/texturepack/JsonUnbakedModelFirmExtra.kt b/src/main/kotlin/features/texturepack/JsonUnbakedModelFirmExtra.kt
deleted file mode 100644
index 9f641b8..0000000
--- a/src/main/kotlin/features/texturepack/JsonUnbakedModelFirmExtra.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-
-package moe.nea.firmament.features.texturepack
-
-import net.minecraft.client.render.model.Baker
-import net.minecraft.util.Identifier
-
-interface JsonUnbakedModelFirmExtra {
- fun storeExtraBaker_firmament(baker: Baker)
-
- fun setHeadModel_firmament(identifier: Identifier?)
- fun getHeadModel_firmament(): Identifier?
-
- fun setTintOverrides_firmament(tintOverrides: TintOverrides?)
- fun getTintOverrides_firmament(): TintOverrides
-
-}
diff --git a/src/main/kotlin/features/texturepack/ModelOverrideData.kt b/src/main/kotlin/features/texturepack/ModelOverrideData.kt
deleted file mode 100644
index 29d9192..0000000
--- a/src/main/kotlin/features/texturepack/ModelOverrideData.kt
+++ /dev/null
@@ -1,15 +0,0 @@
-package moe.nea.firmament.features.texturepack
-
-import net.minecraft.client.render.model.json.ModelOverride
-
-interface ModelOverrideData {
- companion object {
-
- @JvmStatic
- @Suppress("CAST_NEVER_SUCCEEDS")
- fun cast(override: ModelOverride) = override as ModelOverrideData
- }
-
- fun getFirmamentOverrides(): Array<FirmamentModelPredicate>?
- fun setFirmamentOverrides(overrides: Array<FirmamentModelPredicate>?)
-}
diff --git a/src/main/kotlin/features/texturepack/RarityMatcher.kt b/src/main/kotlin/features/texturepack/RarityMatcher.kt
deleted file mode 100644
index 634a171..0000000
--- a/src/main/kotlin/features/texturepack/RarityMatcher.kt
+++ /dev/null
@@ -1,69 +0,0 @@
-
-package moe.nea.firmament.features.texturepack
-
-import com.google.gson.JsonElement
-import io.github.moulberry.repo.data.Rarity
-import moe.nea.firmament.util.useMatch
-
-abstract class RarityMatcher {
- abstract fun match(rarity: Rarity): Boolean
-
- companion object {
- fun parse(jsonElement: JsonElement): RarityMatcher {
- val string = jsonElement.asString
- val range = parseRange(string)
- if (range != null) return range
- return Exact(Rarity.valueOf(string))
- }
-
- private val allRarities = Rarity.entries.joinToString("|", "(?:", ")")
- private val intervalSpec =
- "(?<beginningOpen>[\\[\\(])(?<beginning>$allRarities)?,(?<ending>$allRarities)?(?<endingOpen>[\\]\\)])"
- .toPattern()
-
- fun parseRange(string: String): RangeMatcher? {
- intervalSpec.useMatch<Nothing>(string) {
- // Open in the set-theory sense, meaning does not include its end.
- val beginningOpen = group("beginningOpen") == "("
- val endingOpen = group("endingOpen") == ")"
- val beginning = group("beginning")?.let(Rarity::valueOf)
- val ending = group("ending")?.let(Rarity::valueOf)
- return RangeMatcher(beginning, !beginningOpen, ending, !endingOpen)
- }
- return null
- }
-
- }
-
- data class Exact(val expected: Rarity) : RarityMatcher() {
- override fun match(rarity: Rarity): Boolean {
- return rarity == expected
- }
- }
-
- data class RangeMatcher(
- val beginning: Rarity?,
- val beginningInclusive: Boolean,
- val ending: Rarity?,
- val endingInclusive: Boolean,
- ) : RarityMatcher() {
- override fun match(rarity: Rarity): Boolean {
- if (beginning != null) {
- if (beginningInclusive) {
- if (rarity < beginning) return false
- } else {
- if (rarity <= beginning) return false
- }
- }
- if (ending != null) {
- if (endingInclusive) {
- if (rarity > ending) return false
- } else {
- if (rarity >= ending) return false
- }
- }
- return true
- }
- }
-
-}
diff --git a/src/main/kotlin/features/texturepack/StringMatcher.kt b/src/main/kotlin/features/texturepack/StringMatcher.kt
deleted file mode 100644
index 5eb86ac..0000000
--- a/src/main/kotlin/features/texturepack/StringMatcher.kt
+++ /dev/null
@@ -1,159 +0,0 @@
-
-package moe.nea.firmament.features.texturepack
-
-import com.google.gson.JsonArray
-import com.google.gson.JsonElement
-import com.google.gson.JsonNull
-import com.google.gson.JsonObject
-import com.google.gson.JsonPrimitive
-import com.google.gson.internal.LazilyParsedNumber
-import java.util.function.Predicate
-import kotlinx.serialization.KSerializer
-import kotlinx.serialization.Serializable
-import kotlinx.serialization.descriptors.SerialDescriptor
-import kotlinx.serialization.encoding.Decoder
-import kotlinx.serialization.encoding.Encoder
-import net.minecraft.nbt.NbtString
-import net.minecraft.text.Text
-import moe.nea.firmament.util.MC
-import moe.nea.firmament.util.removeColorCodes
-
-@Serializable(with = StringMatcher.Serializer::class)
-interface StringMatcher {
- fun matches(string: String): Boolean
- fun matches(text: Text): Boolean {
- return matches(text.string)
- }
-
- fun matches(nbt: NbtString): Boolean {
- val string = nbt.asString()
- val jsonStart = string.indexOf('{')
- val stringStart = string.indexOf('"')
- val isString = stringStart >= 0 && string.subSequence(0, stringStart).isBlank()
- val isJson = jsonStart >= 0 && string.subSequence(0, jsonStart).isBlank()
- if (isString || isJson)
- return matches(Text.Serialization.fromJson(string, MC.defaultRegistries) ?: return false)
- return matches(string)
- }
-
- class Equals(input: String, val stripColorCodes: Boolean) : StringMatcher {
- private val expected = if (stripColorCodes) input.removeColorCodes() else input
- override fun matches(string: String): Boolean {
- return expected == (if (stripColorCodes) string.removeColorCodes() else string)
- }
-
- override fun toString(): String {
- return "Equals($expected, stripColorCodes = $stripColorCodes)"
- }
- }
-
- class Pattern(val patternWithColorCodes: String, val stripColorCodes: Boolean) : StringMatcher {
- private val regex: Predicate<String> = patternWithColorCodes.toPattern().asMatchPredicate()
- override fun matches(string: String): Boolean {
- return regex.test(if (stripColorCodes) string.removeColorCodes() else string)
- }
-
- override fun toString(): String {
- return "Pattern($patternWithColorCodes, stripColorCodes = $stripColorCodes)"
- }
- }
-
- object Serializer : KSerializer<StringMatcher> {
- val delegateSerializer = kotlinx.serialization.json.JsonElement.serializer()
- override val descriptor: SerialDescriptor
- get() = SerialDescriptor("StringMatcher", delegateSerializer.descriptor)
-
- override fun deserialize(decoder: Decoder): StringMatcher {
- val delegate = decoder.decodeSerializableValue(delegateSerializer)
- val gsonDelegate = delegate.intoGson()
- return parse(gsonDelegate)
- }
-
- override fun serialize(encoder: Encoder, value: StringMatcher) {
- encoder.encodeSerializableValue(delegateSerializer, Companion.serialize(value).intoKotlinJson())
- }
-
- }
-
- companion object {
- fun serialize(stringMatcher: StringMatcher): JsonElement {
- TODO("Cannot serialize string matchers rn")
- }
-
- fun parse(jsonElement: JsonElement): StringMatcher {
- if (jsonElement is JsonPrimitive) {
- return Equals(jsonElement.asString, true)
- }
- if (jsonElement is JsonObject) {
- val regex = jsonElement["regex"] as JsonPrimitive?
- val text = jsonElement["equals"] as JsonPrimitive?
- val shouldStripColor = when (val color = (jsonElement["color"] as JsonPrimitive?)?.asString) {
- "preserve" -> false
- "strip", null -> true
- else -> error("Unknown color preservation mode: $color")
- }
- if ((regex == null) == (text == null)) error("Could not parse $jsonElement as string matcher")
- if (regex != null)
- return Pattern(regex.asString, shouldStripColor)
- if (text != null)
- return Equals(text.asString, shouldStripColor)
- }
- error("Could not parse $jsonElement as a string matcher")
- }
- }
-}
-
-fun JsonElement.intoKotlinJson(): kotlinx.serialization.json.JsonElement {
- when (this) {
- is JsonNull -> return kotlinx.serialization.json.JsonNull
- is JsonObject -> {
- return kotlinx.serialization.json.JsonObject(this.entrySet()
- .associate { it.key to it.value.intoKotlinJson() })
- }
-
- is JsonArray -> {
- return kotlinx.serialization.json.JsonArray(this.map { it.intoKotlinJson() })
- }
-
- is JsonPrimitive -> {
- if (this.isString)
- return kotlinx.serialization.json.JsonPrimitive(this.asString)
- if (this.isBoolean)
- return kotlinx.serialization.json.JsonPrimitive(this.asBoolean)
- return kotlinx.serialization.json.JsonPrimitive(this.asNumber)
- }
-
- else -> error("Unknown json variant $this")
- }
-}
-
-fun kotlinx.serialization.json.JsonElement.intoGson(): JsonElement {
- when (this) {
- is kotlinx.serialization.json.JsonNull -> return JsonNull.INSTANCE
- is kotlinx.serialization.json.JsonPrimitive -> {
- if (this.isString)
- return JsonPrimitive(this.content)
- if (this.content == "true")
- return JsonPrimitive(true)
- if (this.content == "false")
- return JsonPrimitive(false)
- return JsonPrimitive(LazilyParsedNumber(this.content))
- }
-
- is kotlinx.serialization.json.JsonObject -> {
- val obj = JsonObject()
- for ((k, v) in this) {
- obj.add(k, v.intoGson())
- }
- return obj
- }
-
- is kotlinx.serialization.json.JsonArray -> {
- val arr = JsonArray()
- for (v in this) {
- arr.add(v.intoGson())
- }
- return arr
- }
- }
-}
diff --git a/src/main/kotlin/features/texturepack/TintOverrides.kt b/src/main/kotlin/features/texturepack/TintOverrides.kt
deleted file mode 100644
index 85fcae4..0000000
--- a/src/main/kotlin/features/texturepack/TintOverrides.kt
+++ /dev/null
@@ -1,75 +0,0 @@
-package moe.nea.firmament.features.texturepack
-
-import com.google.gson.JsonObject
-import com.google.gson.JsonPrimitive
-import moe.nea.firmament.util.ErrorUtil
-
-data class TintOverrides(
- val layerMap: Map<Int, TintOverride> = mapOf()
-) {
- val hasOverrides by lazy { layerMap.values.any { it !is Reset } }
-
- companion object {
- val EMPTY = TintOverrides()
- private val threadLocal = object : ThreadLocal<TintOverrides>() {}
- fun enter(overrides: TintOverrides?) {
- ErrorUtil.softCheck("Double entered tintOverrides",
- threadLocal.get() == null)
- threadLocal.set(overrides ?: EMPTY)
- }
-
- fun exit(overrides: TintOverrides?) {
- ErrorUtil.softCheck("Exited with non matching enter tintOverrides",
- threadLocal.get() == (overrides ?: EMPTY))
- threadLocal.remove()
- }
-
- fun getCurrentOverrides(): TintOverrides {
- return ErrorUtil.notNullOr(threadLocal.get(), "Got current tintOverrides without entering") {
- EMPTY
- }
- }
-
- fun parse(jsonObject: JsonObject): TintOverrides {
- val map = mutableMapOf<Int, TintOverride>()
- for ((key, value) in jsonObject.entrySet()) {
- val layerIndex =
- ErrorUtil.notNullOr(key.toIntOrNull(),
- "Unknown layer index $value. Should be integer") { continue }
- if (value.isJsonNull) {
- map[layerIndex] = Reset
- continue
- }
- val override = (value as? JsonPrimitive)
- ?.takeIf(JsonPrimitive::isNumber)
- ?.asInt
- ?.let(::Fixed)
- if (override == null) {
- ErrorUtil.softError("Invalid tint override for a layer: $value")
- continue
- }
- map[layerIndex] = override
- }
- return TintOverrides(map)
- }
- }
-
- fun mergeWithParent(parent: TintOverrides): TintOverrides {
- val mergedMap = parent.layerMap.toMutableMap()
- mergedMap.putAll(this.layerMap)
- return TintOverrides(mergedMap)
- }
-
- fun hasOverrides(): Boolean = hasOverrides
- fun getOverride(tintIndex: Int): Int? {
- return when (val tint = layerMap[tintIndex]) {
- is Reset -> null
- is Fixed -> tint.color
- null -> null
- }
- }
-
- sealed interface TintOverride
- data object Reset : TintOverride
- data class Fixed(val color: Int) : TintOverride
-}
diff --git a/src/main/kotlin/features/texturepack/predicates/AlwaysPredicate.kt b/src/main/kotlin/features/texturepack/predicates/AlwaysPredicate.kt
deleted file mode 100644
index 7e0ddb1..0000000
--- a/src/main/kotlin/features/texturepack/predicates/AlwaysPredicate.kt
+++ /dev/null
@@ -1,19 +0,0 @@
-
-package moe.nea.firmament.features.texturepack.predicates
-
-import com.google.gson.JsonElement
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
-import net.minecraft.item.ItemStack
-
-object AlwaysPredicate : FirmamentModelPredicate {
- override fun test(stack: ItemStack): Boolean {
- return true
- }
-
- object Parser : FirmamentModelPredicateParser {
- override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
- return AlwaysPredicate
- }
- }
-}
diff --git a/src/main/kotlin/features/texturepack/predicates/AndPredicate.kt b/src/main/kotlin/features/texturepack/predicates/AndPredicate.kt
deleted file mode 100644
index 99abaaa..0000000
--- a/src/main/kotlin/features/texturepack/predicates/AndPredicate.kt
+++ /dev/null
@@ -1,28 +0,0 @@
-package moe.nea.firmament.features.texturepack.predicates
-
-import com.google.gson.JsonArray
-import com.google.gson.JsonElement
-import com.google.gson.JsonObject
-import moe.nea.firmament.features.texturepack.CustomModelOverrideParser
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
-import net.minecraft.item.ItemStack
-
-class AndPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate {
- override fun test(stack: ItemStack): Boolean {
- return children.all { it.test(stack) }
- }
-
- object Parser : FirmamentModelPredicateParser {
- override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
- val children =
- (jsonElement as JsonArray)
- .flatMap {
- CustomModelOverrideParser.parsePredicates(it as JsonObject)
- }
- .toTypedArray()
- return AndPredicate(children)
- }
-
- }
-}
diff --git a/src/main/kotlin/features/texturepack/predicates/DisplayNamePredicate.kt b/src/main/kotlin/features/texturepack/predicates/DisplayNamePredicate.kt
deleted file mode 100644
index 04c7a2b..0000000
--- a/src/main/kotlin/features/texturepack/predicates/DisplayNamePredicate.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-
-package moe.nea.firmament.features.texturepack.predicates
-
-import com.google.gson.JsonElement
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
-import moe.nea.firmament.features.texturepack.StringMatcher
-import net.minecraft.item.ItemStack
-import moe.nea.firmament.util.mc.displayNameAccordingToNbt
-
-data class DisplayNamePredicate(val stringMatcher: StringMatcher) : FirmamentModelPredicate {
- override fun test(stack: ItemStack): Boolean {
- val display = stack.displayNameAccordingToNbt
- return stringMatcher.matches(display)
- }
-
- object Parser : FirmamentModelPredicateParser {
- override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
- return DisplayNamePredicate(StringMatcher.parse(jsonElement))
- }
- }
-}
diff --git a/src/main/kotlin/features/texturepack/predicates/ExtraAttributesPredicate.kt b/src/main/kotlin/features/texturepack/predicates/ExtraAttributesPredicate.kt
deleted file mode 100644
index 3c8023d..0000000
--- a/src/main/kotlin/features/texturepack/predicates/ExtraAttributesPredicate.kt
+++ /dev/null
@@ -1,271 +0,0 @@
-
-package moe.nea.firmament.features.texturepack.predicates
-
-import com.google.gson.JsonArray
-import com.google.gson.JsonElement
-import com.google.gson.JsonObject
-import com.google.gson.JsonPrimitive
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
-import moe.nea.firmament.features.texturepack.StringMatcher
-import net.minecraft.item.ItemStack
-import net.minecraft.nbt.NbtByte
-import net.minecraft.nbt.NbtCompound
-import net.minecraft.nbt.NbtDouble
-import net.minecraft.nbt.NbtElement
-import net.minecraft.nbt.NbtFloat
-import net.minecraft.nbt.NbtInt
-import net.minecraft.nbt.NbtList
-import net.minecraft.nbt.NbtLong
-import net.minecraft.nbt.NbtShort
-import net.minecraft.nbt.NbtString
-import moe.nea.firmament.util.extraAttributes
-
-fun interface NbtMatcher {
- fun matches(nbt: NbtElement): Boolean
-
- object Parser {
- fun parse(jsonElement: JsonElement): NbtMatcher? {
- if (jsonElement is JsonPrimitive) {
- if (jsonElement.isString) {
- val string = jsonElement.asString
- return MatchStringExact(string)
- }
- if (jsonElement.isNumber) {
- return MatchNumberExact(jsonElement.asLong) //TODO: parse generic number
- }
- }
- if (jsonElement is JsonObject) {
- var encounteredParser: NbtMatcher? = null
- for (entry in ExclusiveParserType.entries) {
- val data = jsonElement[entry.key] ?: continue
- if (encounteredParser != null) {
- // TODO: warn
- return null
- }
- encounteredParser = entry.parse(data) ?: return null
- }
- return encounteredParser
- }
- return null
- }
-
- enum class ExclusiveParserType(val key: String) {
- STRING("string") {
- override fun parse(element: JsonElement): NbtMatcher? {
- return MatchString(StringMatcher.parse(element))
- }
- },
- INT("int") {
- override fun parse(element: JsonElement): NbtMatcher? {
- return parseGenericNumber(element,
- { it.asInt },
- { (it as? NbtInt)?.intValue() },
- { a, b ->
- if (a == b) Comparison.EQUAL
- else if (a < b) Comparison.LESS_THAN
- else Comparison.GREATER
- })
- }
- },
- FLOAT("float") {
- override fun parse(element: JsonElement): NbtMatcher? {
- return parseGenericNumber(element,
- { it.asFloat },
- { (it as? NbtFloat)?.floatValue() },
- { a, b ->
- if (a == b) Comparison.EQUAL
- else if (a < b) Comparison.LESS_THAN
- else Comparison.GREATER
- })
- }
- },
- DOUBLE("double") {
- override fun parse(element: JsonElement): NbtMatcher? {
- return parseGenericNumber(element,
- { it.asDouble },
- { (it as? NbtDouble)?.doubleValue() },
- { a, b ->
- if (a == b) Comparison.EQUAL
- else if (a < b) Comparison.LESS_THAN
- else Comparison.GREATER
- })
- }
- },
- LONG("long") {
- override fun parse(element: JsonElement): NbtMatcher? {
- return parseGenericNumber(element,
- { it.asLong },
- { (it as? NbtLong)?.longValue() },
- { a, b ->
- if (a == b) Comparison.EQUAL
- else if (a < b) Comparison.LESS_THAN
- else Comparison.GREATER
- })
- }
- },
- SHORT("short") {
- override fun parse(element: JsonElement): NbtMatcher? {
- return parseGenericNumber(element,
- { it.asShort },
- { (it as? NbtShort)?.shortValue() },
- { a, b ->
- if (a == b) Comparison.EQUAL
- else if (a < b) Comparison.LESS_THAN
- else Comparison.GREATER
- })
- }
- },
- BYTE("byte") {
- override fun parse(element: JsonElement): NbtMatcher? {
- return parseGenericNumber(element,
- { it.asByte },
- { (it as? NbtByte)?.byteValue() },
- { a, b ->
- if (a == b) Comparison.EQUAL
- else if (a < b) Comparison.LESS_THAN
- else Comparison.GREATER
- })
- }
- },
- ;
-
- abstract fun parse(element: JsonElement): NbtMatcher?
- }
-
- enum class Comparison {
- LESS_THAN, EQUAL, GREATER
- }
-
- inline fun <T : Any> parseGenericNumber(
- jsonElement: JsonElement,
- primitiveExtractor: (JsonPrimitive) -> T?,
- crossinline nbtExtractor: (NbtElement) -> T?,
- crossinline compare: (T, T) -> Comparison
- ): NbtMatcher? {
- if (jsonElement is JsonPrimitive) {
- val expected = primitiveExtractor(jsonElement) ?: return null
- return NbtMatcher {
- val actual = nbtExtractor(it) ?: return@NbtMatcher false
- compare(actual, expected) == Comparison.EQUAL
- }
- }
- if (jsonElement is JsonObject) {
- val minElement = jsonElement.getAsJsonPrimitive("min")
- val min = if (minElement != null) primitiveExtractor(minElement) ?: return null else null
- val minExclusive = jsonElement.get("minExclusive")?.asBoolean ?: false
- val maxElement = jsonElement.getAsJsonPrimitive("max")
- val max = if (maxElement != null) primitiveExtractor(maxElement) ?: return null else null
- val maxExclusive = jsonElement.get("maxExclusive")?.asBoolean ?: true
- if (min == null && max == null) return null
- return NbtMatcher {
- val actual = nbtExtractor(it) ?: return@NbtMatcher false
- if (max != null) {
- val comp = compare(actual, max)
- if (comp == Comparison.GREATER) return@NbtMatcher false
- if (comp == Comparison.EQUAL && maxExclusive) return@NbtMatcher false
- }
- if (min != null) {
- val comp = compare(actual, min)
- if (comp == Comparison.LESS_THAN) return@NbtMatcher false
- if (comp == Comparison.EQUAL && minExclusive) return@NbtMatcher false
- }
- return@NbtMatcher true
- }
- }
- return null
-
- }
- }
-
- class MatchNumberExact(val number: Long) : NbtMatcher {
- override fun matches(nbt: NbtElement): Boolean {
- return when (nbt) {
- is NbtByte -> nbt.byteValue().toLong() == number
- is NbtInt -> nbt.intValue().toLong() == number
- is NbtShort -> nbt.shortValue().toLong() == number
- is NbtLong -> nbt.longValue().toLong() == number
- else -> false
- }
- }
-
- }
-
- class MatchStringExact(val string: String) : NbtMatcher {
- override fun matches(nbt: NbtElement): Boolean {
- return nbt is NbtString && nbt.asString() == string
- }
-
- override fun toString(): String {
- return "MatchNbtStringExactly($string)"
- }
- }
-
- class MatchString(val string: StringMatcher) : NbtMatcher {
- override fun matches(nbt: NbtElement): Boolean {
- return nbt is NbtString && string.matches(nbt.asString())
- }
-
- override fun toString(): String {
- return "MatchNbtString($string)"
- }
- }
-}
-
-data class ExtraAttributesPredicate(
- val path: NbtPrism,
- val matcher: NbtMatcher,
-) : FirmamentModelPredicate {
-
- object Parser : FirmamentModelPredicateParser {
- override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? {
- if (jsonElement !is JsonObject) return null
- val path = jsonElement.get("path") ?: return null
- val pathSegments = if (path is JsonArray) {
- path.map { (it as JsonPrimitive).asString }
- } else if (path is JsonPrimitive && path.isString) {
- path.asString.split(".")
- } else return null
- val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement)
- ?: return null
- return ExtraAttributesPredicate(NbtPrism(pathSegments), matcher)
- }
- }
-
- override fun test(stack: ItemStack): Boolean {
- return path.access(stack.extraAttributes)
- .any { matcher.matches(it) }
- }
-}
-
-class NbtPrism(val path: List<String>) {
- override fun toString(): String {
- return "Prism($path)"
- }
- fun access(root: NbtElement): Collection<NbtElement> {
- var rootSet = mutableListOf(root)
- var switch = mutableListOf<NbtElement>()
- for (pathSegment in path) {
- if (pathSegment == ".") continue
- for (element in rootSet) {
- if (element is NbtList) {
- if (pathSegment == "*")
- switch.addAll(element)
- val index = pathSegment.toIntOrNull() ?: continue
- if (index !in element.indices) continue
- switch.add(element[index])
- }
- if (element is NbtCompound) {
- if (pathSegment == "*")
- element.keys.mapTo(switch) { element.get(it)!! }
- switch.add(element.get(pathSegment) ?: continue)
- }
- }
- val temp = switch
- switch = rootSet
- rootSet = temp
- switch.clear()
- }
- return rootSet
- }
-}
diff --git a/src/main/kotlin/features/texturepack/predicates/ItemPredicate.kt b/src/main/kotlin/features/texturepack/predicates/ItemPredicate.kt
deleted file mode 100644
index 3cb80c7..0000000
--- a/src/main/kotlin/features/texturepack/predicates/ItemPredicate.kt
+++ /dev/null
@@ -1,34 +0,0 @@
-
-package moe.nea.firmament.features.texturepack.predicates
-
-import com.google.gson.JsonElement
-import com.google.gson.JsonPrimitive
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
-import kotlin.jvm.optionals.getOrNull
-import net.minecraft.item.Item
-import net.minecraft.item.ItemStack
-import net.minecraft.registry.RegistryKey
-import net.minecraft.registry.RegistryKeys
-import net.minecraft.util.Identifier
-import moe.nea.firmament.util.MC
-
-class ItemPredicate(
- val item: Item
-) : FirmamentModelPredicate {
- override fun test(stack: ItemStack): Boolean {
- return stack.item == item
- }
-
- object Parser : FirmamentModelPredicateParser {
- override fun parse(jsonElement: JsonElement): ItemPredicate? {
- if (jsonElement is JsonPrimitive && jsonElement.isString) {
- val itemKey = RegistryKey.of(RegistryKeys.ITEM,
- Identifier.tryParse(jsonElement.asString)
- ?: return null)
- return ItemPredicate(MC.defaultItems.getOptional(itemKey).getOrNull()?.value() ?: return null)
- }
- return null
- }
- }
-}
diff --git a/src/main/kotlin/features/texturepack/predicates/LorePredicate.kt b/src/main/kotlin/features/texturepack/predicates/LorePredicate.kt
deleted file mode 100644
index f0b4737..0000000
--- a/src/main/kotlin/features/texturepack/predicates/LorePredicate.kt
+++ /dev/null
@@ -1,22 +0,0 @@
-
-package moe.nea.firmament.features.texturepack.predicates
-
-import com.google.gson.JsonElement
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
-import moe.nea.firmament.features.texturepack.StringMatcher
-import net.minecraft.item.ItemStack
-import moe.nea.firmament.util.mc.loreAccordingToNbt
-
-class LorePredicate(val matcher: StringMatcher) : FirmamentModelPredicate {
- object Parser : FirmamentModelPredicateParser {
- override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
- return LorePredicate(StringMatcher.parse(jsonElement))
- }
- }
-
- override fun test(stack: ItemStack): Boolean {
- val lore = stack.loreAccordingToNbt
- return lore.any { matcher.matches(it) }
- }
-}
diff --git a/src/main/kotlin/features/texturepack/predicates/NotPredicate.kt b/src/main/kotlin/features/texturepack/predicates/NotPredicate.kt
deleted file mode 100644
index 4986ad9..0000000
--- a/src/main/kotlin/features/texturepack/predicates/NotPredicate.kt
+++ /dev/null
@@ -1,21 +0,0 @@
-
-package moe.nea.firmament.features.texturepack.predicates
-
-import com.google.gson.JsonElement
-import com.google.gson.JsonObject
-import moe.nea.firmament.features.texturepack.CustomModelOverrideParser
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
-import net.minecraft.item.ItemStack
-
-class NotPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate {
- override fun test(stack: ItemStack): Boolean {
- return children.none { it.test(stack) }
- }
-
- object Parser : FirmamentModelPredicateParser {
- override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
- return NotPredicate(CustomModelOverrideParser.parsePredicates(jsonElement as JsonObject).toTypedArray())
- }
- }
-}
diff --git a/src/main/kotlin/features/texturepack/predicates/NumberMatcher.kt b/src/main/kotlin/features/texturepack/predicates/NumberMatcher.kt
deleted file mode 100644
index b0d5178..0000000
--- a/src/main/kotlin/features/texturepack/predicates/NumberMatcher.kt
+++ /dev/null
@@ -1,124 +0,0 @@
-package moe.nea.firmament.features.texturepack.predicates
-
-import com.google.gson.JsonElement
-import com.google.gson.JsonPrimitive
-import moe.nea.firmament.util.useMatch
-
-abstract class NumberMatcher {
- abstract fun test(number: Number): Boolean
-
-
- companion object {
- fun parse(jsonElement: JsonElement): NumberMatcher? {
- if (jsonElement is JsonPrimitive) {
- if (jsonElement.isString) {
- val string = jsonElement.asString
- return parseRange(string) ?: parseOperator(string)
- }
- if (jsonElement.isNumber) {
- val number = jsonElement.asNumber
- val hasDecimals = (number.toString().contains("."))
- return MatchNumberExact(if (hasDecimals) number.toLong() else number.toDouble())
- }
- }
- return null
- }
-
- private val intervalSpec =
- "(?<beginningOpen>[\\[\\(])(?<beginning>[0-9.]+)?,(?<ending>[0-9.]+)?(?<endingOpen>[\\]\\)])"
- .toPattern()
-
- fun parseRange(string: String): RangeMatcher? {
- intervalSpec.useMatch<Nothing>(string) {
- // Open in the set-theory sense, meaning does not include its end.
- val beginningOpen = group("beginningOpen") == "("
- val endingOpen = group("endingOpen") == ")"
- val beginning = group("beginning")?.toDouble()
- val ending = group("ending")?.toDouble()
- return RangeMatcher(beginning, !beginningOpen, ending, !endingOpen)
- }
- return null
- }
-
- enum class Operator(val operator: String) {
- LESS("<") {
- override fun matches(comparisonResult: Int): Boolean {
- return comparisonResult < 0
- }
- },
- LESS_EQUALS("<=") {
- override fun matches(comparisonResult: Int): Boolean {
- return comparisonResult <= 0
- }
- },
- GREATER(">") {
- override fun matches(comparisonResult: Int): Boolean {
- return comparisonResult > 0
- }
- },
- GREATER_EQUALS(">=") {
- override fun matches(comparisonResult: Int): Boolean {
- return comparisonResult >= 0
- }
- },
- ;
-
- abstract fun matches(comparisonResult: Int): Boolean
- }
-
- private val operatorPattern =
- "(?<operator>${Operator.entries.joinToString("|") { it.operator }})(?<value>[0-9.]+)".toPattern()
-
- fun parseOperator(string: String): OperatorMatcher? {
- return operatorPattern.useMatch(string) {
- val operatorName = group("operator")
- val operator = Operator.entries.find { it.operator == operatorName }!!
- val value = group("value").toDouble()
- OperatorMatcher(operator, value)
- }
- }
-
- data class OperatorMatcher(val operator: Operator, val value: Double) : NumberMatcher() {
- override fun test(number: Number): Boolean {
- return operator.matches(number.toDouble().compareTo(value))
- }
- }
-
-
- data class MatchNumberExact(val number: Number) : NumberMatcher() {
- override fun test(number: Number): Boolean {
- return when (this.number) {
- is Double -> number.toDouble() == this.number.toDouble()
- else -> number.toLong() == this.number.toLong()
- }
- }
- }
-
- data class RangeMatcher(
- val beginning: Double?,
- val beginningInclusive: Boolean,
- val ending: Double?,
- val endingInclusive: Boolean,
- ) : NumberMatcher() {
- override fun test(number: Number): Boolean {
- val value = number.toDouble()
- if (beginning != null) {
- if (beginningInclusive) {
- if (value < beginning) return false
- } else {
- if (value <= beginning) return false
- }
- }
- if (ending != null) {
- if (endingInclusive) {
- if (value > ending) return false
- } else {
- if (value >= ending) return false
- }
- }
- return true
- }
- }
- }
-
-}
diff --git a/src/main/kotlin/features/texturepack/predicates/OrPredicate.kt b/src/main/kotlin/features/texturepack/predicates/OrPredicate.kt
deleted file mode 100644
index e3093cd..0000000
--- a/src/main/kotlin/features/texturepack/predicates/OrPredicate.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-
-package moe.nea.firmament.features.texturepack.predicates
-
-import com.google.gson.JsonArray
-import com.google.gson.JsonElement
-import com.google.gson.JsonObject
-import moe.nea.firmament.features.texturepack.CustomModelOverrideParser
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
-import net.minecraft.item.ItemStack
-
-class OrPredicate(val children: Array<FirmamentModelPredicate>) : FirmamentModelPredicate {
- override fun test(stack: ItemStack): Boolean {
- return children.any { it.test(stack) }
- }
-
- object Parser : FirmamentModelPredicateParser {
- override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
- val children =
- (jsonElement as JsonArray)
- .flatMap {
- CustomModelOverrideParser.parsePredicates(it as JsonObject)
- }
- .toTypedArray()
- return OrPredicate(children)
- }
-
- }
-}
diff --git a/src/main/kotlin/features/texturepack/predicates/PetPredicate.kt b/src/main/kotlin/features/texturepack/predicates/PetPredicate.kt
deleted file mode 100644
index b30b7c9..0000000
--- a/src/main/kotlin/features/texturepack/predicates/PetPredicate.kt
+++ /dev/null
@@ -1,66 +0,0 @@
-
-package moe.nea.firmament.features.texturepack.predicates
-
-import com.google.gson.JsonElement
-import com.google.gson.JsonObject
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
-import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
-import moe.nea.firmament.features.texturepack.RarityMatcher
-import moe.nea.firmament.features.texturepack.StringMatcher
-import net.minecraft.item.ItemStack
-import moe.nea.firmament.repo.ExpLadders
-import moe.nea.firmament.util.petData
-
-data class PetPredicate(
- val petId: StringMatcher?,
- val tier: RarityMatcher?,
- val exp: NumberMatcher?,
- val candyUsed: NumberMatcher?,
- val level: NumberMatcher?,
-) : FirmamentModelPredicate {
-
- override fun test(stack: ItemStack): Boolean {
- val petData = stack.petData ?: return false
- if (petId != null) {
- if (!petId.matches(petData.type)) return false
- }
- if (exp != null) {
- if (!exp.test(petData.exp)) return false
- }
- if (candyUsed != null) {
- if (!candyUsed.test(petData.candyUsed)) return false
- }
- if (tier != null) {
- if (!tier.match(petData.tier)) return false
- }
- val levelData by lazy(LazyThreadSafetyMode.NONE) {
- ExpLadders.getExpLadder(petData.type, petData.tier)
- .getPetLevel(petData.exp)
- }
- if (level != null) {
- if (!level.test(levelData.currentLevel)) return false
- }
- return true
- }
-
- object Parser : FirmamentModelPredicateParser {
- override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? {
- if (jsonElement.isJsonPrimitive) {
- return PetPredicate(StringMatcher.Equals(jsonElement.asString, false), null, null, null, null)
- }
- if (jsonElement !is JsonObject) return null
- val idMatcher = jsonElement["id"]?.let(StringMatcher::parse)
- val expMatcher = jsonElement["exp"]?.let(NumberMatcher::parse)
- val levelMatcher = jsonElement["level"]?.let(NumberMatcher::parse)
- val candyMatcher = jsonElement["candyUsed"]?.let(NumberMatcher::parse)
- val tierMatcher = jsonElement["tier"]?.let(RarityMatcher::parse)
- return PetPredicate(
- idMatcher,
- tierMatcher,
- expMatcher,
- candyMatcher,
- levelMatcher,
- )
- }
- }
-}
diff --git a/src/main/kotlin/features/world/ColeWeightCompat.kt b/src/main/kotlin/features/world/ColeWeightCompat.kt
new file mode 100644
index 0000000..f7f1317
--- /dev/null
+++ b/src/main/kotlin/features/world/ColeWeightCompat.kt
@@ -0,0 +1,125 @@
+package moe.nea.firmament.features.world
+
+import kotlinx.serialization.Serializable
+import net.minecraft.text.Text
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.DefaultSource
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.tr
+
+object ColeWeightCompat {
+ @Serializable
+ data class ColeWeightWaypoint(
+ val x: Int?,
+ val y: Int?,
+ val z: Int?,
+ val r: Int = 0,
+ val g: Int = 0,
+ val b: Int = 0,
+ )
+
+ fun fromFirm(waypoints: FirmWaypoints, relativeTo: BlockPos): List<ColeWeightWaypoint> {
+ return waypoints.waypoints.map {
+ ColeWeightWaypoint(it.x - relativeTo.x, it.y - relativeTo.y, it.z - relativeTo.z)
+ }
+ }
+
+ fun intoFirm(waypoints: List<ColeWeightWaypoint>, relativeTo: BlockPos): FirmWaypoints {
+ val w = waypoints
+ .filter { it.x != null && it.y != null && it.z != null }
+ .map { FirmWaypoints.Waypoint(it.x!! + relativeTo.x, it.y!! + relativeTo.y, it.z!! + relativeTo.z) }
+ return FirmWaypoints(
+ "Imported Waypoints",
+ "imported",
+ null,
+ w.toMutableList(),
+ false
+ )
+ }
+
+ fun copyAndInform(
+ source: DefaultSource,
+ origin: BlockPos,
+ positiveFeedback: (Int) -> Text,
+ ) {
+ val waypoints = Waypoints.useNonEmptyWaypoints()
+ ?.let { fromFirm(it, origin) }
+ if (waypoints == null) {
+ source.sendError(Waypoints.textNothingToExport())
+ return
+ }
+ val data =
+ Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints)
+ ClipboardUtils.setTextContent(data)
+ source.sendFeedback(positiveFeedback(waypoints.size))
+ }
+
+ fun importAndInform(
+ source: DefaultSource,
+ pos: BlockPos?,
+ positiveFeedback: (Int) -> Text
+ ) {
+ val text = ClipboardUtils.getTextContents()
+ val wr = tryParse(text).map { intoFirm(it, pos ?: BlockPos.ORIGIN) }
+ val waypoints = wr.getOrElse {
+ source.sendError(
+ tr("firmament.command.waypoint.import.cw.error",
+ "Could not import ColeWeight waypoints."))
+ Firmament.logger.error(it)
+ return
+ }
+ waypoints.lastRelativeImport = pos
+ Waypoints.waypoints = waypoints
+ source.sendFeedback(positiveFeedback(waypoints.size))
+ }
+
+ @Subscribe
+ fun onEvent(event: CommandEvent.SubCommand) {
+ event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) {
+ thenLiteral("exportcw") {
+ thenExecute {
+ copyAndInform(source, BlockPos.ORIGIN) {
+ tr("firmament.command.waypoint.export.cw",
+ "Copied $it waypoints to clipboard in ColeWeight format.")
+ }
+ }
+ }
+ thenLiteral("exportrelativecw") {
+ thenExecute {
+ copyAndInform(source, MC.player?.blockPos ?: BlockPos.ORIGIN) {
+ tr("firmament.command.waypoint.export.cw.relative",
+ "Copied $it relative waypoints to clipboard in ColeWeight format. Make sure to stand in the same position when importing.")
+ }
+ }
+ }
+ thenLiteral("importcw") {
+ thenExecute {
+ importAndInform(source, null) {
+ tr("firmament.command.waypoint.import.cw.success",
+ "Imported $it waypoints from ColeWeight.")
+ }
+ }
+ }
+ thenLiteral("importrelativecw") {
+ thenExecute {
+ importAndInform(source, MC.player!!.blockPos) {
+ tr("firmament.command.waypoint.import.cw.relative",
+ "Imported $it relative waypoints from clipboard. Make sure you stand in the same position as when you exported these waypoints for them to line up correctly.")
+ }
+ }
+ }
+ }
+ }
+
+ fun tryParse(string: String): Result<List<ColeWeightWaypoint>> {
+ return runCatching {
+ Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(string)
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/FairySouls.kt b/src/main/kotlin/features/world/FairySouls.kt
index 1263074..9191a80 100644
--- a/src/main/kotlin/features/world/FairySouls.kt
+++ b/src/main/kotlin/features/world/FairySouls.kt
@@ -1,131 +1,123 @@
-
-
package moe.nea.firmament.features.world
import io.github.moulberry.repo.data.Coordinate
+import me.shedaniel.math.Color
import kotlinx.serialization.Serializable
import kotlinx.serialization.serializer
-import net.minecraft.text.Text
-import net.minecraft.util.math.BlockPos
-import net.minecraft.util.math.Vec3d
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent
import moe.nea.firmament.events.WorldRenderLastEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.SkyBlockIsland
import moe.nea.firmament.util.blockPos
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
-import moe.nea.firmament.util.render.RenderInWorldContext
import moe.nea.firmament.util.render.RenderInWorldContext.Companion.renderInWorld
import moe.nea.firmament.util.unformattedString
-object FairySouls : FirmamentFeature {
-
-
- @Serializable
- data class Data(
- val foundSouls: MutableMap<SkyBlockIsland, MutableSet<Int>> = mutableMapOf()
- )
-
- override val config: ManagedConfig
- get() = TConfig
-
- object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "found-fairysouls", ::Data)
-
-
- object TConfig : ManagedConfig("fairy-souls", Category.MISC) {
- val displaySouls by toggle("show") { false }
- val resetSouls by button("reset") {
- DConfig.data?.foundSouls?.clear() != null
- updateMissingSouls()
- }
- }
-
-
- override val identifier: String get() = "fairy-souls"
-
- val playerReach = 5
- val playerReachSquared = playerReach * playerReach
-
- var currentLocationName: SkyBlockIsland? = null
- var currentLocationSouls: List<Coordinate> = emptyList()
- var currentMissingSouls: List<Coordinate> = emptyList()
-
- fun updateMissingSouls() {
- currentMissingSouls = emptyList()
- val c = DConfig.data ?: return
- val fi = c.foundSouls[currentLocationName] ?: setOf()
- val cms = currentLocationSouls.toMutableList()
- fi.asSequence().sortedDescending().filter { it in cms.indices }.forEach { cms.removeAt(it) }
- currentMissingSouls = cms
- }
-
- fun updateWorldSouls() {
- currentLocationSouls = emptyList()
- val loc = currentLocationName ?: return
- currentLocationSouls = RepoManager.neuRepo.constants.fairySouls.soulLocations[loc.locrawMode] ?: return
- }
-
- fun findNearestClickableSoul(): Coordinate? {
- val player = MC.player ?: return null
- val pos = player.pos
- val location = SBData.skyblockLocation ?: return null
- val soulLocations: List<Coordinate> =
- RepoManager.neuRepo.constants.fairySouls.soulLocations[location.locrawMode] ?: return null
- return soulLocations
- .map { it to it.blockPos.getSquaredDistance(pos) }
- .filter { it.second < playerReachSquared }
- .minByOrNull { it.second }
- ?.first
- }
-
- private fun markNearestSoul() {
- val nearestSoul = findNearestClickableSoul() ?: return
- val c = DConfig.data ?: return
- val loc = currentLocationName ?: return
- val idx = currentLocationSouls.indexOf(nearestSoul)
- c.foundSouls.computeIfAbsent(loc) { mutableSetOf() }.add(idx)
- DConfig.markDirty()
- updateMissingSouls()
- }
-
- @Subscribe
- fun onWorldRender(it: WorldRenderLastEvent) {
- if (!TConfig.displaySouls) return
- renderInWorld(it) {
- currentMissingSouls.forEach {
- block(it.blockPos, 0x80FFFF00.toInt())
- }
- color(1f, 0f, 1f, 1f)
- currentLocationSouls.forEach {
- wireframeCube(it.blockPos)
- }
- }
- }
-
- @Subscribe
- fun onProcessChat(it: ProcessChatEvent) {
- when (it.text.unformattedString) {
- "You have already found that Fairy Soul!" -> {
- markNearestSoul()
- }
-
- "SOUL! You found a Fairy Soul!" -> {
- markNearestSoul()
- }
- }
- }
-
- @Subscribe
- fun onLocationChange(it: SkyblockServerUpdateEvent) {
- currentLocationName = it.newLocraw?.skyblockLocation
- updateWorldSouls()
- updateMissingSouls()
- }
+object FairySouls {
+
+
+ @Serializable
+ data class Data(
+ val foundSouls: MutableMap<SkyBlockIsland, MutableSet<Int>> = mutableMapOf()
+ )
+
+ @Config
+ object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "found-fairysouls", ::Data)
+
+ @Config
+ object TConfig : ManagedConfig("fairy-souls", Category.MISC) {
+ val displaySouls by toggle("show") { false }
+ val resetSouls by button("reset") {
+ DConfig.data?.foundSouls?.clear() != null
+ updateMissingSouls()
+ }
+ }
+
+
+ val identifier: String get() = "fairy-souls"
+
+ val playerReach = 5
+ val playerReachSquared = playerReach * playerReach
+
+ var currentLocationName: SkyBlockIsland? = null
+ var currentLocationSouls: List<Coordinate> = emptyList()
+ var currentMissingSouls: List<Coordinate> = emptyList()
+
+ fun updateMissingSouls() {
+ currentMissingSouls = emptyList()
+ val c = DConfig.data ?: return
+ val fi = c.foundSouls[currentLocationName] ?: setOf()
+ val cms = currentLocationSouls.toMutableList()
+ fi.asSequence().sortedDescending().filter { it in cms.indices }.forEach { cms.removeAt(it) }
+ currentMissingSouls = cms
+ }
+
+ fun updateWorldSouls() {
+ currentLocationSouls = emptyList()
+ val loc = currentLocationName ?: return
+ currentLocationSouls = RepoManager.neuRepo.constants.fairySouls.soulLocations[loc.locrawMode] ?: return
+ }
+
+ fun findNearestClickableSoul(): Coordinate? {
+ val player = MC.player ?: return null
+ val pos = player.pos
+ val location = SBData.skyblockLocation ?: return null
+ val soulLocations: List<Coordinate> =
+ RepoManager.neuRepo.constants.fairySouls.soulLocations[location.locrawMode] ?: return null
+ return soulLocations
+ .map { it to it.blockPos.getSquaredDistance(pos) }
+ .filter { it.second < playerReachSquared }
+ .minByOrNull { it.second }
+ ?.first
+ }
+
+ private fun markNearestSoul() {
+ val nearestSoul = findNearestClickableSoul() ?: return
+ val c = DConfig.data ?: return
+ val loc = currentLocationName ?: return
+ val idx = currentLocationSouls.indexOf(nearestSoul)
+ c.foundSouls.computeIfAbsent(loc) { mutableSetOf() }.add(idx)
+ DConfig.markDirty()
+ updateMissingSouls()
+ }
+
+ @Subscribe
+ fun onWorldRender(it: WorldRenderLastEvent) {
+ if (!TConfig.displaySouls) return
+ renderInWorld(it) {
+ currentMissingSouls.forEach {
+ block(it.blockPos, Color.ofRGBA(176, 0, 255, 128).color)
+ }
+ currentLocationSouls.forEach {
+ wireframeCube(it.blockPos)
+ }
+ }
+ }
+
+ @Subscribe
+ fun onProcessChat(it: ProcessChatEvent) {
+ when (it.text.unformattedString) {
+ "You have already found that Fairy Soul!" -> {
+ markNearestSoul()
+ }
+
+ "SOUL! You found a Fairy Soul!" -> {
+ markNearestSoul()
+ }
+ }
+ }
+
+ @Subscribe
+ fun onLocationChange(it: SkyblockServerUpdateEvent) {
+ currentLocationName = it.newLocraw?.skyblockLocation
+ updateWorldSouls()
+ updateMissingSouls()
+ }
}
diff --git a/src/main/kotlin/features/world/FirmWaypointManager.kt b/src/main/kotlin/features/world/FirmWaypointManager.kt
new file mode 100644
index 0000000..d18483c
--- /dev/null
+++ b/src/main/kotlin/features/world/FirmWaypointManager.kt
@@ -0,0 +1,168 @@
+package moe.nea.firmament.features.world
+
+import com.mojang.brigadier.arguments.StringArgumentType
+import kotlinx.serialization.serializer
+import net.minecraft.text.Text
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.DefaultSource
+import moe.nea.firmament.commands.RestArgumentType
+import moe.nea.firmament.commands.get
+import moe.nea.firmament.commands.suggestsList
+import moe.nea.firmament.commands.thenArgument
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TemplateUtil
+import moe.nea.firmament.util.data.MultiFileDataHolder
+import moe.nea.firmament.util.tr
+
+object FirmWaypointManager {
+ object DataHolder : MultiFileDataHolder<FirmWaypoints>(serializer(), "waypoints")
+
+ val SHARE_PREFIX = "FIRM_WAYPOINTS/"
+ val ENCODED_SHARE_PREFIX = TemplateUtil.getPrefixComparisonSafeBase64Encoding(SHARE_PREFIX)
+
+ fun createExportableCopy(
+ waypoints: FirmWaypoints,
+ ): FirmWaypoints {
+ val copy = waypoints.copy(waypoints = waypoints.waypoints.toMutableList())
+ if (waypoints.isRelativeTo != null) {
+ val origin = waypoints.lastRelativeImport
+ if (origin != null) {
+ copy.waypoints.replaceAll {
+ it.copy(
+ x = it.x - origin.x,
+ y = it.y - origin.y,
+ z = it.z - origin.z,
+ )
+ }
+ } else {
+ TODO("Add warning!")
+ }
+ }
+ return copy
+ }
+
+ fun loadWaypoints(waypoints: FirmWaypoints, sendFeedback: (Text) -> Unit) {
+ val copy = waypoints.deepCopy()
+ if (copy.isRelativeTo != null) {
+ val origin = MC.player!!.blockPos
+ copy.waypoints.replaceAll {
+ it.copy(
+ x = it.x + origin.x,
+ y = it.y + origin.y,
+ z = it.z + origin.z,
+ )
+ }
+ copy.lastRelativeImport = origin.toImmutable()
+ sendFeedback(tr("firmament.command.waypoint.import.ordered.success",
+ "Imported ${copy.size} relative waypoints. Make sure you stand in the correct spot while loading the waypoints: ${copy.isRelativeTo}."))
+ } else {
+ sendFeedback(tr("firmament.command.waypoint.import.success",
+ "Imported ${copy.size} waypoints."))
+ }
+ Waypoints.waypoints = copy
+ }
+
+ fun setOrigin(source: DefaultSource, text: String?) {
+ val waypoints = Waypoints.useEditableWaypoints()
+ waypoints.isRelativeTo = text ?: waypoints.isRelativeTo ?: ""
+ val pos = MC.player!!.blockPos
+ waypoints.lastRelativeImport = pos
+ source.sendFeedback(tr("firmament.command.waypoint.originset",
+ "Set the origin of waypoints to ${FirmFormatters.formatPosition(pos)}. Run /firm waypoints export to save the waypoints relative to this position."))
+ }
+
+ @Subscribe
+ fun onCommands(event: CommandEvent.SubCommand) {
+ event.subcommand(Waypoints.WAYPOINTS_SUBCOMMAND) {
+ thenLiteral("setorigin") {
+ thenExecute {
+ setOrigin(source, null)
+ }
+ thenArgument("hint", RestArgumentType) { text ->
+ thenExecute {
+ setOrigin(source, this[text])
+ }
+ }
+ }
+ thenLiteral("clearorigin") {
+ thenExecute {
+ val waypoints = Waypoints.useEditableWaypoints()
+ waypoints.lastRelativeImport = null
+ waypoints.isRelativeTo = null
+ source.sendFeedback(tr("firmament.command.waypoint.originunset",
+ "Unset the origin of the waypoints. Run /firm waypoints export to save the waypoints with absolute coordinates."))
+ }
+ }
+ thenLiteral("save") {
+ thenArgument("name", StringArgumentType.string()) { name ->
+ suggestsList { DataHolder.list().keys }
+ thenExecute {
+ val waypoints = Waypoints.useNonEmptyWaypoints()
+ if (waypoints == null) {
+ source.sendError(Waypoints.textNothingToExport())
+ return@thenExecute
+ }
+ waypoints.id = get(name)
+ val exportableWaypoints = createExportableCopy(waypoints)
+ DataHolder.insert(get(name), exportableWaypoints)
+ DataHolder.save()
+ source.sendFeedback(tr("firmament.command.waypoint.saved",
+ "Saved waypoints locally as ${get(name)}. Use /firm waypoints load to load them again."))
+ }
+ }
+ }
+ thenLiteral("load") {
+ thenArgument("name", StringArgumentType.string()) { name ->
+ suggestsList { DataHolder.list().keys }
+ thenExecute {
+ val name = get(name)
+ val waypoints = DataHolder.list()[name]
+ if (waypoints == null) {
+ source.sendError(
+ tr("firmament.command.waypoint.nosaved",
+ "No saved waypoint for ${name}. Use tab completion to see available names."))
+ return@thenExecute
+ }
+ loadWaypoints(waypoints, source::sendFeedback)
+ }
+ }
+ }
+ thenLiteral("export") {
+ thenExecute {
+ val waypoints = Waypoints.useNonEmptyWaypoints()
+ if (waypoints == null) {
+ source.sendError(Waypoints.textNothingToExport())
+ return@thenExecute
+ }
+ val exportableWaypoints = createExportableCopy(waypoints)
+ val data = TemplateUtil.encodeTemplate(SHARE_PREFIX, exportableWaypoints)
+ ClipboardUtils.setTextContent(data)
+ source.sendFeedback(tr("firmament.command.waypoint.export",
+ "Copied ${exportableWaypoints.size} waypoints to clipboard in Firmament format."))
+ }
+ }
+ thenLiteral("import") {
+ thenExecute {
+ val text = ClipboardUtils.getTextContents()
+ if (text.startsWith("[")) {
+ source.sendError(tr("firmament.command.waypoint.import.lookslikecw",
+ "The waypoints in your clipboard look like they might be ColeWeight waypoints. If so, use /firm waypoints importcw or /firm waypoints importrelativecw."))
+ return@thenExecute
+ }
+ val waypoints = TemplateUtil.maybeDecodeTemplate<FirmWaypoints>(SHARE_PREFIX, text)
+ if (waypoints == null) {
+ source.sendError(tr("firmament.command.waypoint.import.error",
+ "Could not import Firmament waypoints from your clipboard. Make sure they are Firmament compatible waypoints."))
+ return@thenExecute
+ }
+ loadWaypoints(waypoints, source::sendFeedback)
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/FirmWaypoints.kt b/src/main/kotlin/features/world/FirmWaypoints.kt
new file mode 100644
index 0000000..d0cd55a
--- /dev/null
+++ b/src/main/kotlin/features/world/FirmWaypoints.kt
@@ -0,0 +1,37 @@
+package moe.nea.firmament.features.world
+
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import net.minecraft.util.math.BlockPos
+
+@Serializable
+data class FirmWaypoints(
+ var label: String,
+ var id: String,
+ /**
+ * A hint to indicate where to stand while loading the waypoints.
+ */
+ var isRelativeTo: String?,
+ var waypoints: MutableList<Waypoint>,
+ var isOrdered: Boolean,
+ // TODO: val resetOnSwap: Boolean,
+) {
+
+ fun deepCopy() = copy(waypoints = waypoints.toMutableList())
+ @Transient
+ var lastRelativeImport: BlockPos? = null
+
+ val size get() = waypoints.size
+ @Serializable
+ data class Waypoint(
+ val x: Int,
+ val y: Int,
+ val z: Int,
+ ) {
+ val blockPos get() = BlockPos(x, y, z)
+
+ companion object {
+ fun from(blockPos: BlockPos) = Waypoint(blockPos.x, blockPos.y, blockPos.z)
+ }
+ }
+}
diff --git a/src/main/kotlin/features/world/TemporaryWaypoints.kt b/src/main/kotlin/features/world/TemporaryWaypoints.kt
new file mode 100644
index 0000000..f5653ad
--- /dev/null
+++ b/src/main/kotlin/features/world/TemporaryWaypoints.kt
@@ -0,0 +1,72 @@
+package moe.nea.firmament.features.world
+
+import me.shedaniel.math.Color
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.text.Text
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.events.WorldRenderLastEvent
+import moe.nea.firmament.features.world.Waypoints.TConfig
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.render.RenderInWorldContext
+
+object TemporaryWaypoints {
+ data class TemporaryWaypoint(
+ val pos: BlockPos,
+ val postedAt: TimeMark,
+ )
+ val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>()
+ val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern()
+ @Subscribe
+ fun onProcessChat(it: ProcessChatEvent) {
+ val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString)
+ if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) {
+ temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint(BlockPos(
+ matcher.group(1).toInt(),
+ matcher.group(2).toInt(),
+ matcher.group(3).toInt(),
+ ), TimeMark.now())
+ }
+ }
+ @Subscribe
+ fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) {
+ temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration }
+ if (temporaryPlayerWaypointList.isEmpty()) return
+ RenderInWorldContext.renderInWorld(event) {
+ temporaryPlayerWaypointList.forEach { (_, waypoint) ->
+ block(waypoint.pos, Color.ofRGBA(255, 255, 0, 128).color)
+ }
+ temporaryPlayerWaypointList.forEach { (player, waypoint) ->
+ val skin =
+ MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player }?.skinTextures?.texture
+ withFacingThePlayer(waypoint.pos.toCenterPos()) {
+ waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player))
+ if (skin != null) {
+ matrixStack.translate(0F, -20F, 0F)
+ // Head front
+ texture(
+ skin, 16, 16,
+ 1 / 8f, 1 / 8f,
+ 2 / 8f, 2 / 8f,
+ )
+ // Head overlay
+ texture(
+ skin, 16, 16,
+ 5 / 8f, 1 / 8f,
+ 6 / 8f, 2 / 8f,
+ )
+ }
+ }
+ }
+ }
+ }
+
+ @Subscribe
+ fun onWorldReady(event: WorldReadyEvent) {
+ temporaryPlayerWaypointList.clear()
+ }
+
+}
diff --git a/src/main/kotlin/features/world/Waypoints.kt b/src/main/kotlin/features/world/Waypoints.kt
index 16db059..318b6c2 100644
--- a/src/main/kotlin/features/world/Waypoints.kt
+++ b/src/main/kotlin/features/world/Waypoints.kt
@@ -2,138 +2,115 @@ package moe.nea.firmament.features.world
import com.mojang.brigadier.arguments.IntegerArgumentType
import me.shedaniel.math.Color
-import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
-import kotlinx.serialization.Serializable
-import kotlin.collections.component1
-import kotlin.collections.component2
-import kotlin.collections.set
import kotlin.time.Duration.Companion.hours
import kotlin.time.Duration.Companion.seconds
import net.minecraft.command.argument.BlockPosArgumentType
-import net.minecraft.server.command.CommandOutput
-import net.minecraft.server.command.ServerCommandSource
import net.minecraft.text.Text
-import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec3d
-import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.commands.get
import moe.nea.firmament.commands.thenArgument
import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.commands.thenLiteral
import moe.nea.firmament.events.CommandEvent
-import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.events.TickEvent
import moe.nea.firmament.events.WorldReadyEvent
import moe.nea.firmament.events.WorldRenderLastEvent
-import moe.nea.firmament.features.FirmamentFeature
-import moe.nea.firmament.gui.config.ManagedConfig
-import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.MC
-import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.mc.asFakeServer
import moe.nea.firmament.util.render.RenderInWorldContext
+import moe.nea.firmament.util.tr
-object Waypoints : FirmamentFeature {
- override val identifier: String
+object Waypoints {
+ val identifier: String
get() = "waypoints"
+ @Config
object TConfig : ManagedConfig(identifier, Category.MINING) { // TODO: add to misc
val tempWaypointDuration by duration("temp-waypoint-duration", 0.seconds, 1.hours) { 30.seconds }
val showIndex by toggle("show-index") { true }
val skipToNearest by toggle("skip-to-nearest") { false }
+ val resetWaypointOrderOnWorldSwap by toggle("reset-order-on-swap") { true }
// TODO: look ahead size
}
- data class TemporaryWaypoint(
- val pos: BlockPos,
- val postedAt: TimeMark,
- )
-
- override val config get() = TConfig
-
- val temporaryPlayerWaypointList = mutableMapOf<String, TemporaryWaypoint>()
- val temporaryPlayerWaypointMatcher = "(?i)x: (-?[0-9]+),? y: (-?[0-9]+),? z: (-?[0-9]+)".toPattern()
-
- val waypoints = mutableListOf<BlockPos>()
- var ordered = false
+ var waypoints: FirmWaypoints? = null
var orderedIndex = 0
- @Serializable
- data class ColeWeightWaypoint(
- val x: Int,
- val y: Int,
- val z: Int,
- val r: Int = 0,
- val g: Int = 0,
- val b: Int = 0,
- )
-
@Subscribe
fun onRenderOrderedWaypoints(event: WorldRenderLastEvent) {
- if (waypoints.isEmpty()) return
+ val w = useNonEmptyWaypoints() ?: return
RenderInWorldContext.renderInWorld(event) {
- if (!ordered) {
- waypoints.withIndex().forEach {
- block(it.value, 0x800050A0.toInt())
- if (TConfig.showIndex)
- withFacingThePlayer(it.value.toCenterPos()) {
- text(Text.literal(it.index.toString()))
- }
+ if (!w.isOrdered) {
+ w.waypoints.withIndex().forEach {
+ block(it.value.blockPos, Color.ofRGBA(0, 80, 160, 128).color)
+ if (TConfig.showIndex) withFacingThePlayer(it.value.blockPos.toCenterPos()) {
+ text(Text.literal(it.index.toString()))
+ }
}
} else {
- orderedIndex %= waypoints.size
+ orderedIndex %= w.waypoints.size
val firstColor = Color.ofRGBA(0, 200, 40, 180)
- color(firstColor)
- tracer(waypoints[orderedIndex].toCenterPos(), lineWidth = 3f)
- waypoints.withIndex().toList()
- .wrappingWindow(orderedIndex, 3)
- .zip(
- listOf(
- firstColor,
- Color.ofRGBA(180, 200, 40, 150),
- Color.ofRGBA(180, 80, 20, 140),
- )
+ tracer(w.waypoints[orderedIndex].blockPos.toCenterPos(), color = firstColor.color, lineWidth = 3f)
+ w.waypoints.withIndex().toList().wrappingWindow(orderedIndex, 3).zip(
+ listOf(
+ firstColor,
+ Color.ofRGBA(180, 200, 40, 150),
+ Color.ofRGBA(180, 80, 20, 140),
)
- .reversed()
- .forEach { (waypoint, col) ->
- val (index, pos) = waypoint
- block(pos, col.color)
- if (TConfig.showIndex)
- withFacingThePlayer(pos.toCenterPos()) {
- text(Text.literal(index.toString()))
- }
+ ).reversed().forEach { (waypoint, col) ->
+ val (index, pos) = waypoint
+ block(pos.blockPos, col.color)
+ if (TConfig.showIndex) withFacingThePlayer(pos.blockPos.toCenterPos()) {
+ text(Text.literal(index.toString()))
}
+ }
}
}
}
@Subscribe
fun onTick(event: TickEvent) {
- if (waypoints.isEmpty() || !ordered) return
- orderedIndex %= waypoints.size
+ val w = useNonEmptyWaypoints() ?: return
+ if (!w.isOrdered) return
+ orderedIndex %= w.waypoints.size
val p = MC.player?.pos ?: return
if (TConfig.skipToNearest) {
orderedIndex =
- (waypoints.withIndex().minBy { it.value.getSquaredDistance(p) }.index + 1) % waypoints.size
+ (w.waypoints.withIndex().minBy { it.value.blockPos.getSquaredDistance(p) }.index + 1) % w.waypoints.size
+
} else {
- if (waypoints[orderedIndex].isWithinDistance(p, 3.0)) {
- orderedIndex = (orderedIndex + 1) % waypoints.size
+ if (w.waypoints[orderedIndex].blockPos.isWithinDistance(p, 3.0)) {
+ orderedIndex = (orderedIndex + 1) % w.waypoints.size
}
}
}
+
+ fun useEditableWaypoints(): FirmWaypoints {
+ var w = waypoints
+ if (w == null) {
+ w = FirmWaypoints("Unlabeled", "unknown", null, mutableListOf(), false)
+ waypoints = w
+ }
+ return w
+ }
+
+ fun useNonEmptyWaypoints(): FirmWaypoints? {
+ val w = waypoints
+ if (w == null) return null
+ if (w.waypoints.isEmpty()) return null
+ return w
+ }
+
+ val WAYPOINTS_SUBCOMMAND = "waypoints"
+
@Subscribe
- fun onProcessChat(it: ProcessChatEvent) {
- val matcher = temporaryPlayerWaypointMatcher.matcher(it.unformattedString)
- if (it.nameHeuristic != null && TConfig.tempWaypointDuration > 0.seconds && matcher.find()) {
- temporaryPlayerWaypointList[it.nameHeuristic] = TemporaryWaypoint(
- BlockPos(
- matcher.group(1).toInt(),
- matcher.group(2).toInt(),
- matcher.group(3).toInt(),
- ),
- TimeMark.now()
- )
+ fun onWorldSwap(event: WorldReadyEvent) {
+ if (TConfig.resetWaypointOrderOnWorldSwap) {
+ orderedIndex = 0
}
}
@@ -142,8 +119,10 @@ object Waypoints : FirmamentFeature {
event.subcommand("waypoint") {
thenArgument("pos", BlockPosArgumentType.blockPos()) { pos ->
thenExecute {
+ source
val position = pos.get(this).toAbsoluteBlockPos(source.asFakeServer())
- waypoints.add(position)
+ val w = useEditableWaypoints()
+ w.waypoints.add(FirmWaypoints.Waypoint.from(position))
source.sendFeedback(
Text.stringifiedTranslatable(
"firmament.command.waypoint.added",
@@ -155,28 +134,72 @@ object Waypoints : FirmamentFeature {
}
}
}
- event.subcommand("waypoints") {
+ event.subcommand(WAYPOINTS_SUBCOMMAND) {
+ thenLiteral("reset") {
+ thenExecute {
+ orderedIndex = 0
+ source.sendFeedback(
+ tr(
+ "firmament.command.waypoint.reset",
+ "Reset your ordered waypoint index back to 0. If you want to delete all waypoints use /firm waypoints clear instead."
+ )
+ )
+ }
+ }
+ thenLiteral("changeindex") {
+ thenArgument("from", IntegerArgumentType.integer(0)) { fromIndex ->
+ thenArgument("to", IntegerArgumentType.integer(0)) { toIndex ->
+ thenExecute {
+ val w = useEditableWaypoints()
+ val toIndex = toIndex.get(this)
+ val fromIndex = fromIndex.get(this)
+ if (fromIndex !in w.waypoints.indices) {
+ source.sendError(textInvalidIndex(fromIndex))
+ return@thenExecute
+ }
+ if (toIndex !in w.waypoints.indices) {
+ source.sendError(textInvalidIndex(toIndex))
+ return@thenExecute
+ }
+ val waypoint = w.waypoints.removeAt(fromIndex)
+ w.waypoints.add(
+ if (toIndex > fromIndex) toIndex - 1
+ else toIndex,
+ waypoint
+ )
+ source.sendFeedback(
+ tr(
+ "firmament.command.waypoint.indexchange",
+ "Moved waypoint from index $fromIndex to $toIndex. Note that this only matters for ordered waypoints."
+ )
+ )
+ }
+ }
+ }
+ }
thenLiteral("clear") {
thenExecute {
- waypoints.clear()
+ waypoints = null
source.sendFeedback(Text.translatable("firmament.command.waypoint.clear"))
}
}
thenLiteral("toggleordered") {
thenExecute {
- ordered = !ordered
- if (ordered) {
+ val w = useEditableWaypoints()
+ w.isOrdered = !w.isOrdered
+ if (w.isOrdered) {
val p = MC.player?.pos ?: Vec3d.ZERO
- orderedIndex =
- waypoints.withIndex().minByOrNull { it.value.getSquaredDistance(p) }?.index ?: 0
+ orderedIndex = // TODO: this should be extracted to a utility method
+ w.waypoints.withIndex().minByOrNull { it.value.blockPos.getSquaredDistance(p) }?.index ?: 0
}
- source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.$ordered"))
+ source.sendFeedback(Text.translatable("firmament.command.waypoint.ordered.toggle.${w.isOrdered}"))
}
}
thenLiteral("skip") {
thenExecute {
- if (ordered && waypoints.isNotEmpty()) {
- orderedIndex = (orderedIndex + 1) % waypoints.size
+ val w = useNonEmptyWaypoints()
+ if (w != null && w.isOrdered) {
+ orderedIndex = (orderedIndex + 1) % w.size
source.sendFeedback(Text.translatable("firmament.command.waypoint.skip"))
} else {
source.sendError(Text.translatable("firmament.command.waypoint.skip.error"))
@@ -187,79 +210,35 @@ object Waypoints : FirmamentFeature {
thenArgument("index", IntegerArgumentType.integer(0)) { indexArg ->
thenExecute {
val index = get(indexArg)
- if (index in waypoints.indices) {
- waypoints.removeAt(index)
- source.sendFeedback(Text.stringifiedTranslatable(
- "firmament.command.waypoint.remove",
- index))
+ val w = useNonEmptyWaypoints()
+ if (w != null && index in w.waypoints.indices) {
+ w.waypoints.removeAt(index)
+ source.sendFeedback(
+ Text.stringifiedTranslatable(
+ "firmament.command.waypoint.remove",
+ index
+ )
+ )
} else {
source.sendError(Text.stringifiedTranslatable("firmament.command.waypoint.remove.error"))
}
}
}
}
- thenLiteral("import") {
- thenExecute {
- val contents = ClipboardUtils.getTextContents()
- val data = try {
- Firmament.json.decodeFromString<List<ColeWeightWaypoint>>(contents)
- } catch (ex: Exception) {
- Firmament.logger.error("Could not load waypoints from clipboard", ex)
- source.sendError(Text.translatable("firmament.command.waypoint.import.error"))
- return@thenExecute
- }
- waypoints.clear()
- data.mapTo(waypoints) { BlockPos(it.x, it.y, it.z) }
- source.sendFeedback(
- Text.stringifiedTranslatable(
- "firmament.command.waypoint.import",
- data.size
- )
- )
- }
- }
- }
- }
-
- @Subscribe
- fun onRenderTemporaryWaypoints(event: WorldRenderLastEvent) {
- temporaryPlayerWaypointList.entries.removeIf { it.value.postedAt.passedTime() > TConfig.tempWaypointDuration }
- if (temporaryPlayerWaypointList.isEmpty()) return
- RenderInWorldContext.renderInWorld(event) {
- temporaryPlayerWaypointList.forEach { (player, waypoint) ->
- block(waypoint.pos, 0xFFFFFF00.toInt())
- }
- temporaryPlayerWaypointList.forEach { (player, waypoint) ->
- val skin =
- MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player }
- ?.skinTextures
- ?.texture
- withFacingThePlayer(waypoint.pos.toCenterPos()) {
- waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player))
- if (skin != null) {
- matrixStack.translate(0F, -20F, 0F)
- // Head front
- texture(
- skin, 16, 16,
- 1 / 8f, 1 / 8f,
- 2 / 8f, 2 / 8f,
- )
- // Head overlay
- texture(
- skin, 16, 16,
- 5 / 8f, 1 / 8f,
- 6 / 8f, 2 / 8f,
- )
- }
- }
- }
}
}
- @Subscribe
- fun onWorldReady(event: WorldReadyEvent) {
- temporaryPlayerWaypointList.clear()
- }
+ fun textInvalidIndex(index: Int) =
+ tr(
+ "firmament.command.waypoint.invalid-index",
+ "Invalid index $index provided."
+ )
+
+ fun textNothingToExport(): Text =
+ tr(
+ "firmament.command.waypoint.export.nowaypoints",
+ "No waypoints to export found. Add some with /firm waypoint ~ ~ ~."
+ )
}
fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> {
@@ -272,35 +251,3 @@ fun <E> List<E>.wrappingWindow(startIndex: Int, windowSize: Int): List<E> {
}
return result
}
-
-
-fun FabricClientCommandSource.asFakeServer(): ServerCommandSource {
- val source = this
- return ServerCommandSource(
- object : CommandOutput {
- override fun sendMessage(message: Text?) {
- source.player.sendMessage(message, false)
- }
-
- override fun shouldReceiveFeedback(): Boolean {
- return true
- }
-
- override fun shouldTrackOutput(): Boolean {
- return true
- }
-
- override fun shouldBroadcastConsoleToOps(): Boolean {
- return true
- }
- },
- source.position,
- source.rotation,
- null,
- 0,
- "FakeServerCommandSource",
- Text.literal("FakeServerCommandSource"),
- null,
- source.player
- )
-}
diff --git a/src/main/kotlin/gui/BarComponent.kt b/src/main/kotlin/gui/BarComponent.kt
index b82c666..b144e0d 100644
--- a/src/main/kotlin/gui/BarComponent.kt
+++ b/src/main/kotlin/gui/BarComponent.kt
@@ -1,15 +1,13 @@
package moe.nea.firmament.gui
-import com.mojang.blaze3d.systems.RenderSystem
import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
-import io.github.notenoughupdates.moulconfig.common.RenderContext
import io.github.notenoughupdates.moulconfig.gui.GuiComponent
import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
import io.github.notenoughupdates.moulconfig.observer.GetSetter
-import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigRenderContext
import me.shedaniel.math.Color
+import net.minecraft.client.gl.RenderPipelines
import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.render.RenderLayer
import net.minecraft.util.Identifier
import moe.nea.firmament.Firmament
@@ -33,7 +31,7 @@ class BarComponent(
) {
fun draw(context: DrawContext, x: Int, y: Int, width: Int, height: Int, color: Color) {
context.drawTexturedQuad(
- RenderLayer::getGuiTextured,
+ RenderPipelines.GUI_TEXTURED,
identifier,
x, y, x + width, x + height,
u1, u2, v1, v2,
@@ -81,7 +79,7 @@ class BarComponent(
}
override fun render(context: GuiImmediateContext) {
- val renderContext = (context.renderContext as ModernRenderContext).drawContext
+ val renderContext = (context.renderContext as MoulConfigRenderContext).drawContext
var i = 0
val x = 0
val y = 0
@@ -104,7 +102,6 @@ class BarComponent(
(context.width - 4) * total.get() / context.width,
total.get()
)
- RenderSystem.setShaderColor(1F, 1F, 1F, 1F)
}
@@ -113,11 +110,3 @@ class BarComponent(
fun Identifier.toMoulConfig(): MyResourceLocation {
return MyResourceLocation(this.namespace, this.path)
}
-
-fun RenderContext.color(color: Color) {
- color(color.red, color.green, color.blue, color.alpha)
-}
-
-fun RenderContext.color(red: Int, green: Int, blue: Int, alpha: Int) {
- color(red / 255f, green / 255f, blue / 255f, alpha / 255f)
-}
diff --git a/src/main/kotlin/gui/CheckboxComponent.kt b/src/main/kotlin/gui/CheckboxComponent.kt
new file mode 100644
index 0000000..4d29a96
--- /dev/null
+++ b/src/main/kotlin/gui/CheckboxComponent.kt
@@ -0,0 +1,57 @@
+package moe.nea.firmament.gui
+
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+import io.github.notenoughupdates.moulconfig.gui.MouseEvent
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigRenderContext
+import net.minecraft.client.gl.RenderPipelines
+import moe.nea.firmament.Firmament
+
+class CheckboxComponent<T>(
+ val state: GetSetter<T>,
+ val value: T,
+) : GuiComponent() {
+ override fun getWidth(): Int {
+ return 16
+ }
+
+ override fun getHeight(): Int {
+ return 16
+ }
+
+ fun isEnabled(): Boolean {
+ return state.get() == value
+ }
+
+ override fun render(context: GuiImmediateContext) {
+ val ctx = (context.renderContext as MoulConfigRenderContext).drawContext
+ ctx.drawGuiTexture(
+ RenderPipelines.GUI_TEXTURED,
+ if (isEnabled()) Firmament.identifier("widget/checkbox_checked")
+ else Firmament.identifier("widget/checkbox_unchecked"),
+ 0, 0,
+ 16, 16
+ )
+ }
+
+ var isClicking = false
+
+ override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean {
+ if (mouseEvent is MouseEvent.Click) {
+ if (isClicking && !mouseEvent.mouseState && mouseEvent.mouseButton == 0) {
+ isClicking = false
+ if (context.isHovered)
+ state.set(value)
+ blur()
+ return true
+ }
+ if (mouseEvent.mouseState && mouseEvent.mouseButton == 0 && context.isHovered) {
+ requestFocus()
+ isClicking = true
+ return true
+ }
+ }
+ return false
+ }
+}
diff --git a/src/main/kotlin/gui/FirmButtonComponent.kt b/src/main/kotlin/gui/FirmButtonComponent.kt
index 82e5b05..1469c09 100644
--- a/src/main/kotlin/gui/FirmButtonComponent.kt
+++ b/src/main/kotlin/gui/FirmButtonComponent.kt
@@ -1,4 +1,3 @@
-
package moe.nea.firmament.gui
import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
@@ -11,71 +10,78 @@ import io.github.notenoughupdates.moulconfig.observer.GetSetter
open class FirmButtonComponent(
- child: GuiComponent,
- val isEnabled: GetSetter<Boolean> = GetSetter.constant(true),
- val noBackground: Boolean = false,
- val action: Runnable,
+ child: GuiComponent,
+ val isEnabled: GetSetter<Boolean> = GetSetter.constant(true),
+ val noBackground: Boolean = false,
+ val action: (mouseButton: Int) -> Unit,
) : PanelComponent(child, if (noBackground) 0 else 2, DefaultBackgroundRenderer.TRANSPARENT) {
- /* TODO: make use of vanillas built in nine slicer */
- val hoveredBg =
- NinePatch.builder(MyResourceLocation("minecraft", "textures/gui/sprites/widget/button_highlighted.png"))
- .cornerSize(5)
- .cornerUv(5 / 200F, 5 / 20F)
- .mode(NinePatch.Mode.STRETCHING)
- .build()
- val unhoveredBg = NinePatch.builder(MyResourceLocation("minecraft", "textures/gui/sprites/widget/button.png"))
- .cornerSize(5)
- .cornerUv(5 / 200F, 5 / 20F)
- .mode(NinePatch.Mode.STRETCHING)
- .build()
- val disabledBg =
- NinePatch.builder(MyResourceLocation("minecraft", "textures/gui/sprites/widget/button_disabled.png"))
- .cornerSize(5)
- .cornerUv(5 / 200F, 5 / 20F)
- .mode(NinePatch.Mode.STRETCHING)
- .build()
- val activeBg = NinePatch.builder(MyResourceLocation("firmament", "textures/gui/sprites/widget/button_active.png"))
- .cornerSize(5)
- .cornerUv(5 / 200F, 5 / 20F)
- .mode(NinePatch.Mode.STRETCHING)
- .build()
- var isClicking = false
- override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean {
- if (!isEnabled.get()) return false
- if (isClicking) {
- if (mouseEvent is MouseEvent.Click && !mouseEvent.mouseState && mouseEvent.mouseButton == 0) {
- isClicking = false
- if (context.isHovered) {
- action.run()
- }
- return true
- }
- }
- if (!context.isHovered) return false
- if (mouseEvent !is MouseEvent.Click) return false
- if (mouseEvent.mouseState && mouseEvent.mouseButton == 0) {
- requestFocus()
- isClicking = true
- return true
- }
- return false
- }
+ constructor(
+ child: GuiComponent,
+ isEnabled: GetSetter<Boolean> = GetSetter.constant(true),
+ noBackground: Boolean = false,
+ action: Runnable,
+ ) : this(child, isEnabled, noBackground, { action.run() })
+
+ /* TODO: make use of vanillas built in nine slicer */
+ val hoveredBg =
+ NinePatch.builder(MyResourceLocation("minecraft", "textures/gui/sprites/widget/button_highlighted.png"))
+ .cornerSize(5)
+ .cornerUv(5 / 200F, 5 / 20F)
+ .mode(NinePatch.Mode.STRETCHING)
+ .build()
+ val unhoveredBg = NinePatch.builder(MyResourceLocation("minecraft", "textures/gui/sprites/widget/button.png"))
+ .cornerSize(5)
+ .cornerUv(5 / 200F, 5 / 20F)
+ .mode(NinePatch.Mode.STRETCHING)
+ .build()
+ val disabledBg =
+ NinePatch.builder(MyResourceLocation("minecraft", "textures/gui/sprites/widget/button_disabled.png"))
+ .cornerSize(5)
+ .cornerUv(5 / 200F, 5 / 20F)
+ .mode(NinePatch.Mode.STRETCHING)
+ .build()
+ val activeBg = NinePatch.builder(MyResourceLocation("firmament", "textures/gui/sprites/widget/button_active.png"))
+ .cornerSize(5)
+ .cornerUv(5 / 200F, 5 / 20F)
+ .mode(NinePatch.Mode.STRETCHING)
+ .build()
+ var isClicking = false
+ override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean {
+ if (!isEnabled.get()) return false
+ if (isClicking) {
+ if (mouseEvent is MouseEvent.Click && !mouseEvent.mouseState) {
+ isClicking = false
+ if (context.isHovered) {
+ action.invoke(mouseEvent.mouseButton)
+ }
+ return true
+ }
+ }
+ if (!context.isHovered) return false
+ if (mouseEvent !is MouseEvent.Click) return false
+ if (mouseEvent.mouseState) {
+ requestFocus()
+ isClicking = true
+ return true
+ }
+ return false
+ }
- open fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> =
- if (!isEnabled.get()) disabledBg
- else if (context.isHovered || isClicking) hoveredBg
- else unhoveredBg
+ open fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> =
+ if (!isEnabled.get()) disabledBg
+ else if (context.isHovered || isClicking) hoveredBg
+ else unhoveredBg
- override fun render(context: GuiImmediateContext) {
- context.renderContext.pushMatrix()
- if (!noBackground)
- context.renderContext.drawNinePatch(
- getBackground(context),
- 0f, 0f, context.width, context.height
- )
- context.renderContext.translate(insets.toFloat(), insets.toFloat(), 0f)
- element.render(getChildContext(context))
- context.renderContext.popMatrix()
- }
+ override fun render(context: GuiImmediateContext) {
+ context.renderContext.pushMatrix()
+ if (!noBackground)
+ context.renderContext.drawNinePatch(
+ getBackground(context),
+ 0f, 0f, context.width, context.height
+ )
+ context.renderContext.translate(insets.toFloat(), insets.toFloat())
+ element.render(getChildContext(context))
+ context.renderContext.popMatrix()
+ }
}
diff --git a/src/main/kotlin/gui/FirmHoverComponent.kt b/src/main/kotlin/gui/FirmHoverComponent.kt
index b1792ce..eed795a 100644
--- a/src/main/kotlin/gui/FirmHoverComponent.kt
+++ b/src/main/kotlin/gui/FirmHoverComponent.kt
@@ -1,5 +1,6 @@
package moe.nea.firmament.gui
+import io.github.notenoughupdates.moulconfig.common.text.StructuredText
import io.github.notenoughupdates.moulconfig.gui.GuiComponent
import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent
@@ -10,50 +11,51 @@ import kotlin.time.Duration
import moe.nea.firmament.util.TimeMark
class FirmHoverComponent(
- val child: GuiComponent,
- val hoverLines: Supplier<List<String>>,
- val hoverDelay: Duration,
+ val child: GuiComponent,
+ val hoverLines: Supplier<List<String>>,
+ val hoverDelay: Duration,
) : GuiComponent() {
- override fun getWidth(): Int {
- return child.width
- }
-
- override fun getHeight(): Int {
- return child.height
- }
-
- override fun <T : Any?> foldChildren(
- initial: T,
- visitor: BiFunction<GuiComponent, T, T>
- ): T {
- return visitor.apply(child, initial)
- }
-
- override fun render(context: GuiImmediateContext) {
- if (context.isHovered && (permaHover || lastMouseMove.passedTime() > hoverDelay)) {
- context.renderContext.scheduleDrawTooltip(hoverLines.get())
- permaHover = true
- } else {
- permaHover = false
- }
- if (!context.isHovered) {
- lastMouseMove = TimeMark.now()
- }
- child.render(context)
-
- }
-
- var permaHover = false
- var lastMouseMove = TimeMark.farPast()
-
- override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean {
- if (mouseEvent is MouseEvent.Move) {
- lastMouseMove = TimeMark.now()
- }
- return child.mouseEvent(mouseEvent, context)
- }
-
- override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
- return child.keyboardEvent(event, context)
- }
+ override fun getWidth(): Int {
+ return child.width
+ }
+
+ override fun getHeight(): Int {
+ return child.height
+ }
+
+ override fun <T : Any?> foldChildren(
+ initial: T,
+ visitor: BiFunction<GuiComponent, T, T>
+ ): T {
+ return visitor.apply(child, initial)
+ }
+
+ override fun render(context: GuiImmediateContext) {
+ if (context.isHovered && (permaHover || lastMouseMove.passedTime() > hoverDelay)) {
+ context.renderContext.scheduleDrawTooltip(context.mouseX, context.mouseY, hoverLines.get()
+ .map { it -> StructuredText.of(it) })
+ permaHover = true
+ } else {
+ permaHover = false
+ }
+ if (!context.isHovered) {
+ lastMouseMove = TimeMark.now()
+ }
+ child.render(context)
+
+ }
+
+ var permaHover = false
+ var lastMouseMove = TimeMark.farPast()
+
+ override fun mouseEvent(mouseEvent: MouseEvent, context: GuiImmediateContext): Boolean {
+ if (mouseEvent is MouseEvent.Move) {
+ lastMouseMove = TimeMark.now()
+ }
+ return child.mouseEvent(mouseEvent, context)
+ }
+
+ override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
+ return child.keyboardEvent(event, context)
+ }
}
diff --git a/src/main/kotlin/gui/ImageComponent.kt b/src/main/kotlin/gui/ImageComponent.kt
index bba7dee..695c0ed 100644
--- a/src/main/kotlin/gui/ImageComponent.kt
+++ b/src/main/kotlin/gui/ImageComponent.kt
@@ -6,28 +6,30 @@ import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
import java.util.function.Supplier
class ImageComponent(
- private val width: Int,
- private val height: Int,
- val resourceLocation: Supplier<MyResourceLocation>,
- val u1: Float,
- val u2: Float,
- val v1: Float,
- val v2: Float,
+ private val width: Int,
+ private val height: Int,
+ val resourceLocation: Supplier<MyResourceLocation>,
+ val u1: Float,
+ val u2: Float,
+ val v1: Float,
+ val v2: Float,
) : GuiComponent() {
- override fun getWidth(): Int {
- return width
- }
+ override fun getWidth(): Int {
+ return width
+ }
- override fun getHeight(): Int {
- return height
- }
+ override fun getHeight(): Int {
+ return height
+ }
- override fun render(context: GuiImmediateContext) {
- context.renderContext.bindTexture(resourceLocation.get())
- context.renderContext.drawTexturedRect(
- 0f, 0f,
- context.width.toFloat(), context.height.toFloat(),
- u1, v1, u2, v2
- )
- }
+ override fun render(context: GuiImmediateContext) {
+ context.renderContext.drawComplexTexture(
+ resourceLocation.get(),
+ 0f, 0f,
+ context.width.toFloat(), context.height.toFloat(),
+ {
+ it.uv(u1, v1, u2, v2)
+ }
+ )
+ }
}
diff --git a/src/main/kotlin/gui/config/AllConfigsGui.kt b/src/main/kotlin/gui/config/AllConfigsGui.kt
index 73ff444..345269d 100644
--- a/src/main/kotlin/gui/config/AllConfigsGui.kt
+++ b/src/main/kotlin/gui/config/AllConfigsGui.kt
@@ -4,9 +4,17 @@ import io.github.notenoughupdates.moulconfig.observer.ObservableList
import io.github.notenoughupdates.moulconfig.xml.Bind
import net.minecraft.client.gui.screen.Screen
import net.minecraft.text.Text
+import moe.nea.firmament.annotations.Subscribe
+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.util.MC
import moe.nea.firmament.util.MoulConfigUtils
import moe.nea.firmament.util.ScreenUtil.setScreenLater
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
object AllConfigsGui {
//
@@ -15,9 +23,11 @@ object AllConfigsGui {
// RepoManager.Config
// ) + FeatureManager.allFeatures.mapNotNull { it.config }
+ @Config
object ConfigConfig : ManagedConfig("configconfig", Category.META) {
val enableYacl by toggle("enable-yacl") { false }
val enableMoulConfig by toggle("enable-moulconfig") { true }
+ val enableWideMC by toggle("wide-moulconfig") { false }
}
fun <T> List<T>.toObservableList(): ObservableList<T> = ObservableList(this)
@@ -66,7 +76,7 @@ object AllConfigsGui {
return MoulConfigUtils.loadScreen("config/main", CategoryView(), parent)
}
- fun makeScreen(parent: Screen? = null): Screen {
+ fun makeScreen(search: String? = null, parent: Screen? = null): Screen {
val wantedKey = when {
ConfigConfig.enableMoulConfig -> "moulconfig"
ConfigConfig.enableYacl -> "yacl"
@@ -74,10 +84,23 @@ object AllConfigsGui {
}
val provider = FirmamentConfigScreenProvider.providers.find { it.key == wantedKey }
?: FirmamentConfigScreenProvider.providers.first()
- return provider.open(parent)
+ return provider.open(search, parent)
}
fun showAllGuis() {
setScreenLater(makeScreen())
}
+
+ @Subscribe
+ fun registerCommands(event: CommandEvent.SubCommand) {
+ event.subcommand("search") {
+ thenArgument("search", RestArgumentType) { search ->
+ thenExecute {
+ val search = this[search]
+ setScreenLater(makeScreen(search = search))
+ }
+ }
+ }
+ }
+
}
diff --git a/src/main/kotlin/gui/config/BooleanHandler.kt b/src/main/kotlin/gui/config/BooleanHandler.kt
index 8592777..b954401 100644
--- a/src/main/kotlin/gui/config/BooleanHandler.kt
+++ b/src/main/kotlin/gui/config/BooleanHandler.kt
@@ -9,6 +9,7 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.boolean
import kotlinx.serialization.json.jsonPrimitive
+import moe.nea.firmament.util.data.ManagedConfig
class BooleanHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<Boolean> {
override fun toJson(element: Boolean): JsonElement? {
@@ -29,7 +30,7 @@ class BooleanHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<Bo
override fun set(newValue: Boolean) {
opt.set(newValue)
- config.save()
+ config.markDirty()
}
}, 200)
))
diff --git a/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt b/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt
index 19e7383..8ecdfa2 100644
--- a/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt
+++ b/src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt
@@ -8,7 +8,7 @@ class BuiltInConfigScreenProvider : FirmamentConfigScreenProvider {
override val key: String
get() = "builtin"
- override fun open(parent: Screen?): Screen {
+ override fun open(search: String?, parent: Screen?): Screen {
return AllConfigsGui.makeBuiltInScreen(parent)
}
}
diff --git a/src/main/kotlin/gui/config/ChoiceHandler.kt b/src/main/kotlin/gui/config/ChoiceHandler.kt
new file mode 100644
index 0000000..321b40d
--- /dev/null
+++ b/src/main/kotlin/gui/config/ChoiceHandler.kt
@@ -0,0 +1,49 @@
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.gui.HorizontalAlign
+import io.github.notenoughupdates.moulconfig.gui.VerticalAlign
+import io.github.notenoughupdates.moulconfig.gui.component.AlignComponent
+import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import kotlinx.serialization.json.JsonElement
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.util.StringIdentifiable
+import moe.nea.firmament.gui.CheckboxComponent
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.data.ManagedConfig
+import moe.nea.firmament.util.json.KJsonOps
+
+class ChoiceHandler<E>(
+ val enumClass: Class<E>,
+ val universe: List<E>,
+) : ManagedConfig.OptionHandler<E> where E : Enum<E>, E : StringIdentifiable {
+ val codec = StringIdentifiable.createCodec {
+ @Suppress("UNCHECKED_CAST", "PLATFORM_CLASS_MAPPED_TO_KOTLIN")
+ (universe as java.util.List<*>).toArray(arrayOfNulls<Enum<E>>(0)) as Array<E>
+ }
+ val renderer = EnumRenderer.default<E>()
+
+ override fun toJson(element: E): JsonElement? {
+ return codec.encodeStart(KJsonOps.INSTANCE, element)
+ .promotePartial { ErrorUtil.softError("Failed to encode json element '$element': $it") }.result()
+ .getOrNull()
+ }
+
+ override fun fromJson(element: JsonElement): E {
+ return codec.decode(KJsonOps.INSTANCE, element)
+ .promotePartial { ErrorUtil.softError("Failed to decode json element '$element': $it") }
+ .result()
+ .get()
+ .first
+ }
+
+ override fun emitGuiElements(opt: ManagedOption<E>, guiAppender: GuiAppender) {
+ guiAppender.appendFullRow(TextComponent(opt.labelText.string))
+ for (e in universe) {
+ guiAppender.appendFullRow(RowComponent(
+ AlignComponent(CheckboxComponent(opt, e), { HorizontalAlign.LEFT }, { VerticalAlign.CENTER }),
+ TextComponent(renderer.getName(opt, e).string)
+ ))
+ }
+ }
+}
diff --git a/src/main/kotlin/gui/config/ClickHandler.kt b/src/main/kotlin/gui/config/ClickHandler.kt
index fa1c621..9ea83aa 100644
--- a/src/main/kotlin/gui/config/ClickHandler.kt
+++ b/src/main/kotlin/gui/config/ClickHandler.kt
@@ -5,6 +5,7 @@ package moe.nea.firmament.gui.config
import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
import kotlinx.serialization.json.JsonElement
import moe.nea.firmament.gui.FirmButtonComponent
+import moe.nea.firmament.util.data.ManagedConfig
class ClickHandler(val config: ManagedConfig, val runnable: () -> Unit) : ManagedConfig.OptionHandler<Unit> {
override fun toJson(element: Unit): JsonElement? {
diff --git a/src/main/kotlin/gui/config/ColourHandler.kt b/src/main/kotlin/gui/config/ColourHandler.kt
new file mode 100644
index 0000000..33daa6d
--- /dev/null
+++ b/src/main/kotlin/gui/config/ColourHandler.kt
@@ -0,0 +1,83 @@
+package moe.nea.firmament.gui.config
+
+import io.github.notenoughupdates.moulconfig.ChromaColour
+import io.github.notenoughupdates.moulconfig.gui.component.ColorSelectComponent
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.Json
+import kotlinx.serialization.json.JsonElement
+import moe.nea.firmament.util.data.ManagedConfig
+
+class ColourHandler(val config: ManagedConfig) :
+ ManagedConfig.OptionHandler<ChromaColour> {
+ @Serializable
+ data class ChromaDelegate(
+ @SerialName("h")
+ val hue: Float,
+ @SerialName("s")
+ val saturation: Float,
+ @SerialName("b")
+ val brightness: Float,
+ @SerialName("a")
+ val alpha: Int,
+ @SerialName("c")
+ val timeForFullRotationInMillis: Int,
+ ) {
+ constructor(delegate: ChromaColour) : this(
+ delegate.hue,
+ delegate.saturation,
+ delegate.brightness,
+ delegate.alpha,
+ delegate.timeForFullRotationInMillis
+ )
+
+ fun into(): ChromaColour = ChromaColour(hue, saturation, brightness, timeForFullRotationInMillis, alpha)
+ }
+
+ object ChromaSerializer : KSerializer<ChromaColour> {
+ override val descriptor: SerialDescriptor
+ get() = SerialDescriptor("FirmChromaColour", ChromaDelegate.serializer().descriptor)
+
+ override fun serialize(
+ encoder: Encoder,
+ value: ChromaColour
+ ) {
+ encoder.encodeSerializableValue(ChromaDelegate.serializer(), ChromaDelegate(value))
+ }
+
+ override fun deserialize(decoder: Decoder): ChromaColour {
+ return decoder.decodeSerializableValue(ChromaDelegate.serializer()).into()
+ }
+ }
+
+ override fun toJson(element: ChromaColour): JsonElement? {
+ return Json.encodeToJsonElement(ChromaSerializer, element)
+ }
+
+ override fun fromJson(element: JsonElement): ChromaColour {
+ return Json.decodeFromJsonElement(ChromaSerializer, element)
+ }
+
+ override fun emitGuiElements(
+ opt: ManagedOption<ChromaColour>,
+ guiAppender: GuiAppender
+ ) {
+ guiAppender.appendLabeledRow(
+ opt.labelText,
+ ColorSelectComponent(
+ 0,
+ 0,
+ opt.value.toLegacyString(),
+ {
+ opt.value = ChromaColour.forLegacyString(it)
+ config.markDirty()
+ },
+ { }
+ )
+ )
+ }
+}
diff --git a/src/main/kotlin/gui/config/DurationHandler.kt b/src/main/kotlin/gui/config/DurationHandler.kt
index 8d485b1..0fc945f 100644
--- a/src/main/kotlin/gui/config/DurationHandler.kt
+++ b/src/main/kotlin/gui/config/DurationHandler.kt
@@ -3,6 +3,7 @@
package moe.nea.firmament.gui.config
import io.github.notenoughupdates.moulconfig.common.IMinecraft
+import io.github.notenoughupdates.moulconfig.common.text.StructuredText
import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
import io.github.notenoughupdates.moulconfig.gui.component.SliderComponent
import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
@@ -14,8 +15,8 @@ import kotlinx.serialization.json.long
import kotlin.time.Duration
import kotlin.time.DurationUnit
import kotlin.time.toDuration
-import net.minecraft.text.Text
import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.data.ManagedConfig
class DurationHandler(val config: ManagedConfig, val min: Duration, val max: Duration) :
ManagedConfig.OptionHandler<Duration> {
@@ -31,8 +32,8 @@ class DurationHandler(val config: ManagedConfig, val min: Duration, val max: Dur
guiAppender.appendLabeledRow(
opt.labelText,
RowComponent(
- TextComponent(IMinecraft.instance.defaultFontRenderer,
- { FirmFormatters.formatTimespan(opt.value) },
+ TextComponent(IMinecraft.INSTANCE.defaultFontRenderer,
+ { StructuredText.of(FirmFormatters.formatTimespan(opt.value)) },
40,
TextComponent.TextAlignment.CENTER,
true,
diff --git a/src/main/kotlin/gui/config/EnumRenderer.kt b/src/main/kotlin/gui/config/EnumRenderer.kt
new file mode 100644
index 0000000..3b80b7e
--- /dev/null
+++ b/src/main/kotlin/gui/config/EnumRenderer.kt
@@ -0,0 +1,15 @@
+package moe.nea.firmament.gui.config
+
+import net.minecraft.text.Text
+
+interface EnumRenderer<E : Any> {
+ fun getName(option: ManagedOption<E>, value: E): Text
+
+ companion object {
+ fun <E : Enum<E>> default() = object : EnumRenderer<E> {
+ override fun getName(option: ManagedOption<E>, value: E): Text {
+ return Text.translatable(option.rawLabelText + ".choice." + value.name.lowercase())
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt b/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt
index faad1cc..8700ffa 100644
--- a/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt
+++ b/src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt
@@ -7,7 +7,7 @@ interface FirmamentConfigScreenProvider {
val key: String
val isEnabled: Boolean get() = true
- fun open(parent: Screen?): Screen
+ fun open(search: String?, parent: Screen?): Screen
companion object : CompatLoader<FirmamentConfigScreenProvider>(FirmamentConfigScreenProvider::class) {
val providers by lazy {
diff --git a/src/main/kotlin/gui/config/HudMetaHandler.kt b/src/main/kotlin/gui/config/HudMetaHandler.kt
index a9659ee..fae827d 100644
--- a/src/main/kotlin/gui/config/HudMetaHandler.kt
+++ b/src/main/kotlin/gui/config/HudMetaHandler.kt
@@ -8,18 +8,26 @@ import kotlinx.serialization.json.encodeToJsonElement
import net.minecraft.client.gui.screen.Screen
import net.minecraft.text.MutableText
import net.minecraft.text.Text
+import moe.nea.firmament.Firmament
import moe.nea.firmament.gui.FirmButtonComponent
import moe.nea.firmament.jarvis.JarvisIntegration
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.data.ManagedConfig
-class HudMetaHandler(val config: ManagedConfig, val label: MutableText, val width: Int, val height: Int) :
+class HudMetaHandler(
+ val config: ManagedConfig,
+ val propertyName: String,
+ val label: MutableText,
+ val width: Int,
+ val height: Int
+) :
ManagedConfig.OptionHandler<HudMeta> {
override fun toJson(element: HudMeta): JsonElement? {
return Json.encodeToJsonElement(element.position)
}
override fun fromJson(element: JsonElement): HudMeta {
- return HudMeta(Json.decodeFromJsonElement(element), label, width, height)
+ return HudMeta(Json.decodeFromJsonElement(element), Firmament.identifier(propertyName), label, width, height)
}
fun openEditor(option: ManagedOption<HudMeta>, oldScreen: Screen) {
@@ -34,7 +42,8 @@ class HudMetaHandler(val config: ManagedConfig, val label: MutableText, val widt
opt.labelText,
FirmButtonComponent(
TextComponent(
- Text.stringifiedTranslatable("firmament.hud.edit", label).string),
+ Text.stringifiedTranslatable("firmament.hud.edit", label).string
+ ),
) {
openEditor(opt, guiAppender.screenAccessor())
})
diff --git a/src/main/kotlin/gui/config/IntegerHandler.kt b/src/main/kotlin/gui/config/IntegerHandler.kt
index 31ce90f..ab0237a 100644
--- a/src/main/kotlin/gui/config/IntegerHandler.kt
+++ b/src/main/kotlin/gui/config/IntegerHandler.kt
@@ -3,6 +3,7 @@
package moe.nea.firmament.gui.config
import io.github.notenoughupdates.moulconfig.common.IMinecraft
+import io.github.notenoughupdates.moulconfig.common.text.StructuredText
import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
import io.github.notenoughupdates.moulconfig.gui.component.SliderComponent
import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
@@ -12,6 +13,7 @@ import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.int
import kotlinx.serialization.json.jsonPrimitive
import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.data.ManagedConfig
class IntegerHandler(val config: ManagedConfig, val min: Int, val max: Int) : ManagedConfig.OptionHandler<Int> {
override fun toJson(element: Int): JsonElement? {
@@ -26,8 +28,8 @@ class IntegerHandler(val config: ManagedConfig, val min: Int, val max: Int) : Ma
guiAppender.appendLabeledRow(
opt.labelText,
RowComponent(
- TextComponent(IMinecraft.instance.defaultFontRenderer,
- { FirmFormatters.formatCommas(opt.value, 0) },
+ TextComponent(IMinecraft.INSTANCE.defaultFontRenderer,
+ { StructuredText.of(FirmFormatters.formatCommas(opt.value, 0)) },
40,
TextComponent.TextAlignment.CENTER,
true,
diff --git a/src/main/kotlin/gui/config/JAnyHud.kt b/src/main/kotlin/gui/config/JAnyHud.kt
index 35c4eb2..1cde4f9 100644
--- a/src/main/kotlin/gui/config/JAnyHud.kt
+++ b/src/main/kotlin/gui/config/JAnyHud.kt
@@ -1,48 +1,67 @@
-
-
package moe.nea.firmament.gui.config
import moe.nea.jarvis.api.JarvisHud
-import moe.nea.jarvis.api.JarvisScalable
+import org.joml.Matrix3x2f
+import org.joml.Vector2i
+import org.joml.Vector2ic
import kotlinx.serialization.Serializable
import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import moe.nea.firmament.jarvis.JarvisIntegration
@Serializable
data class HudPosition(
- var x: Double,
- var y: Double,
- var scale: Float,
+ var x: Int,
+ var y: Int,
+ var scale: Float,
)
data class HudMeta(
- val position: HudPosition,
- private val label: Text,
- private val width: Int,
- private val height: Int,
-) : JarvisScalable, JarvisHud {
- override fun getX(): Double = position.x
+ val position: HudPosition,
+ private val id: Identifier,
+ private val label: Text,
+ private val width: Int,
+ private val height: Int,
+) : JarvisHud, JarvisHud.Scalable {
+ override fun getLabel(): Text = label
+ override fun getUnscaledWidth(): Int {
+ return width
+ }
+
+ override fun getUnscaledHeight(): Int {
+ return height
+ }
- override fun setX(newX: Double) {
- position.x = newX
- }
+ override fun getHudId(): Identifier {
+ return id
+ }
- override fun getY(): Double = position.y
+ override fun getPosition(): Vector2ic {
+ return Vector2i(position.x, position.y)
+ }
- override fun setY(newY: Double) {
- position.y = newY
- }
+ override fun setPosition(p0: Vector2ic) {
+ position.x = p0.x()
+ position.y = p0.y()
+ }
- override fun getLabel(): Text = label
+ override fun isEnabled(): Boolean {
+ return true // TODO: this should be actually truthful, if possible
+ }
- override fun getWidth(): Int = width
+ override fun isVisible(): Boolean {
+ return true // TODO: this should be actually truthful, if possible
+ }
- override fun getHeight(): Int = height
+ override fun getScale(): Float = position.scale
- override fun getScale(): Float = position.scale
+ override fun setScale(newScale: Float) {
+ position.scale = newScale
+ }
- override fun setScale(newScale: Float) {
- position.scale = newScale
- }
+ fun applyTransformations(matrix4f: Matrix3x2f) {
+ applyTransformations(JarvisIntegration.jarvis, matrix4f)
+ }
}
diff --git a/src/main/kotlin/gui/config/KeyBindingHandler.kt b/src/main/kotlin/gui/config/KeyBindingHandler.kt
index d7d0b47..3c08da2 100644
--- a/src/main/kotlin/gui/config/KeyBindingHandler.kt
+++ b/src/main/kotlin/gui/config/KeyBindingHandler.kt
@@ -1,11 +1,5 @@
package moe.nea.firmament.gui.config
-import io.github.notenoughupdates.moulconfig.common.IMinecraft
-import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
-import io.github.notenoughupdates.moulconfig.deps.libninepatch.NinePatch
-import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
-import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent
-import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
import kotlinx.serialization.json.Json
import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.decodeFromJsonElement
@@ -13,6 +7,7 @@ import kotlinx.serialization.json.encodeToJsonElement
import moe.nea.firmament.gui.FirmButtonComponent
import moe.nea.firmament.keybindings.FirmamentKeyBindings
import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.data.ManagedConfig
class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) :
ManagedConfig.OptionHandler<SavedKeyBinding> {
@@ -35,39 +30,12 @@ class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) :
{ opt.value },
{
opt.value = it
- opt.element.save()
+ opt.element.markDirty()
},
{ button.blur() },
{ button.requestFocus() }
)
- button = object : FirmButtonComponent(
- TextComponent(
- IMinecraft.instance.defaultFontRenderer,
- { sm.label.string },
- 130,
- TextComponent.TextAlignment.LEFT,
- false,
- false
- ), action = {
- sm.onClick()
- }) {
- override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
- if (event is KeyboardEvent.KeyPressed) {
- return sm.keyboardEvent(event.keycode, event.pressed)
- }
- return super.keyboardEvent(event, context)
- }
-
- override fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> {
- if (sm.editing) return activeBg
- return super.getBackground(context)
- }
-
-
- override fun onLostFocus() {
- sm.onLostFocus()
- }
- }
+ button = sm.createButton()
sm.updateLabel()
return button
}
diff --git a/src/main/kotlin/gui/config/KeyBindingStateManager.kt b/src/main/kotlin/gui/config/KeyBindingStateManager.kt
index cc8178d..d8ec359 100644
--- a/src/main/kotlin/gui/config/KeyBindingStateManager.kt
+++ b/src/main/kotlin/gui/config/KeyBindingStateManager.kt
@@ -1,8 +1,18 @@
package moe.nea.firmament.gui.config
+import io.github.notenoughupdates.moulconfig.common.IMinecraft
+import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
+import io.github.notenoughupdates.moulconfig.deps.libninepatch.NinePatch
+import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigPlatform
import org.lwjgl.glfw.GLFW
import net.minecraft.text.Text
import net.minecraft.util.Formatting
+import moe.nea.firmament.gui.FirmButtonComponent
+import moe.nea.firmament.keybindings.GenericInputButton
+import moe.nea.firmament.keybindings.InputModifiers
import moe.nea.firmament.keybindings.SavedKeyBinding
class KeyBindingStateManager(
@@ -12,73 +22,65 @@ class KeyBindingStateManager(
val requestFocus: () -> Unit,
) {
var editing = false
- var lastPressed = 0
- var lastPressedNonModifier = 0
+ var lastPressed: GenericInputButton? = null
var label: Text = Text.literal("")
- fun onClick() {
+ fun onClick(mouseButton: Int) {
if (editing) {
- editing = false
- blur()
- } else {
+ keyboardEvent(GenericInputButton.mouse(mouseButton), true)
+ } else if (mouseButton == GLFW.GLFW_MOUSE_BUTTON_LEFT) {
editing = true
requestFocus()
}
updateLabel()
}
- fun keyboardEvent(keyCode: Int, pressed: Boolean): Boolean {
- return if (pressed) onKeyPressed(keyCode, SavedKeyBinding.getModInt())
- else onKeyReleased(keyCode, SavedKeyBinding.getModInt())
+ fun keyboardEvent(keyCode: GenericInputButton, pressed: Boolean): Boolean {
+ return if (pressed) onKeyPressed(keyCode, InputModifiers.current())
+ else onKeyReleased(keyCode, InputModifiers.current())
}
- fun onKeyPressed(ch: Int, modifiers: Int): Boolean {
+ fun onKeyPressed(
+ ch: GenericInputButton,
+ modifiers: InputModifiers
+ ): Boolean { // TODO !!!!!: genericify this method to allow for other inputs
if (!editing) {
return false
}
- if (ch == GLFW.GLFW_KEY_ESCAPE) {
- lastPressedNonModifier = 0
+ if (ch == GenericInputButton.escape()) {
editing = false
- lastPressed = 0
- setValue(SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN))
+ lastPressed = null
+ setValue(SavedKeyBinding.unbound())
updateLabel()
blur()
return true
}
- if (ch == GLFW.GLFW_KEY_LEFT_SHIFT || ch == GLFW.GLFW_KEY_RIGHT_SHIFT
- || ch == GLFW.GLFW_KEY_LEFT_ALT || ch == GLFW.GLFW_KEY_RIGHT_ALT
- || ch == GLFW.GLFW_KEY_LEFT_CONTROL || ch == GLFW.GLFW_KEY_RIGHT_CONTROL
- ) {
+ if (ch.isModifier()) {
lastPressed = ch
} else {
- setValue(SavedKeyBinding(
- ch, modifiers
- ))
+ setValue(SavedKeyBinding(ch, modifiers))
editing = false
blur()
- lastPressed = 0
- lastPressedNonModifier = 0
+ lastPressed = null
}
updateLabel()
return true
}
fun onLostFocus() {
- lastPressedNonModifier = 0
editing = false
- lastPressed = 0
+ lastPressed = null
updateLabel()
}
- fun onKeyReleased(ch: Int, modifiers: Int): Boolean {
+ fun onKeyReleased(ch: GenericInputButton, modifiers: InputModifiers): Boolean {
if (!editing)
return false
- if (lastPressedNonModifier == ch || (lastPressedNonModifier == 0 && ch == lastPressed)) {
+ if (ch == lastPressed) { // TODO: check modifiers dont duplicate (CTRL+CTRL)
setValue(SavedKeyBinding(ch, modifiers))
editing = false
blur()
- lastPressed = 0
- lastPressedNonModifier = 0
+ lastPressed = null
}
updateLabel()
return true
@@ -87,16 +89,11 @@ class KeyBindingStateManager(
fun updateLabel() {
var stroke = value().format()
if (editing) {
- stroke = Text.literal("")
- val (shift, ctrl, alt) = SavedKeyBinding.getMods(SavedKeyBinding.getModInt())
- if (shift) {
- stroke.append("SHIFT + ")
- }
- if (alt) {
- stroke.append("ALT + ")
- }
- if (ctrl) {
- stroke.append("CTRL + ")
+ stroke = Text.empty()
+ val modifiers = InputModifiers.current()
+ if (!modifiers.isEmpty()) {
+ stroke.append(modifiers.format())
+ stroke.append(" + ")
}
stroke.append("???")
stroke.styled { it.withColor(Formatting.YELLOW) }
@@ -104,5 +101,39 @@ class KeyBindingStateManager(
label = stroke
}
+ fun createButton(): FirmButtonComponent {
+ return object : FirmButtonComponent(
+ TextComponent(
+ IMinecraft.INSTANCE.defaultFontRenderer,
+ { MoulConfigPlatform.wrap(this@KeyBindingStateManager.label) },
+ 130,
+ TextComponent.TextAlignment.LEFT,
+ false,
+ false
+ ), action = {
+ this@KeyBindingStateManager.onClick(it)
+ }) {
+ override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
+ if (event is KeyboardEvent.KeyPressed) {
+ return this@KeyBindingStateManager.keyboardEvent(
+ GenericInputButton.ofKeyAndScan(
+ event.keycode,
+ event.scancode
+ ), event.pressed
+ )
+ }
+ return super.keyboardEvent(event, context)
+ }
+ override fun getBackground(context: GuiImmediateContext): NinePatch<MyResourceLocation> {
+ if (this@KeyBindingStateManager.editing) return activeBg
+ return super.getBackground(context)
+ }
+
+
+ override fun onLostFocus() {
+ this@KeyBindingStateManager.onLostFocus()
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/gui/config/ManagedConfig.kt b/src/main/kotlin/gui/config/ManagedConfig.kt
deleted file mode 100644
index 8222a46..0000000
--- a/src/main/kotlin/gui/config/ManagedConfig.kt
+++ /dev/null
@@ -1,215 +0,0 @@
-package moe.nea.firmament.gui.config
-
-import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
-import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
-import io.github.notenoughupdates.moulconfig.gui.GuiContext
-import io.github.notenoughupdates.moulconfig.gui.component.CenterComponent
-import io.github.notenoughupdates.moulconfig.gui.component.ColumnComponent
-import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent
-import io.github.notenoughupdates.moulconfig.gui.component.RowComponent
-import io.github.notenoughupdates.moulconfig.gui.component.ScrollPanelComponent
-import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
-import moe.nea.jarvis.api.Point
-import org.lwjgl.glfw.GLFW
-import kotlinx.serialization.encodeToString
-import kotlinx.serialization.json.JsonElement
-import kotlinx.serialization.json.JsonObject
-import kotlin.io.path.createDirectories
-import kotlin.io.path.readText
-import kotlin.io.path.writeText
-import kotlin.time.Duration
-import net.minecraft.client.gui.screen.Screen
-import net.minecraft.text.Text
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.gui.FirmButtonComponent
-import moe.nea.firmament.keybindings.SavedKeyBinding
-import moe.nea.firmament.util.ScreenUtil.setScreenLater
-import moe.nea.firmament.util.collections.InstanceList
-
-abstract class ManagedConfig(
- override val name: String,
- val category: Category,
- // TODO: allow vararg secondaryCategories: Category,
-) : ManagedConfigElement() {
- enum class Category {
- // Böse Kategorie, nicht benutzten lol
- MISC,
- CHAT,
- INVENTORY,
- MINING,
- EVENTS,
- INTEGRATIONS,
- META,
- DEV,
- ;
-
- val labelText: Text = Text.translatable("firmament.config.category.${name.lowercase()}")
- val description: Text = Text.translatable("firmament.config.category.${name.lowercase()}.description")
- val configs: MutableList<ManagedConfig> = mutableListOf()
- }
-
- companion object {
- val allManagedConfigs = InstanceList<ManagedConfig>("ManagedConfig")
- }
-
- interface OptionHandler<T : Any> {
- fun initOption(opt: ManagedOption<T>) {}
- fun toJson(element: T): JsonElement?
- fun fromJson(element: JsonElement): T
- fun emitGuiElements(opt: ManagedOption<T>, guiAppender: GuiAppender)
- }
-
- init {
- allManagedConfigs.getAll().forEach {
- require(it.name != name) { "Duplicate name '$name' used for config" }
- }
- allManagedConfigs.add(this)
- category.configs.add(this)
- }
-
- val file = Firmament.CONFIG_DIR.resolve("$name.json")
- val data: JsonObject by lazy {
- try {
- Firmament.json.decodeFromString(
- file.readText()
- )
- } catch (e: Exception) {
- Firmament.logger.info("Could not read config $name. Loading empty config.")
- JsonObject(mutableMapOf())
- }
- }
-
- fun save() {
- val data = JsonObject(allOptions.mapNotNull { (key, value) ->
- value.toJson()?.let {
- key to it
- }
- }.toMap())
- file.parent.createDirectories()
- file.writeText(Firmament.json.encodeToString(data))
- }
-
-
- val allOptions = mutableMapOf<String, ManagedOption<*>>()
- val sortedOptions = mutableListOf<ManagedOption<*>>()
-
- private var latestGuiAppender: GuiAppender? = null
-
- protected fun <T : Any> option(
- propertyName: String,
- default: () -> T,
- handler: OptionHandler<T>
- ): ManagedOption<T> {
- if (propertyName in allOptions) error("Cannot register the same name twice")
- return ManagedOption(this, propertyName, default, handler).also {
- it.handler.initOption(it)
- it.load(data)
- allOptions[propertyName] = it
- sortedOptions.add(it)
- }
- }
-
- protected fun toggle(propertyName: String, default: () -> Boolean): ManagedOption<Boolean> {
- return option(propertyName, default, BooleanHandler(this))
- }
-
- protected fun duration(
- propertyName: String,
- min: Duration,
- max: Duration,
- default: () -> Duration,
- ): ManagedOption<Duration> {
- return option(propertyName, default, DurationHandler(this, min, max))
- }
-
-
- protected fun position(
- propertyName: String,
- width: Int,
- height: Int,
- default: () -> Point,
- ): ManagedOption<HudMeta> {
- val label = Text.translatable("firmament.config.${name}.${propertyName}")
- return option(propertyName, {
- val p = default()
- HudMeta(HudPosition(p.x, p.y, 1F), label, width, height)
- }, HudMetaHandler(this, label, width, height))
- }
-
- protected fun keyBinding(
- propertyName: String,
- default: () -> Int,
- ): ManagedOption<SavedKeyBinding> = keyBindingWithOutDefaultModifiers(propertyName) { SavedKeyBinding(default()) }
-
- protected fun keyBindingWithOutDefaultModifiers(
- propertyName: String,
- default: () -> SavedKeyBinding,
- ): ManagedOption<SavedKeyBinding> {
- return option(propertyName, default, KeyBindingHandler("firmament.config.${name}.${propertyName}", this))
- }
-
- protected fun keyBindingWithDefaultUnbound(
- propertyName: String,
- ): ManagedOption<SavedKeyBinding> {
- return keyBindingWithOutDefaultModifiers(propertyName) { SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN) }
- }
-
- protected fun integer(
- propertyName: String,
- min: Int,
- max: Int,
- default: () -> Int,
- ): ManagedOption<Int> {
- return option(propertyName, default, IntegerHandler(this, min, max))
- }
-
- protected fun button(propertyName: String, runnable: () -> Unit): ManagedOption<Unit> {
- return option(propertyName, { }, ClickHandler(this, runnable))
- }
-
- protected fun string(propertyName: String, default: () -> String): ManagedOption<String> {
- return option(propertyName, default, StringHandler(this))
- }
-
-
- fun reloadGui() {
- latestGuiAppender?.reloadables?.forEach { it() }
- }
-
- val translationKey get() = "firmament.config.${name}"
- val labelText: Text = Text.translatable(translationKey)
-
- fun getConfigEditor(parent: Screen? = null): Screen {
- var screen: Screen? = null
- val guiapp = GuiAppender(400) { requireNotNull(screen) { "Screen Accessor called too early" } }
- latestGuiAppender = guiapp
- guiapp.appendFullRow(RowComponent(
- FirmButtonComponent(TextComponent("←")) {
- if (parent != null) {
- save()
- setScreenLater(parent)
- } else {
- AllConfigsGui.showAllGuis()
- }
- }
- ))
- sortedOptions.forEach { it.appendToGui(guiapp) }
- guiapp.reloadables.forEach { it() }
- val component = CenterComponent(PanelComponent(ScrollPanelComponent(400, 300, ColumnComponent(guiapp.panel)),
- 10,
- PanelComponent.DefaultBackgroundRenderer.VANILLA))
- screen = object : GuiComponentWrapper(GuiContext(component)) {
- override fun close() {
- if (context.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) {
- client!!.setScreen(parent)
- }
- }
- }
- return screen
- }
-
- fun showConfigEditor(parent: Screen? = null) {
- setScreenLater(getConfigEditor(parent))
- }
-
-}
diff --git a/src/main/kotlin/gui/config/ManagedConfigElement.kt b/src/main/kotlin/gui/config/ManagedConfigElement.kt
deleted file mode 100644
index 28cd6b8..0000000
--- a/src/main/kotlin/gui/config/ManagedConfigElement.kt
+++ /dev/null
@@ -1,8 +0,0 @@
-
-
-package moe.nea.firmament.gui.config
-
-abstract class ManagedConfigElement {
- abstract val name: String
-
-}
diff --git a/src/main/kotlin/gui/config/ManagedOption.kt b/src/main/kotlin/gui/config/ManagedOption.kt
index d1aba83..1f742a7 100644
--- a/src/main/kotlin/gui/config/ManagedOption.kt
+++ b/src/main/kotlin/gui/config/ManagedOption.kt
@@ -6,8 +6,8 @@ import kotlinx.serialization.json.JsonObject
import kotlin.properties.ReadWriteProperty
import kotlin.reflect.KProperty
import net.minecraft.text.Text
-import moe.nea.firmament.Firmament
import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.data.ManagedConfig
class ManagedOption<T : Any>(
val element: ManagedConfig,
@@ -28,7 +28,13 @@ class ManagedOption<T : Any>(
val descriptionTranslationKey = "firmament.config.${element.name}.${propertyName}.description"
val labelDescription: Text = Text.translatable(descriptionTranslationKey)
- lateinit var value: T
+ var _actualValue: T? = null
+ var value: T
+ get() = _actualValue ?: error("Lateinit variable not initialized")
+ set(value) {
+ _actualValue = value
+ element.onChange(this)
+ }
override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) {
this.value = value
@@ -44,7 +50,7 @@ class ManagedOption<T : Any>(
value = handler.fromJson(root[propertyName]!!)
return
} catch (e: Exception) {
- ErrorUtil.softError(
+ ErrorUtil.logError(
"Exception during loading of config file ${element.name}. This will reset this config.",
e
)
diff --git a/src/main/kotlin/gui/config/StringHandler.kt b/src/main/kotlin/gui/config/StringHandler.kt
index a326abb..f1eacab 100644
--- a/src/main/kotlin/gui/config/StringHandler.kt
+++ b/src/main/kotlin/gui/config/StringHandler.kt
@@ -8,6 +8,7 @@ import kotlinx.serialization.json.JsonElement
import kotlinx.serialization.json.JsonPrimitive
import kotlinx.serialization.json.jsonPrimitive
import net.minecraft.text.Text
+import moe.nea.firmament.util.data.ManagedConfig
class StringHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<String> {
override fun toJson(element: String): JsonElement? {
@@ -25,7 +26,7 @@ class StringHandler(val config: ManagedConfig) : ManagedConfig.OptionHandler<Str
object : GetSetter<String> by opt {
override fun set(newValue: String) {
opt.set(newValue)
- config.save()
+ config.markDirty()
}
},
130,
diff --git a/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt b/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt
new file mode 100644
index 0000000..59ca71e
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/ConfigLoadContext.kt
@@ -0,0 +1,77 @@
+package moe.nea.firmament.gui.config.storage
+
+import java.io.PrintWriter
+import java.nio.file.Path
+import org.apache.commons.io.output.StringBuilderWriter
+import kotlin.io.path.Path
+import kotlin.io.path.createParentDirectories
+import kotlin.io.path.writeText
+import moe.nea.firmament.Firmament
+
+data class ConfigLoadContext(
+ val loadId: String,
+) : AutoCloseable {
+ val logFile = Path("logs")
+ .resolve(Firmament.MOD_ID)
+ .resolve("config-$loadId.log")
+ .toAbsolutePath()
+ val logBuffer = StringBuilder()
+
+ var shouldSaveLogBuffer = false
+ fun markShouldSaveLogBuffer() {
+ shouldSaveLogBuffer = true
+ }
+
+ fun logDebug(message: String) {
+ logBuffer.append("[DEBUG] ").append(message).appendLine()
+ }
+
+ fun logInfo(message: String) {
+ if (Firmament.DEBUG)
+ Firmament.logger.info("[ConfigUpgrade] $message")
+ logBuffer.append("[INFO] ").append(message).appendLine()
+ }
+
+ fun logError(message: String, exception: Throwable) {
+ markShouldSaveLogBuffer()
+ if (Firmament.DEBUG)
+ Firmament.logger.error("[ConfigUpgrade] $message", exception)
+ logBuffer.append("[ERROR] ").append(message).appendLine()
+ PrintWriter(StringBuilderWriter(logBuffer)).use {
+ exception.printStackTrace(it)
+ }
+ logBuffer.appendLine()
+ }
+
+ fun logError(message: String) {
+ markShouldSaveLogBuffer()
+ Firmament.logger.error("[ConfigUpgrade] $message")
+ logBuffer.append("[ERROR] ").append(message).appendLine()
+ }
+
+ fun ensureWritable(path: Path) {
+ path.createParentDirectories()
+ }
+
+ fun use(block: (ConfigLoadContext) -> Unit) {
+ try {
+ block(this)
+ } catch (ex: Exception) {
+ logError("Caught exception on CLC", ex)
+ } finally {
+ close()
+ }
+ }
+
+ override fun close() {
+ logInfo("Closing out config load.")
+ if (shouldSaveLogBuffer) {
+ try {
+ ensureWritable(logFile)
+ logFile.writeText(logBuffer.toString())
+ } catch (ex: Exception) {
+ logError("Could not save config load log", ex)
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt b/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt
new file mode 100644
index 0000000..8258fe7
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/ConfigStorageClass.kt
@@ -0,0 +1,8 @@
+package moe.nea.firmament.gui.config.storage
+
+enum class ConfigStorageClass { // TODO: make this encode type info somehow
+ PROFILE,
+ STORAGE,
+ CONFIG,
+}
+
diff --git a/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt b/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt
new file mode 100644
index 0000000..f8e3104
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/FirmamentConfigLoader.kt
@@ -0,0 +1,204 @@
+package moe.nea.firmament.gui.config.storage
+
+import java.util.UUID
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.jsonObject
+import kotlin.io.path.Path
+import kotlin.io.path.exists
+import kotlin.io.path.forEachDirectoryEntry
+import kotlin.io.path.isDirectory
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.name
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+import moe.nea.firmament.util.data.IConfigProvider
+import moe.nea.firmament.util.data.IDataHolder
+import moe.nea.firmament.util.data.ProfileKeyedConfig
+import moe.nea.firmament.util.json.intoGson
+import moe.nea.firmament.util.json.intoKotlinJson
+
+object FirmamentConfigLoader {
+ val currentConfigVersion = 1000
+ val configFolder = Path("config/firmament")
+ .toAbsolutePath()
+ val storageFolder = configFolder.resolve("storage")
+ val profilePath = configFolder.resolve("profiles")
+ val tagLines = listOf(
+ "<- your config version here",
+ "I'm a teapot",
+ "mail.example.com ESMTP",
+ "Apples"
+ )
+ val configVersionFile = configFolder.resolve("config.version")
+
+ fun loadConfig() {
+ if (configFolder.exists()) {
+ if (!configVersionFile.exists()) {
+ LegacyImporter.importFromLegacy()
+ }
+ updateConfigs()
+ }
+
+ ConfigLoadContext("load-${System.currentTimeMillis()}").use { loadContext ->
+ val configData = FirstLevelSplitJsonFolder(loadContext, configFolder).load()
+ loadConfigFromData(configData, Unit, ConfigStorageClass.CONFIG)
+ val storageData = FirstLevelSplitJsonFolder(loadContext, storageFolder).load()
+ loadConfigFromData(storageData, Unit, ConfigStorageClass.STORAGE)
+ val profileData =
+ profilePath.listDirectoryEntries()
+ .filter { it.isDirectory() }
+ .associate {
+ UUID.fromString(it.name) to FirstLevelSplitJsonFolder(loadContext, it).load()
+ }
+ profileData.forEach { (key, value) ->
+ loadConfigFromData(value, key, ConfigStorageClass.PROFILE)
+ }
+ }
+ }
+
+ fun <T> loadConfigFromData(
+ configData: JsonObject,
+ key: T,
+ storageClass: ConfigStorageClass
+ ) {
+ for (holder in allConfigs) {
+ if (holder.storageClass == storageClass) {
+ (holder as IDataHolder<T>).loadFrom(key, configData)
+ }
+ }
+ }
+
+ fun <T> collectConfigFromData(
+ key: T,
+ storageClass: ConfigStorageClass,
+ ): JsonObject {
+ var json = JsonObject(mapOf())
+ for (holder in allConfigs) {
+ if (holder.storageClass == storageClass) {
+ json = mergeJson(json, (holder as IDataHolder<T>).saveTo(key))
+ }
+ }
+ return json
+ }
+
+ fun <T> saveStorage(
+ storageClass: ConfigStorageClass,
+ key: T,
+ firstLevelSplitJsonFolder: FirstLevelSplitJsonFolder,
+ ) {
+ firstLevelSplitJsonFolder.save(
+ collectConfigFromData(key, storageClass)
+ )
+ }
+
+ fun collectAllProfileIds(): Set<UUID> {
+ return allConfigs
+ .filter { it.storageClass == ConfigStorageClass.PROFILE }
+ .flatMapTo(mutableSetOf()) {
+ (it as ProfileKeyedConfig<*>).keys()
+ }
+ }
+
+ fun saveAll() {
+ ConfigLoadContext("save-${System.currentTimeMillis()}").use { context ->
+ saveStorage(
+ ConfigStorageClass.CONFIG,
+ Unit,
+ FirstLevelSplitJsonFolder(context, configFolder)
+ )
+ saveStorage(
+ ConfigStorageClass.STORAGE,
+ Unit,
+ FirstLevelSplitJsonFolder(context, storageFolder)
+ )
+ collectAllProfileIds().forEach { profileId ->
+ saveStorage(
+ ConfigStorageClass.PROFILE,
+ profileId,
+ FirstLevelSplitJsonFolder(context, profilePath.resolve(profileId.toString()))
+ )
+ }
+ }
+ }
+
+ fun mergeJson(a: JsonObject, b: JsonObject): JsonObject {
+ fun mergeInner(a: JsonElement?, b: JsonElement?): JsonElement {
+ if (a == null)
+ return b!!
+ if (b == null)
+ return a
+ a as JsonObject
+ b as JsonObject
+ return buildJsonObject {
+ (a.keys + b.keys)
+ .forEach {
+ put(it, mergeInner(a[it], b[it]))
+ }
+ }
+ }
+ return mergeInner(a, b) as JsonObject
+ }
+
+ val allConfigs: List<IDataHolder<*>> = IConfigProvider.providers.allValidInstances.flatMap { it.configs }
+
+ fun updateConfigs() {
+ val startVersion = configVersionFile.readText()
+ .substringBefore(' ')
+ .trim()
+ .toInt()
+ ConfigLoadContext("update-from-$startVersion-to-$currentConfigVersion-${System.currentTimeMillis()}")
+ .use { loadContext ->
+ updateOneConfig(
+ loadContext,
+ startVersion,
+ ConfigStorageClass.CONFIG,
+ FirstLevelSplitJsonFolder(loadContext, configFolder)
+ )
+ updateOneConfig(
+ loadContext,
+ startVersion,
+ ConfigStorageClass.STORAGE,
+ FirstLevelSplitJsonFolder(loadContext, storageFolder)
+ )
+ profilePath.forEachDirectoryEntry {
+ updateOneConfig(
+ loadContext,
+ startVersion,
+ ConfigStorageClass.PROFILE,
+ FirstLevelSplitJsonFolder(loadContext, it)
+ )
+ }
+ configVersionFile.writeText("$currentConfigVersion ${tagLines.random()}")
+ }
+ }
+
+ private fun updateOneConfig(
+ loadContext: ConfigLoadContext,
+ startVersion: Int,
+ storageClass: ConfigStorageClass,
+ firstLevelSplitJsonFolder: FirstLevelSplitJsonFolder
+ ) {
+ loadContext.logInfo("Starting upgrade from at ${firstLevelSplitJsonFolder.folder} ($storageClass) to $startVersion")
+ var data = firstLevelSplitJsonFolder.load()
+ for (nextVersion in (startVersion + 1)..currentConfigVersion) {
+ data = updateOneConfigOnce(nextVersion, storageClass, data)
+ }
+ firstLevelSplitJsonFolder.save(data)
+ }
+
+ private fun updateOneConfigOnce(
+ nextVersion: Int,
+ storageClass: ConfigStorageClass,
+ data: JsonObject
+ ): JsonObject {
+ return ConfigFixEvent.publish(ConfigFixEvent(storageClass, nextVersion, data.intoGson().asJsonObject))
+ .data.intoKotlinJson().jsonObject
+ }
+
+ fun markDirty(holder: IDataHolder<*>) {
+ saveAll()
+ }
+
+}
diff --git a/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt b/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt
new file mode 100644
index 0000000..ff544d5
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/FirstLevelSplitJsonFolder.kt
@@ -0,0 +1,83 @@
+@file:OptIn(ExperimentalSerializationApi::class)
+
+package moe.nea.firmament.gui.config.storage
+
+import java.nio.file.Path
+import kotlinx.serialization.ExperimentalSerializationApi
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.decodeFromStream
+import kotlinx.serialization.json.encodeToStream
+import kotlin.io.path.deleteExisting
+import kotlin.io.path.inputStream
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.nameWithoutExtension
+import kotlin.io.path.outputStream
+import moe.nea.firmament.Firmament
+
+class FirstLevelSplitJsonFolder(
+ val context: ConfigLoadContext,
+ val folder: Path
+) {
+ fun load(): JsonObject {
+ context.logInfo("Loading FLSJF from $folder")
+ return folder.listDirectoryEntries("*.json")
+ .mapNotNull(::loadIndividualFile)
+ .toMap()
+ .let(::JsonObject)
+ .also { context.logInfo("FLSJF from $folder - Voller Erfolg!") }
+ }
+
+ fun loadIndividualFile(path: Path): Pair<String, JsonElement>? {
+ context.logInfo("Loading partial file from $path")
+ return try {
+ path.inputStream().use {
+ path.nameWithoutExtension to Firmament.json.decodeFromStream(JsonElement.serializer(), it)
+ }
+ } catch (ex: Exception) {
+ context.logError("Could not load file from $path", ex)
+ null
+ }
+ }
+
+ fun save(value: JsonObject) {
+ context.logInfo("Saving FLSJF to $folder")
+ context.logDebug("Current value:\n$value")
+ val entries = folder.listDirectoryEntries("*.json")
+ .toMutableList()
+ for ((name, element) in value) {
+ val path = saveIndividualFile(name, element)
+ if (path != null) {
+ entries.remove(path)
+ }
+ }
+ if (entries.isNotEmpty()) {
+ context.logInfo("Deleting additional files.")
+ for (path in entries) {
+ context.logInfo("Deleting $path")
+// context.backup(path)
+ try {
+ path.deleteExisting()
+ } catch (ex: Exception) {
+ context.logError("Could not delete $path", ex)
+ }
+ }
+ }
+ context.logInfo("FLSJF to $folder - Voller Erfolg!")
+ }
+
+ fun saveIndividualFile(name: String, element: JsonElement): Path? {
+ try {
+ context.logInfo("Saving partial file with name $name")
+ val path = folder.resolve("$name.json")
+ context.ensureWritable(path)
+ path.outputStream().use {
+ Firmament.json.encodeToStream(JsonElement.serializer(), element, it)
+ }
+ return path
+ } catch (ex: Exception) {
+ context.logError("Could not save $name with value $element", ex)
+ return null
+ }
+ }
+}
diff --git a/src/main/kotlin/gui/config/storage/LegacyImporter.kt b/src/main/kotlin/gui/config/storage/LegacyImporter.kt
new file mode 100644
index 0000000..d06afcc
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/LegacyImporter.kt
@@ -0,0 +1,66 @@
+package moe.nea.firmament.gui.config.storage
+
+import java.nio.file.Path
+import kotlin.io.path.copyTo
+import kotlin.io.path.createDirectories
+import kotlin.io.path.createParentDirectories
+import kotlin.io.path.exists
+import kotlin.io.path.forEachDirectoryEntry
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.moveTo
+import kotlin.io.path.name
+import kotlin.io.path.nameWithoutExtension
+import kotlin.io.path.writeText
+import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.configFolder
+import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.configVersionFile
+import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader.storageFolder
+
+object LegacyImporter {
+ val legacyConfigVersion = 995
+ val backupPath = configFolder.resolveSibling("firmament-legacy-config-${System.currentTimeMillis()}")
+
+ fun copyIf(from: Path, to: Path) {
+ if (from.exists()) {
+ to.createParentDirectories()
+ from.copyTo(to)
+ }
+ }
+
+ val legacyStorage = listOf(
+ "inventory-buttons",
+ "macros",
+ )
+
+ fun importFromLegacy() {
+ configFolder.moveTo(backupPath)
+ configFolder.createDirectories()
+
+ legacyStorage.forEach {
+ copyIf(
+ backupPath.resolve("$it.json"),
+ storageFolder.resolve("$it.json")
+ )
+ }
+
+ backupPath.listDirectoryEntries("*.json")
+ .filter { it.nameWithoutExtension !in legacyStorage }
+ .forEach { path ->
+ val name = path.name
+ path.copyTo(configFolder.resolve(name))
+ }
+
+ backupPath.resolve("profiles")
+ .forEachDirectoryEntry { category ->
+ category.forEachDirectoryEntry { profile ->
+ copyIf(
+ profile,
+ FirmamentConfigLoader.profilePath
+ .resolve(profile.nameWithoutExtension)
+ .resolve(category.name + ".json")
+ )
+ }
+ }
+
+ configVersionFile.writeText(legacyConfigVersion.toString())
+ }
+}
diff --git a/src/main/kotlin/gui/config/storage/README.md b/src/main/kotlin/gui/config/storage/README.md
new file mode 100644
index 0000000..aad4afe
--- /dev/null
+++ b/src/main/kotlin/gui/config/storage/README.md
@@ -0,0 +1,68 @@
+<!--
+SPDX-FileCopyrightText: 2025 Linnea Gräf <nea@nea.moe>
+
+SPDX-License-Identifier: CC0-1.0
+-->
+
+# Plan for the 2026 Config Renewal of Firmament
+
+The current config system in Firmament is not growing at a reasonable pace. Here is a list of my grievances with it:
+
+- the config files are split, resulting in making migrations between different config files (which might load in
+ different order) difficult
+- it is difficult to detect extraneous properties / files, because not all files are loaded and consumed at once
+- profile specific data should be in a different hierarchy. the current hierarchy of `profiles/topic/<uuid>.json` orders
+ data from different profiles to be closer than data from the same profile. this also contributes to the two former
+ problems.
+
+## Goals
+
+- i want to retain having multiple different files for different topics, as well as a folder structure that makes sense
+ for profiles.
+- i want to split up "storage" type data, with "config" type data
+- i want to support partial loads with some broken files (resetting the files that are broken)
+- i want to support backups on any detected error (or simply at will)
+ - notably i do not care about the structure of the backups much. even just a all json files merged backup is fine
+ for me, for now.
+
+## Implementation
+
+### FirstLevelSplitJsonFolder
+
+One of the basic components of this new config folder is a `FirstLevelSplitJsonFolder`. A `FLSJF` takes in a folder
+containing multiple JSON-files and loads all of them unconditionally. Each file is then inserted side by side into a
+json object, to be processed further by other mechanisms.
+
+In essence the `FLSJF` takes a folder structure like this:
+
+```
+file-1.json
+file-2.json
+file-3.json
+```
+
+and turns it into a single merged json object:
+
+```json
+{
+ "file-1": "the json content of file-1.json",
+ "file-2": "the json content of file-2.json",
+ "file-3": "the json content of file-3.json"
+}
+```
+
+As with any stage of the implementation, any unparsable files shall be copied over to a backup spot and discarded.
+
+Nota bene: Folders are wholesale ignored.
+
+### Config folders
+
+Firmament stores all configs and data in the root config folder `./config/firmament`.
+
+- Any config data is stored as an [`FLSJF`](#firstlevelsplitjsonfolder) in the root config folder
+- Any generic storage data is stored as an [`FLSJF`](#firstlevelsplitjsonfolder) in `${rootConfigFolder}/storage/`.
+- Any profile specific storage data is stored as an [`FLSJF`](#firstlevelsplitjsonfolder) for each profile in `${rootConfigFolder}/profileStorage/${profileUuid}/`.
+- Any backup data is stored in `${rootConfigFolder}/backups/${launchId}/${loadId}/${fileName}`.
+ - Where `launchId` is `${currentLaunchTimestamp}-${random()}` to avoid collisions.
+ - Where `loadId` depends on which stage of the config load we are doing (`merge`/`upgrade`/etc.) and what type of config we are loading (`profileSpecific`/`config`/etc.).
+ - And where `fileName` may be a relative filename of where this data was originally found or some internal descriptor for the merged data stage we are on.
diff --git a/src/main/kotlin/gui/entity/EntityRenderer.kt b/src/main/kotlin/gui/entity/EntityRenderer.kt
index 022b9a3..3e21710 100644
--- a/src/main/kotlin/gui/entity/EntityRenderer.kt
+++ b/src/main/kotlin/gui/entity/EntityRenderer.kt
@@ -27,41 +27,81 @@ object EntityRenderer {
}
val entityIds: Map<String, () -> LivingEntity> = mapOf(
- "Zombie" to t(EntityType.ZOMBIE),
+ "Armadillo" to t(EntityType.ARMADILLO),
+ "ArmorStand" to t(EntityType.ARMOR_STAND),
+ "Axolotl" to t(EntityType.AXOLOTL),
+ "Bat" to t(EntityType.BAT),
+ "Bee" to t(EntityType.BEE),
+ "Blaze" to t(EntityType.BLAZE),
+ "Bogged" to t(EntityType.BOGGED),
+ "Breeze" to t(EntityType.BREEZE),
+ "CaveSpider" to t(EntityType.CAVE_SPIDER),
"Chicken" to t(EntityType.CHICKEN),
- "Slime" to t(EntityType.SLIME),
- "Wolf" to t(EntityType.WOLF),
- "Skeleton" to t(EntityType.SKELETON),
+ "Cod" to t(EntityType.COD),
+ "Cow" to t(EntityType.COW),
+ "Creaking" to t(EntityType.CREAKING),
"Creeper" to t(EntityType.CREEPER),
+ "Dolphin" to t(EntityType.DOLPHIN),
+ "Donkey" to t(EntityType.DONKEY),
+ "Dragon" to t(EntityType.ENDER_DRAGON),
+ "Drowned" to t(EntityType.DROWNED),
+ "Eisengolem" to t(EntityType.IRON_GOLEM),
+ "Enderman" to t(EntityType.ENDERMAN),
+ "Endermite" to t(EntityType.ENDERMITE),
+ "Evoker" to t(EntityType.EVOKER),
+ "Fox" to t(EntityType.FOX),
+ "Frog" to t(EntityType.FROG),
+ "Ghast" to t(EntityType.GHAST),
+ "Giant" to t(EntityType.GIANT),
+ "GlowSquid" to t(EntityType.GLOW_SQUID),
+ "Goat" to t(EntityType.GOAT),
+ "Guardian" to t(EntityType.GUARDIAN),
+ "Horse" to t(EntityType.HORSE),
+ "Husk" to t(EntityType.HUSK),
+ "Illusioner" to t(EntityType.ILLUSIONER),
+ "LLama" to t(EntityType.LLAMA),
+ "MagmaCube" to t(EntityType.MAGMA_CUBE),
+ "Mooshroom" to t(EntityType.MOOSHROOM),
+ "Mule" to t(EntityType.MULE),
"Ocelot" to t(EntityType.OCELOT),
- "Blaze" to t(EntityType.BLAZE),
+ "Panda" to t(EntityType.PANDA),
+ "Phantom" to t(EntityType.PHANTOM),
+ "Pig" to t(EntityType.PIG),
+ "Piglin" to t(EntityType.PIGLIN),
+ "PiglinBrute" to t(EntityType.PIGLIN_BRUTE),
+ "Pigman" to t(EntityType.ZOMBIFIED_PIGLIN),
+ "Pillager" to t(EntityType.PILLAGER),
+ "Player" to { makeGuiPlayer(fakeWorld) },
+ "PolarBear" to t(EntityType.POLAR_BEAR),
+ "Pufferfish" to t(EntityType.PUFFERFISH),
"Rabbit" to t(EntityType.RABBIT),
+ "Salmom" to t(EntityType.SALMON),
+ "Salmon" to t(EntityType.SALMON),
"Sheep" to t(EntityType.SHEEP),
- "Horse" to t(EntityType.HORSE),
- "Eisengolem" to t(EntityType.IRON_GOLEM),
+ "Shulker" to t(EntityType.SHULKER),
"Silverfish" to t(EntityType.SILVERFISH),
- "Witch" to t(EntityType.WITCH),
- "Endermite" to t(EntityType.ENDERMITE),
+ "Skeleton" to t(EntityType.SKELETON),
+ "Slime" to t(EntityType.SLIME),
+ "Sniffer" to t(EntityType.SNIFFER),
"Snowman" to t(EntityType.SNOW_GOLEM),
- "Villager" to t(EntityType.VILLAGER),
- "Guardian" to t(EntityType.GUARDIAN),
- "ArmorStand" to t(EntityType.ARMOR_STAND),
- "Squid" to t(EntityType.SQUID),
- "Bat" to t(EntityType.BAT),
"Spider" to t(EntityType.SPIDER),
- "CaveSpider" to t(EntityType.CAVE_SPIDER),
- "Pigman" to t(EntityType.ZOMBIFIED_PIGLIN),
- "Ghast" to t(EntityType.GHAST),
- "MagmaCube" to t(EntityType.MAGMA_CUBE),
+ "Squid" to t(EntityType.SQUID),
+ "Stray" to t(EntityType.STRAY),
+ "Strider" to t(EntityType.STRIDER),
+ "Tadpole" to t(EntityType.TADPOLE),
+ "TropicalFish" to t(EntityType.TROPICAL_FISH),
+ "Turtle" to t(EntityType.TURTLE),
+ "Vex" to t(EntityType.VEX),
+ "Villager" to t(EntityType.VILLAGER),
+ "Vindicator" to t(EntityType.VINDICATOR),
+ "Warden" to t(EntityType.WARDEN),
+ "Witch" to t(EntityType.WITCH),
"Wither" to t(EntityType.WITHER),
- "Enderman" to t(EntityType.ENDERMAN),
- "Mooshroom" to t(EntityType.MOOSHROOM),
"WitherSkeleton" to t(EntityType.WITHER_SKELETON),
- "Cow" to t(EntityType.COW),
- "Dragon" to t(EntityType.ENDER_DRAGON),
- "Player" to { makeGuiPlayer(fakeWorld) },
- "Pig" to t(EntityType.PIG),
- "Giant" to t(EntityType.GIANT),
+ "Wolf" to t(EntityType.WOLF),
+ "Zoglin" to t(EntityType.ZOGLIN),
+ "Zombie" to t(EntityType.ZOMBIE),
+ "ZombieVillager" to t(EntityType.ZOMBIE_VILLAGER)
)
val entityModifiers: Map<String, EntityModifier> = mapOf(
"playerdata" to ModifyPlayerSkin,
@@ -83,7 +123,8 @@ object EntityRenderer {
for (modifierJson in modifiers) {
val modifier = ErrorUtil.notNullOr(
modifierJson["type"]?.asString?.let(entityModifiers::get),
- "Could not create entity with id $entityId. Failed to apply modifier $modifierJson") { return null }
+ "Could not create entity with id $entityId. Failed to apply modifier $modifierJson"
+ ) { return null }
entity = modifier.apply(entity, modifierJson)
}
return entity
@@ -111,10 +152,14 @@ object EntityRenderer {
renderContext: DrawContext,
posX: Int,
posY: Int,
- mouseX: Float,
- mouseY: Float
+ // TODO: Add width, height properties here
+ width: Double,
+ height: Double,
+ mouseX: Double,
+ mouseY: Double,
+ entityScale: Double = (height - 10.0) / 2.0
) {
- var bottomOffset = 0.0F
+ var bottomOffset = 0.0
var currentEntity = entity
val maxSize = entity.iterate { it.firstPassenger as? LivingEntity }
.map { it.height }
@@ -125,9 +170,9 @@ object EntityRenderer {
renderContext,
posX,
posY,
- posX + 50,
- posY + 80,
- minOf(2F / maxSize, 1F) * 30,
+ (posX + width).toInt(),
+ (posY + height).toInt(),
+ minOf(2F / maxSize, 1F) * entityScale,
-bottomOffset,
mouseX,
mouseY,
@@ -146,36 +191,38 @@ object EntityRenderer {
y1: Int,
x2: Int,
y2: Int,
- size: Float,
- bottomOffset: Float,
- mouseX: Float,
- mouseY: Float,
+ size: Double,
+ bottomOffset: Double,
+ 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 hw = (x2 - x1) / 2
+ val hh = (y2 - y1) / 2
+ val targetYaw = atan(((centerX - mouseX) / hw)).toFloat()
+ val targetPitch = atan(((centerY - mouseY) / hh - entity.standingEyeHeight * hh / 40)).toFloat()
val rotateToFaceTheFront = Quaternionf().rotateZ(Math.PI.toFloat())
val rotateToFaceTheCamera = Quaternionf().rotateX(targetPitch * 20.0f * (Math.PI.toFloat() / 180))
rotateToFaceTheFront.mul(rotateToFaceTheCamera)
val oldBodyYaw = entity.bodyYaw
val oldYaw = entity.yaw
val oldPitch = entity.pitch
- val oldPrevHeadYaw = entity.prevHeadYaw
+ val oldPrevHeadYaw = entity.lastHeadYaw
val oldHeadYaw = entity.headYaw
entity.bodyYaw = 180.0f + targetYaw * 20.0f
entity.yaw = 180.0f + targetYaw * 40.0f
entity.pitch = -targetPitch * 20.0f
entity.headYaw = entity.yaw
- entity.prevHeadYaw = entity.yaw
- val vector3f = Vector3f(0.0f, entity.height / 2.0f + bottomOffset, 0.0f)
- InventoryScreen.drawEntity(
+ entity.lastHeadYaw = entity.yaw
+ val vector3f = Vector3f(0.0f, (entity.height / 2.0f + bottomOffset).toFloat(), 0.0f)
+ InventoryScreen.drawEntity( // TODO: fix multiple entities rendering the same entity
context,
- centerX,
- centerY,
- size,
+ x1, y1,
+ x2, y2,
+ size.toFloat(),
vector3f,
rotateToFaceTheFront,
rotateToFaceTheCamera,
@@ -184,7 +231,7 @@ object EntityRenderer {
entity.bodyYaw = oldBodyYaw
entity.yaw = oldYaw
entity.pitch = oldPitch
- entity.prevHeadYaw = oldPrevHeadYaw
+ entity.lastHeadYaw = oldPrevHeadYaw
entity.headYaw = oldHeadYaw
context.disableScissor()
}
diff --git a/src/main/kotlin/gui/entity/FakeWorld.kt b/src/main/kotlin/gui/entity/FakeWorld.kt
deleted file mode 100644
index 7ec385c..0000000
--- a/src/main/kotlin/gui/entity/FakeWorld.kt
+++ /dev/null
@@ -1,338 +0,0 @@
-package moe.nea.firmament.gui.entity
-
-import java.util.UUID
-import java.util.function.BooleanSupplier
-import java.util.function.Consumer
-import net.minecraft.block.Block
-import net.minecraft.block.BlockState
-import net.minecraft.client.gui.screen.world.SelectWorldScreen
-import net.minecraft.component.type.MapIdComponent
-import net.minecraft.entity.Entity
-import net.minecraft.entity.damage.DamageSource
-import net.minecraft.entity.player.PlayerEntity
-import net.minecraft.fluid.Fluid
-import net.minecraft.item.FuelRegistry
-import net.minecraft.item.map.MapState
-import net.minecraft.particle.ParticleEffect
-import net.minecraft.recipe.BrewingRecipeRegistry
-import net.minecraft.recipe.RecipeManager
-import net.minecraft.recipe.RecipePropertySet
-import net.minecraft.recipe.StonecuttingRecipe
-import net.minecraft.recipe.display.CuttingRecipeDisplay
-import net.minecraft.registry.DynamicRegistryManager
-import net.minecraft.registry.Registries
-import net.minecraft.registry.RegistryKey
-import net.minecraft.registry.RegistryKeys
-import net.minecraft.registry.ServerDynamicRegistryType
-import net.minecraft.registry.entry.RegistryEntry
-import net.minecraft.resource.DataConfiguration
-import net.minecraft.resource.ResourcePackManager
-import net.minecraft.resource.featuretoggle.FeatureFlags
-import net.minecraft.resource.featuretoggle.FeatureSet
-import net.minecraft.scoreboard.Scoreboard
-import net.minecraft.server.SaveLoading
-import net.minecraft.server.command.CommandManager
-import net.minecraft.sound.SoundCategory
-import net.minecraft.sound.SoundEvent
-import net.minecraft.util.Identifier
-import net.minecraft.util.TypeFilter
-import net.minecraft.util.function.LazyIterationConsumer
-import net.minecraft.util.math.BlockPos
-import net.minecraft.util.math.Box
-import net.minecraft.util.math.ChunkPos
-import net.minecraft.util.math.Direction
-import net.minecraft.util.math.Vec3d
-import net.minecraft.world.BlockView
-import net.minecraft.world.Difficulty
-import net.minecraft.world.MutableWorldProperties
-import net.minecraft.world.World
-import net.minecraft.world.biome.Biome
-import net.minecraft.world.biome.BiomeKeys
-import net.minecraft.world.chunk.Chunk
-import net.minecraft.world.chunk.ChunkManager
-import net.minecraft.world.chunk.ChunkStatus
-import net.minecraft.world.chunk.EmptyChunk
-import net.minecraft.world.chunk.light.LightingProvider
-import net.minecraft.world.entity.EntityLookup
-import net.minecraft.world.event.GameEvent
-import net.minecraft.world.explosion.ExplosionBehavior
-import net.minecraft.world.tick.OrderedTick
-import net.minecraft.world.tick.QueryableTickScheduler
-import net.minecraft.world.tick.TickManager
-import moe.nea.firmament.util.MC
-
-fun createDynamicRegistry(): DynamicRegistryManager.Immutable {
- // TODO: use SaveLoading.load() to properly load a full registry
- return DynamicRegistryManager.of(Registries.REGISTRIES)
-}
-
-class FakeWorld(
- registries: DynamicRegistryManager.Immutable = createDynamicRegistry(),
-) : World(
- Properties,
- RegistryKey.of(RegistryKeys.WORLD, Identifier.of("firmament", "fakeworld")),
- registries,
- MC.defaultRegistries.getOrThrow(RegistryKeys.DIMENSION_TYPE)
- .getOrThrow(RegistryKey.of(RegistryKeys.DIMENSION_TYPE, Identifier.of("minecraft", "overworld"))),
- true,
- false,
- 0L,
- 0
-) {
- object Properties : MutableWorldProperties {
- override fun getSpawnPos(): BlockPos {
- return BlockPos.ORIGIN
- }
-
- override fun getSpawnAngle(): Float {
- return 0F
- }
-
- override fun getTime(): Long {
- return 0
- }
-
- override fun getTimeOfDay(): Long {
- return 0
- }
-
- override fun isThundering(): Boolean {
- return false
- }
-
- override fun isRaining(): Boolean {
- return false
- }
-
- override fun setRaining(raining: Boolean) {
- }
-
- override fun isHardcore(): Boolean {
- return false
- }
-
- override fun getDifficulty(): Difficulty {
- return Difficulty.HARD
- }
-
- override fun isDifficultyLocked(): Boolean {
- return false
- }
-
- override fun setSpawnPos(pos: BlockPos?, angle: Float) {}
- }
-
- override fun getPlayers(): List<PlayerEntity> {
- return emptyList()
- }
-
- override fun getBrightness(direction: Direction?, shaded: Boolean): Float {
- return 1f
- }
-
- override fun getGeneratorStoredBiome(biomeX: Int, biomeY: Int, biomeZ: Int): RegistryEntry<Biome> {
- return registryManager.getOptionalEntry(BiomeKeys.PLAINS).get()
- }
-
- override fun getSeaLevel(): Int {
- return 0
- }
-
- override fun getEnabledFeatures(): FeatureSet {
- return FeatureFlags.VANILLA_FEATURES
- }
-
- class FakeTickScheduler<T> : QueryableTickScheduler<T> {
- override fun scheduleTick(orderedTick: OrderedTick<T>?) {
- }
-
- override fun isQueued(pos: BlockPos?, type: T): Boolean {
- return true
- }
-
- override fun getTickCount(): Int {
- return 0
- }
-
- override fun isTicking(pos: BlockPos?, type: T): Boolean {
- return true
- }
-
- }
-
- override fun getBlockTickScheduler(): QueryableTickScheduler<Block> {
- return FakeTickScheduler()
- }
-
- override fun getFluidTickScheduler(): QueryableTickScheduler<Fluid> {
- return FakeTickScheduler()
- }
-
-
- class FakeChunkManager(val world: FakeWorld) : ChunkManager() {
- override fun getChunk(x: Int, z: Int, leastStatus: ChunkStatus?, create: Boolean): Chunk {
- return EmptyChunk(
- world,
- ChunkPos(x, z),
- world.registryManager.getOptionalEntry(BiomeKeys.PLAINS).get()
- )
- }
-
- override fun getWorld(): BlockView {
- return world
- }
-
- override fun tick(shouldKeepTicking: BooleanSupplier?, tickChunks: Boolean) {
- }
-
- override fun getDebugString(): String {
- return "FakeChunkManager"
- }
-
- override fun getLoadedChunkCount(): Int {
- return 0
- }
-
- override fun getLightingProvider(): LightingProvider {
- return FakeLightingProvider(this)
- }
- }
-
- class FakeLightingProvider(chunkManager: FakeChunkManager) : LightingProvider(chunkManager, false, false)
-
- override fun getChunkManager(): ChunkManager {
- return FakeChunkManager(this)
- }
-
- override fun playSound(
- source: PlayerEntity?,
- x: Double,
- y: Double,
- z: Double,
- sound: RegistryEntry<SoundEvent>?,
- category: SoundCategory?,
- volume: Float,
- pitch: Float,
- seed: Long
- ) {
- }
-
- override fun syncWorldEvent(player: PlayerEntity?, eventId: Int, pos: BlockPos?, data: Int) {
- }
-
- override fun emitGameEvent(event: RegistryEntry<GameEvent>?, emitterPos: Vec3d?, emitter: GameEvent.Emitter?) {
- }
-
- override fun updateListeners(pos: BlockPos?, oldState: BlockState?, newState: BlockState?, flags: Int) {
- }
-
- override fun playSoundFromEntity(
- source: PlayerEntity?,
- entity: Entity?,
- sound: RegistryEntry<SoundEvent>?,
- category: SoundCategory?,
- volume: Float,
- pitch: Float,
- seed: Long
- ) {
- }
-
- override fun createExplosion(
- entity: Entity?,
- damageSource: DamageSource?,
- behavior: ExplosionBehavior?,
- x: Double,
- y: Double,
- z: Double,
- power: Float,
- createFire: Boolean,
- explosionSourceType: ExplosionSourceType?,
- smallParticle: ParticleEffect?,
- largeParticle: ParticleEffect?,
- soundEvent: RegistryEntry<SoundEvent>?
- ) {
- TODO("Not yet implemented")
- }
-
- override fun asString(): String {
- return "FakeWorld"
- }
-
- override fun getEntityById(id: Int): Entity? {
- return null
- }
-
- override fun getTickManager(): TickManager {
- return TickManager()
- }
-
- override fun getMapState(id: MapIdComponent?): MapState? {
- return null
- }
-
- override fun putMapState(id: MapIdComponent?, state: MapState?) {
- }
-
- override fun increaseAndGetMapId(): MapIdComponent {
- return MapIdComponent(0)
- }
-
- override fun setBlockBreakingInfo(entityId: Int, pos: BlockPos?, progress: Int) {
- }
-
- override fun getScoreboard(): Scoreboard {
- return Scoreboard()
- }
-
- override fun getRecipeManager(): RecipeManager {
- return object : RecipeManager {
- override fun getPropertySet(key: RegistryKey<RecipePropertySet>?): RecipePropertySet {
- return RecipePropertySet.EMPTY
- }
-
- override fun getStonecutterRecipes(): CuttingRecipeDisplay.Grouping<StonecuttingRecipe> {
- return CuttingRecipeDisplay.Grouping.empty()
- }
- }
- }
-
- object FakeEntityLookup : EntityLookup<Entity> {
- override fun get(id: Int): Entity? {
- return null
- }
-
- override fun get(uuid: UUID?): Entity? {
- return null
- }
-
- override fun iterate(): MutableIterable<Entity> {
- return mutableListOf()
- }
-
- override fun <U : Entity?> forEachIntersects(
- filter: TypeFilter<Entity, U>?,
- box: Box?,
- consumer: LazyIterationConsumer<U>?
- ) {
- }
-
- override fun forEachIntersects(box: Box?, action: Consumer<Entity>?) {
- }
-
- override fun <U : Entity?> forEach(filter: TypeFilter<Entity, U>?, consumer: LazyIterationConsumer<U>?) {
- }
-
- }
-
- override fun getEntityLookup(): EntityLookup<Entity> {
- return FakeEntityLookup
- }
-
- override fun getBrewingRecipeRegistry(): BrewingRecipeRegistry {
- return BrewingRecipeRegistry.EMPTY
- }
-
- override fun getFuelRegistry(): FuelRegistry {
- TODO("Not yet implemented")
- }
-}
diff --git a/src/main/kotlin/gui/entity/GuiPlayer.kt b/src/main/kotlin/gui/entity/GuiPlayer.kt
index f728dbf..e7f2e45 100644
--- a/src/main/kotlin/gui/entity/GuiPlayer.kt
+++ b/src/main/kotlin/gui/entity/GuiPlayer.kt
@@ -8,7 +8,6 @@ import net.minecraft.client.util.SkinTextures
import net.minecraft.client.util.SkinTextures.Model
import net.minecraft.client.world.ClientWorld
import net.minecraft.util.Identifier
-import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec3d
import net.minecraft.world.World
@@ -16,13 +15,8 @@ import net.minecraft.world.World
* @see moe.nea.firmament.init.EarlyRiser
*/
fun makeGuiPlayer(world: World): GuiPlayer {
- val constructor = GuiPlayer::class.java.getDeclaredConstructor(
- World::class.java,
- BlockPos::class.java,
- Float::class.javaPrimitiveType,
- GameProfile::class.java
- )
- val player = constructor.newInstance(world, BlockPos.ORIGIN, 0F, GameProfile(UUID.randomUUID(), "Linnea"))
+ val constructor = GuiPlayer::class.java.getDeclaredConstructor(ClientWorld::class.java, GameProfile::class.java)
+ val player = constructor.newInstance(world, GameProfile(UUID.randomUUID(), "Linnea"))
player.postInit()
return player
}
diff --git a/src/main/kotlin/gui/entity/ModifyEquipment.kt b/src/main/kotlin/gui/entity/ModifyEquipment.kt
index a558936..b2c6e5b 100644
--- a/src/main/kotlin/gui/entity/ModifyEquipment.kt
+++ b/src/main/kotlin/gui/entity/ModifyEquipment.kt
@@ -8,10 +8,11 @@ import net.minecraft.entity.LivingEntity
import net.minecraft.item.Item
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.SBItemStack
import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.mc.arbitraryUUID
import moe.nea.firmament.util.mc.setEncodedSkullOwner
-import moe.nea.firmament.util.mc.zeroUUID
object ModifyEquipment : EntityModifier {
val names = mapOf(
@@ -31,12 +32,13 @@ object ModifyEquipment : EntityModifier {
return entity
}
+ @OptIn(ExpensiveItemCacheApi::class)
private fun createItem(item: String): ItemStack {
val split = item.split("#")
if (split.size != 2) return SBItemStack(SkyblockId(item)).asImmutableItemStack()
val (type, data) = split
return when (type) {
- "SKULL" -> ItemStack(Items.PLAYER_HEAD).also { it.setEncodedSkullOwner(zeroUUID, data) }
+ "SKULL" -> ItemStack(Items.PLAYER_HEAD).also { it.setEncodedSkullOwner(arbitraryUUID, data) }
"LEATHER_LEGGINGS" -> coloredLeatherArmor(Items.LEATHER_LEGGINGS, data)
"LEATHER_BOOTS" -> coloredLeatherArmor(Items.LEATHER_BOOTS, data)
"LEATHER_HELMET" -> coloredLeatherArmor(Items.LEATHER_HELMET, data)
@@ -47,7 +49,7 @@ object ModifyEquipment : EntityModifier {
private fun coloredLeatherArmor(leatherArmor: Item, data: String): ItemStack {
val stack = ItemStack(leatherArmor)
- stack.set(DataComponentTypes.DYED_COLOR, DyedColorComponent(data.toInt(16), false))
+ stack.set(DataComponentTypes.DYED_COLOR, DyedColorComponent(data.toInt(16)))
return stack
}
}
diff --git a/src/main/kotlin/gui/entity/ModifyHorse.kt b/src/main/kotlin/gui/entity/ModifyHorse.kt
index f094ca4..c797f14 100644
--- a/src/main/kotlin/gui/entity/ModifyHorse.kt
+++ b/src/main/kotlin/gui/entity/ModifyHorse.kt
@@ -1,12 +1,9 @@
-
package moe.nea.firmament.gui.entity
import com.google.gson.JsonNull
import com.google.gson.JsonObject
-import kotlin.experimental.and
-import kotlin.experimental.inv
-import kotlin.experimental.or
import net.minecraft.entity.EntityType
+import net.minecraft.entity.EquipmentSlot
import net.minecraft.entity.LivingEntity
import net.minecraft.entity.SpawnReason
import net.minecraft.entity.passive.AbstractHorseEntity
@@ -15,48 +12,45 @@ import net.minecraft.item.Items
import moe.nea.firmament.gui.entity.EntityRenderer.fakeWorld
object ModifyHorse : EntityModifier {
- override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
- require(entity is AbstractHorseEntity)
- var entity: AbstractHorseEntity = entity
- info["kind"]?.let {
- entity = when (it.asString) {
- "skeleton" -> EntityType.SKELETON_HORSE.create(fakeWorld, SpawnReason.LOAD)!!
- "zombie" -> EntityType.ZOMBIE_HORSE.create(fakeWorld, SpawnReason.LOAD)!!
- "mule" -> EntityType.MULE.create(fakeWorld, SpawnReason.LOAD)!!
- "donkey" -> EntityType.DONKEY.create(fakeWorld, SpawnReason.LOAD)!!
- "horse" -> EntityType.HORSE.create(fakeWorld, SpawnReason.LOAD)!!
- else -> error("Unknown horse kind $it")
- }
- }
- info["armor"]?.let {
- if (it is JsonNull) {
- entity.setHorseArmor(ItemStack.EMPTY)
- } else {
- when (it.asString) {
- "iron" -> entity.setHorseArmor(ItemStack(Items.IRON_HORSE_ARMOR))
- "golden" -> entity.setHorseArmor(ItemStack(Items.GOLDEN_HORSE_ARMOR))
- "diamond" -> entity.setHorseArmor(ItemStack(Items.DIAMOND_HORSE_ARMOR))
- else -> error("Unknown horse armor $it")
- }
- }
- }
- info["saddled"]?.let {
- entity.setIsSaddled(it.asBoolean)
- }
- return entity
- }
+ override fun apply(entity: LivingEntity, info: JsonObject): LivingEntity {
+ require(entity is AbstractHorseEntity)
+ var entity: AbstractHorseEntity = entity
+ info["kind"]?.let {
+ entity = when (it.asString) {
+ "skeleton" -> EntityType.SKELETON_HORSE.create(fakeWorld, SpawnReason.LOAD)!!
+ "zombie" -> EntityType.ZOMBIE_HORSE.create(fakeWorld, SpawnReason.LOAD)!!
+ "mule" -> EntityType.MULE.create(fakeWorld, SpawnReason.LOAD)!!
+ "donkey" -> EntityType.DONKEY.create(fakeWorld, SpawnReason.LOAD)!!
+ "horse" -> EntityType.HORSE.create(fakeWorld, SpawnReason.LOAD)!!
+ else -> error("Unknown horse kind $it")
+ }
+ }
+ info["armor"]?.let {
+ if (it is JsonNull) {
+ entity.setHorseArmor(ItemStack.EMPTY)
+ } else {
+ when (it.asString) {
+ "iron" -> entity.setHorseArmor(ItemStack(Items.IRON_HORSE_ARMOR))
+ "golden" -> entity.setHorseArmor(ItemStack(Items.GOLDEN_HORSE_ARMOR))
+ "diamond" -> entity.setHorseArmor(ItemStack(Items.DIAMOND_HORSE_ARMOR))
+ else -> error("Unknown horse armor $it")
+ }
+ }
+ }
+ info["saddled"]?.let {
+ entity.setIsSaddled(it.asBoolean)
+ }
+ return entity
+ }
}
fun AbstractHorseEntity.setIsSaddled(shouldBeSaddled: Boolean) {
- val oldFlag = dataTracker.get(AbstractHorseEntity.HORSE_FLAGS)
- dataTracker.set(
- AbstractHorseEntity.HORSE_FLAGS,
- if (shouldBeSaddled) oldFlag or AbstractHorseEntity.SADDLED_FLAG.toByte()
- else oldFlag and AbstractHorseEntity.SADDLED_FLAG.toByte().inv()
- )
+ this.equipStack(EquipmentSlot.SADDLE,
+ if (shouldBeSaddled) ItemStack(Items.SADDLE)
+ else ItemStack.EMPTY)
}
fun AbstractHorseEntity.setHorseArmor(itemStack: ItemStack) {
- items.setStack(1, itemStack)
+ this.equipBodyArmor(itemStack)
}
diff --git a/src/main/kotlin/gui/hud/MoulConfigHud.kt b/src/main/kotlin/gui/hud/MoulConfigHud.kt
index e99b069..8259ebe 100644
--- a/src/main/kotlin/gui/hud/MoulConfigHud.kt
+++ b/src/main/kotlin/gui/hud/MoulConfigHud.kt
@@ -1,66 +1,68 @@
-
package moe.nea.firmament.gui.hud
-import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
import io.github.notenoughupdates.moulconfig.gui.GuiContext
import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigScreenComponent
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.SynchronousResourceReloader
+import net.minecraft.text.Text
import moe.nea.firmament.events.FinalizeResourceManagerEvent
import moe.nea.firmament.events.HudRenderEvent
import moe.nea.firmament.gui.config.HudMeta
+import moe.nea.firmament.jarvis.JarvisIntegration
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.MoulConfigUtils
abstract class MoulConfigHud(
- val name: String,
- val hudMeta: HudMeta,
+ val name: String,
+ val hudMeta: HudMeta,
) {
- companion object {
- private val componentWrapper by lazy {
- object : GuiComponentWrapper(GuiContext(TextComponent("§cERROR"))) {
- init {
- this.client = MC.instance
- }
- }
- }
- }
+ companion object {
+ private val componentWrapper by lazy {
+ object : MoulConfigScreenComponent(Text.empty(), GuiContext(TextComponent("§cERROR")), null) {
+ init {
+ this.client = MC.instance
+ }
+ }
+ }
+ }
- private var fragment: GuiContext? = null
+ private var fragment: GuiContext? = null
- fun forceInit() {
- }
+ fun forceInit() {
+ }
- open fun shouldRender(): Boolean {
- return true
- }
+ open fun shouldRender(): Boolean {
+ return true
+ }
- init {
- require(name.matches("^[a-z_/]+$".toRegex()))
- HudRenderEvent.subscribe("MoulConfigHud:render") {
- if (!shouldRender()) return@subscribe
- val renderContext = componentWrapper.createContext(it.context)
- if (fragment == null)
- loadFragment()
- it.context.matrices.push()
- hudMeta.applyTransformations(it.context.matrices)
- val renderContextTranslated =
- renderContext.translated(hudMeta.absoluteX, hudMeta.absoluteY, hudMeta.width, hudMeta.height)
- .scaled(hudMeta.scale)
- fragment!!.root.render(renderContextTranslated)
- it.context.matrices.pop()
- }
- FinalizeResourceManagerEvent.subscribe("MoulConfigHud:finalizeResourceManager") {
- MC.resourceManager.registerReloader(object : SynchronousResourceReloader {
- override fun reload(manager: ResourceManager?) {
- fragment = null
- }
- })
- }
- }
+ init {
+ require(name.matches("^[a-z_/]+$".toRegex()))
+ HudRenderEvent.subscribe("MoulConfigHud:render") {
+ if (!shouldRender()) return@subscribe
+ val renderContext = componentWrapper.createContext(it.context)
+ if (fragment == null)
+ loadFragment()
+ it.context.matrices.pushMatrix()
+ hudMeta.applyTransformations(it.context.matrices)
+ val pos = hudMeta.getEffectivePosition(JarvisIntegration.jarvis)
+ val renderContextTranslated =
+ renderContext.translated(pos.x(), pos.y(), hudMeta.effectiveWidth, hudMeta.effectiveHeight)
+ .scaled(hudMeta.scale)
+ fragment!!.root.render(renderContextTranslated)
+ it.context.matrices.popMatrix()
+ }
+ FinalizeResourceManagerEvent.subscribe("MoulConfigHud:finalizeResourceManager") {
+ MC.resourceManager.registerReloader(object : SynchronousResourceReloader {
+ override fun reload(manager: ResourceManager?) {
+ fragment = null
+ }
+ })
+ }
+ }
- fun loadFragment() {
- fragment = MoulConfigUtils.loadGui(name, this)
- }
+ fun loadFragment() {
+ fragment = MoulConfigUtils.loadGui(name, this)
+ }
}
diff --git a/src/main/kotlin/jarvis/JarvisIntegration.kt b/src/main/kotlin/jarvis/JarvisIntegration.kt
index 96f47f7..18c46c9 100644
--- a/src/main/kotlin/jarvis/JarvisIntegration.kt
+++ b/src/main/kotlin/jarvis/JarvisIntegration.kt
@@ -9,10 +9,10 @@ import moe.nea.jarvis.api.JarvisPlugin
import net.minecraft.client.gui.screen.Screen
import net.minecraft.text.Text
import moe.nea.firmament.Firmament
-import moe.nea.firmament.features.FeatureManager
import moe.nea.firmament.gui.config.HudMeta
import moe.nea.firmament.gui.config.HudMetaHandler
-import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader
+import moe.nea.firmament.util.data.ManagedConfig
class JarvisIntegration : JarvisPlugin {
override fun getModId(): String =
@@ -27,9 +27,7 @@ class JarvisIntegration : JarvisPlugin {
}
val configs
- get() = listOf(
- RepoManager.Config
- ) + FeatureManager.allFeatures.mapNotNull { it.config }
+ get() = FirmamentConfigLoader.allConfigs.filterIsInstance<ManagedConfig>()
override fun getAllHuds(): List<JarvisHud> {
@@ -39,7 +37,7 @@ class JarvisIntegration : JarvisPlugin {
}
override fun onHudEditorClosed() {
- configs.forEach { it.save() }
+ configs.forEach { it.markDirty() }
}
override fun getAllConfigOptions(): List<JarvisConfigOption> {
diff --git a/src/main/kotlin/keybindings/FirmamentKeyBindings.kt b/src/main/kotlin/keybindings/FirmamentKeyBindings.kt
index e2bed8d..59b131a 100644
--- a/src/main/kotlin/keybindings/FirmamentKeyBindings.kt
+++ b/src/main/kotlin/keybindings/FirmamentKeyBindings.kt
@@ -1,26 +1,25 @@
-
-
package moe.nea.firmament.keybindings
import net.fabricmc.fabric.api.client.keybinding.v1.KeyBindingHelper
import net.minecraft.client.option.KeyBinding
import net.minecraft.client.util.InputUtil
-import moe.nea.firmament.gui.config.KeyBindingHandler
import moe.nea.firmament.gui.config.ManagedOption
+import moe.nea.firmament.util.TestUtil
object FirmamentKeyBindings {
- fun registerKeyBinding(name: String, config: ManagedOption<SavedKeyBinding>) {
- val vanillaKeyBinding = KeyBindingHelper.registerKeyBinding(
- KeyBinding(
- name,
- InputUtil.Type.KEYSYM,
- -1,
- "firmament.key.category"
- )
- )
- keyBindings[vanillaKeyBinding] = config
- }
+ fun registerKeyBinding(name: String, config: ManagedOption<SavedKeyBinding>) {
+ val vanillaKeyBinding = KeyBinding(
+ name,
+ InputUtil.Type.KEYSYM,
+ -1,
+ "firmament.key.category"
+ )
+ if (!TestUtil.isInTest) {
+ KeyBindingHelper.registerKeyBinding(vanillaKeyBinding)
+ }
+ keyBindings[vanillaKeyBinding] = config
+ }
- val keyBindings = mutableMapOf<KeyBinding, ManagedOption<SavedKeyBinding>>()
+ val keyBindings = mutableMapOf<KeyBinding, ManagedOption<SavedKeyBinding>>()
}
diff --git a/src/main/kotlin/keybindings/FirmamentKeyboardState.kt b/src/main/kotlin/keybindings/FirmamentKeyboardState.kt
new file mode 100644
index 0000000..65288bc
--- /dev/null
+++ b/src/main/kotlin/keybindings/FirmamentKeyboardState.kt
@@ -0,0 +1,23 @@
+package moe.nea.firmament.keybindings
+
+import java.util.BitSet
+import org.lwjgl.glfw.GLFW
+
+object FirmamentKeyboardState {
+
+ private val pressedScancodes = BitSet()
+
+ @Synchronized
+ fun isScancodeDown(scancode: Int): Boolean {
+ // TODO: maintain a record of keycodes that were pressed for this scanCode to check if they are still held
+ return pressedScancodes.get(scancode)
+ }
+
+ @Synchronized
+ fun maintainState(key: Int, scancode: Int, action: Int, modifiers: Int) {
+ when (action) {
+ GLFW.GLFW_PRESS -> pressedScancodes.set(scancode)
+ GLFW.GLFW_RELEASE -> pressedScancodes.clear(scancode)
+ }
+ }
+}
diff --git a/src/main/kotlin/keybindings/GenericInputButton.kt b/src/main/kotlin/keybindings/GenericInputButton.kt
new file mode 100644
index 0000000..53c0e56
--- /dev/null
+++ b/src/main/kotlin/keybindings/GenericInputButton.kt
@@ -0,0 +1,289 @@
+package moe.nea.firmament.keybindings
+
+import org.lwjgl.glfw.GLFW
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.buildJsonObject
+import kotlinx.serialization.json.int
+import kotlinx.serialization.json.put
+import net.minecraft.client.MinecraftClient
+import net.minecraft.client.util.InputUtil
+import net.minecraft.text.Text
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.mc.InitLevel
+
+@Serializable(with = GenericInputButton.Serializer::class)
+sealed interface GenericInputButton {
+
+ object Serializer : KSerializer<GenericInputButton> {
+ override val descriptor: SerialDescriptor
+ get() = SerialDescriptor("Firmament:GenericInputButton", JsonElement.serializer().descriptor)
+
+ override fun serialize(
+ encoder: Encoder,
+ value: GenericInputButton
+ ) {
+ JsonElement.serializer().serialize(
+ encoder,
+ when (value) {
+ is KeyCodeButton -> buildJsonObject { put("keyCode", value.keyCode) }
+ is MouseButton -> buildJsonObject { put("mouse", value.mouseButton) }
+ is ScanCodeButton -> buildJsonObject { put("scanCode", value.scanCode) }
+ Unbound -> JsonNull
+ })
+ }
+
+ override fun deserialize(decoder: Decoder): GenericInputButton {
+ val element = JsonElement.serializer().deserialize(decoder)
+ if (element is JsonNull)
+ return Unbound
+ require(element is JsonObject)
+ (element["keyCode"] as? JsonPrimitive)?.let {
+ return KeyCodeButton(it.int)
+ }
+ (element["mouse"] as? JsonPrimitive)?.let {
+ return MouseButton(it.int)
+ }
+ (element["scanCode"] as? JsonPrimitive)?.let {
+ return ScanCodeButton(it.int)
+ }
+ error("Could not parse GenericInputButton: $element")
+ }
+ }
+
+ companion object {
+
+ fun escape() = ofKeyCode(GLFW.GLFW_KEY_ESCAPE)
+ fun ofKeyCode(keyCode: Int): GenericInputButton = KeyCodeButton(keyCode)
+ fun ofScanCode(scanCode: Int): GenericInputButton = ScanCodeButton(scanCode)
+ fun ofScanCodeFromKeyCode(keyCode: Int): GenericInputButton = ScanCodeButton(GLFW.glfwGetKeyScancode(keyCode))
+ fun unbound(): GenericInputButton = Unbound
+ fun mouse(mouseButton: Int): GenericInputButton = MouseButton(mouseButton)
+ fun ofKeyAndScan(keyCode: Int, scanCode: Int): GenericInputButton {
+ if (keyCode == GLFW.GLFW_KEY_UNKNOWN)
+ return ofScanCode(scanCode)
+ return ofKeyCode(keyCode) // TODO: should i always upgrade to a scanCode?
+ }
+ }
+
+ data object Unbound : GenericInputButton {
+ override fun toInputKey(): InputUtil.Key {
+ return InputUtil.UNKNOWN_KEY
+ }
+
+ override fun isBound(): Boolean {
+ return false
+ }
+
+ override fun isPressed(): Boolean {
+ return false
+ }
+ }
+
+ data class MouseButton(
+ val mouseButton: Int
+ ) : GenericInputButton {
+ override fun toInputKey(): InputUtil.Key {
+ return InputUtil.Type.MOUSE.createFromCode(mouseButton)
+ }
+
+ override fun isPressed(): Boolean {
+ return GLFW.glfwGetMouseButton(MC.window.handle, mouseButton) == GLFW.GLFW_PRESS
+ }
+ }
+
+ data class KeyCodeButton(
+ val keyCode: Int
+ ) : GenericInputButton {
+ override fun toInputKey(): InputUtil.Key {
+ return InputUtil.Type.KEYSYM.createFromCode(keyCode)
+ }
+
+ override fun isPressed(): Boolean {
+ return InputUtil.isKeyPressed(MC.window.handle, keyCode)
+ }
+
+ override fun isCtrl(): Boolean {
+ return keyCode in InputModifiers.controlKeys
+ }
+
+ override fun isAlt(): Boolean {
+ return keyCode in InputModifiers.altKeys
+ }
+
+ override fun isShift(): Boolean {
+ return keyCode in InputModifiers.shiftKeys
+ }
+
+ override fun isSuper(): Boolean {
+ return keyCode in InputModifiers.superKeys
+ }
+ }
+
+ data class ScanCodeButton(
+ val scanCode: Int
+ ) : GenericInputButton {
+ override fun toInputKey(): InputUtil.Key {
+ return InputUtil.Type.SCANCODE.createFromCode(scanCode)
+ }
+
+ override fun isPressed(): Boolean {
+ return FirmamentKeyboardState.isScancodeDown(scanCode)
+ }
+ }
+
+ fun isBound() = true
+
+ fun isModifier() = isCtrl() || isAlt() || isSuper() || isShift()
+ fun isCtrl() = false
+ fun isAlt() = false
+ fun isSuper() = false
+ fun isShift() = false
+
+ fun toInputKey(): InputUtil.Key
+ fun format(): Text =
+ if (InitLevel.isAtLeast(InitLevel.RENDER_INIT)) {
+ toInputKey().localizedText
+ } else {
+ Text.of(toString())
+ }
+
+ fun matches(inputAction: GenericInputAction) = inputAction.matches(this)
+ fun isPressed(): Boolean
+}
+
+sealed interface GenericInputAction {
+ fun matches(inputButton: GenericInputButton): Boolean
+
+ data class MouseInput(
+ val mouseButton: Int
+ ) : GenericInputAction {
+ override fun matches(inputButton: GenericInputButton): Boolean {
+ return inputButton is GenericInputButton.MouseButton && inputButton.mouseButton == mouseButton
+ }
+ }
+
+ data class KeyboardInput(
+ val keyCode: Int,
+ val scanCode: Int,
+ ) : GenericInputAction {
+ override fun matches(inputButton: GenericInputButton): Boolean {
+ return when (inputButton) {
+ is GenericInputButton.KeyCodeButton -> inputButton.keyCode == keyCode
+ is GenericInputButton.ScanCodeButton -> inputButton.scanCode == scanCode
+ else -> false
+ }
+ }
+ }
+
+ companion object {
+ @JvmStatic
+ fun mouse(mouseButton: Int): GenericInputAction = MouseInput(mouseButton)
+
+ @JvmStatic
+ fun key(keyCode: Int, scanCode: Int): GenericInputAction = KeyboardInput(keyCode, scanCode)
+ }
+}
+
+@Serializable
+data class InputModifiers(
+ val modifiers: Int
+) {
+ companion object {
+ @JvmStatic
+ fun current(): InputModifiers {
+ val h = MC.window.handle
+ val ctrl = if (MinecraftClient.IS_SYSTEM_MAC) {
+ InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SUPER)
+ || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_SUPER)
+ } else InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_CONTROL)
+ || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_CONTROL)
+ val shift = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SHIFT) || InputUtil.isKeyPressed(
+ h,
+ GLFW.GLFW_KEY_RIGHT_SHIFT
+ )
+ val alt = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_ALT)
+ || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_ALT)
+ val `super` = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SUPER)
+ || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_SUPER)
+ return of(
+ ctrl = ctrl,
+ shift = shift,
+ alt = alt,
+ `super` = `super`,
+ )
+ }
+
+
+ val superKeys = listOf(GLFW.GLFW_KEY_LEFT_SUPER, GLFW.GLFW_KEY_RIGHT_SUPER)
+ val controlKeys = if (MinecraftClient.IS_SYSTEM_MAC) {
+ listOf(GLFW.GLFW_KEY_LEFT_SUPER, GLFW.GLFW_KEY_RIGHT_SUPER)
+ } else {
+ listOf(GLFW.GLFW_KEY_LEFT_CONTROL, GLFW.GLFW_KEY_RIGHT_CONTROL)
+ }
+ val shiftKeys = listOf(GLFW.GLFW_KEY_LEFT_SHIFT, GLFW.GLFW_KEY_RIGHT_SHIFT)
+ val altKeys = listOf(GLFW.GLFW_KEY_LEFT_ALT, GLFW.GLFW_KEY_RIGHT_ALT)
+
+ fun of(
+ vararg useNamedArgs: Boolean,
+ ctrl: Boolean = false,
+ shift: Boolean = false,
+ alt: Boolean = false,
+ `super`: Boolean = false
+ ): InputModifiers {
+ require(useNamedArgs.isEmpty())
+ return InputModifiers(
+ (if (ctrl) GLFW.GLFW_MOD_CONTROL else 0)
+ or (if (shift) GLFW.GLFW_MOD_SHIFT else 0)
+ or (if (alt) GLFW.GLFW_MOD_ALT else 0)
+ or (if (`super`) GLFW.GLFW_MOD_SUPER else 0)
+ )
+ }
+
+ @JvmStatic
+ fun of(modifiers: Int) = InputModifiers(modifiers)
+
+ fun none(): InputModifiers {
+ return InputModifiers(0)
+ }
+ }
+
+ fun isAtLeast(other: InputModifiers): Boolean {
+ return this.modifiers and other.modifiers == this.modifiers
+ }
+
+ fun isEmpty() = modifiers == 0
+
+ fun getFlag(flag: Int) = modifiers and flag != 0
+ val ctrl get() = getFlag(GLFW.GLFW_MOD_CONTROL) // TODO: consult someone on control vs command again
+ val shift get() = getFlag(GLFW.GLFW_MOD_SHIFT)
+ val alt get() = getFlag(GLFW.GLFW_MOD_ALT)
+ val `super` get() = getFlag(GLFW.GLFW_MOD_SUPER)
+
+ override fun toString(): String {
+ return listOfNotNull(
+ if (ctrl) "CTRL" else null,
+ if (shift) "SHIFT" else null,
+ if (alt) "ALT" else null,
+ if (`super`) "SUPER" else null,
+ ).joinToString(" + ")
+ }
+
+ fun matches(other: InputModifiers, atLeast: Boolean): Boolean {
+ if (atLeast)
+ return isAtLeast(other)
+ return this == other
+ }
+
+ fun format(): Text { // TODO: translation for mods
+ return Text.of(toString())
+ }
+
+}
diff --git a/src/main/kotlin/keybindings/IKeyBinding.kt b/src/main/kotlin/keybindings/IKeyBinding.kt
deleted file mode 100644
index 1975361..0000000
--- a/src/main/kotlin/keybindings/IKeyBinding.kt
+++ /dev/null
@@ -1,29 +0,0 @@
-
-
-package moe.nea.firmament.keybindings
-
-import net.minecraft.client.option.KeyBinding
-
-interface IKeyBinding {
- fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean
-
- fun withModifiers(wantedModifiers: Int): IKeyBinding {
- val old = this
- return object : IKeyBinding {
- override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
- return old.matches(keyCode, scanCode, modifiers) && (modifiers and wantedModifiers) == wantedModifiers
- }
- }
- }
-
- companion object {
- fun minecraft(keyBinding: KeyBinding) = object : IKeyBinding {
- override fun matches(keyCode: Int, scanCode: Int, modifiers: Int) =
- keyBinding.matchesKey(keyCode, scanCode)
- }
-
- fun ofKeyCode(wantedKeyCode: Int) = object : IKeyBinding {
- override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean = keyCode == wantedKeyCode
- }
- }
-}
diff --git a/src/main/kotlin/keybindings/SavedKeyBinding.kt b/src/main/kotlin/keybindings/SavedKeyBinding.kt
index 5bca87e..48716d3 100644
--- a/src/main/kotlin/keybindings/SavedKeyBinding.kt
+++ b/src/main/kotlin/keybindings/SavedKeyBinding.kt
@@ -1,106 +1,48 @@
-
-
package moe.nea.firmament.keybindings
-import org.lwjgl.glfw.GLFW
import kotlinx.serialization.Serializable
-import net.minecraft.client.MinecraftClient
-import net.minecraft.client.util.InputUtil
import net.minecraft.text.Text
-import moe.nea.firmament.util.MC
@Serializable
data class SavedKeyBinding(
- val keyCode: Int,
- val shift: Boolean = false,
- val ctrl: Boolean = false,
- val alt: Boolean = false,
-) : IKeyBinding {
- val isBound: Boolean get() = keyCode != GLFW.GLFW_KEY_UNKNOWN
-
- constructor(keyCode: Int, mods: Triple<Boolean, Boolean, Boolean>) : this(
- keyCode,
- mods.first && keyCode != GLFW.GLFW_KEY_LEFT_SHIFT && keyCode != GLFW.GLFW_KEY_RIGHT_SHIFT,
- mods.second && keyCode != GLFW.GLFW_KEY_LEFT_CONTROL && keyCode != GLFW.GLFW_KEY_RIGHT_CONTROL,
- mods.third && keyCode != GLFW.GLFW_KEY_LEFT_ALT && keyCode != GLFW.GLFW_KEY_RIGHT_ALT,
- )
-
- constructor(keyCode: Int, mods: Int) : this(keyCode, getMods(mods))
-
- companion object {
- fun getMods(modifiers: Int): Triple<Boolean, Boolean, Boolean> {
- return Triple(
- modifiers and GLFW.GLFW_MOD_SHIFT != 0,
- modifiers and GLFW.GLFW_MOD_CONTROL != 0,
- modifiers and GLFW.GLFW_MOD_ALT != 0,
- )
- }
-
- fun getModInt(): Int {
- val h = MC.window.handle
- val ctrl = if (MinecraftClient.IS_SYSTEM_MAC) {
- InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SUPER)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_SUPER)
- } else InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_CONTROL)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_CONTROL)
- val shift = isShiftDown()
- val alt = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_ALT)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_ALT)
- var mods = 0
- if (ctrl) mods = mods or GLFW.GLFW_MOD_CONTROL
- if (shift) mods = mods or GLFW.GLFW_MOD_SHIFT
- if (alt) mods = mods or GLFW.GLFW_MOD_ALT
- return mods
- }
-
- private val h get() = MC.window.handle
- fun isShiftDown() = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SHIFT)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_SHIFT)
-
- }
-
- fun isPressed(atLeast: Boolean = false): Boolean {
- if (!isBound) return false
- val h = MC.window.handle
- if (!InputUtil.isKeyPressed(h, keyCode)) return false
-
- val ctrl = if (MinecraftClient.IS_SYSTEM_MAC) {
- InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_SUPER)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_SUPER)
- } else InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_CONTROL)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_CONTROL)
- val shift = isShiftDown()
- val alt = InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_LEFT_ALT)
- || InputUtil.isKeyPressed(h, GLFW.GLFW_KEY_RIGHT_ALT)
- if (atLeast)
- return (ctrl >= this.ctrl) &&
- (alt >= this.alt) &&
- (shift >= this.shift)
-
- return (ctrl == this.ctrl) &&
- (alt == this.alt) &&
- (shift == this.shift)
- }
-
- override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
- if (this.keyCode == GLFW.GLFW_KEY_UNKNOWN) return false
- return keyCode == this.keyCode && getMods(modifiers) == Triple(shift, ctrl, alt)
- }
-
- fun format(): Text {
- val stroke = Text.literal("")
- if (ctrl) {
- stroke.append("CTRL + ")
- }
- if (alt) {
- stroke.append("ALT + ")
- }
- if (shift) {
- stroke.append("SHIFT + ") // TODO: translations?
- }
-
- stroke.append(InputUtil.Type.KEYSYM.createFromCode(keyCode).localizedText)
- return stroke
- }
+ val button: GenericInputButton,
+ val modifiers: InputModifiers,
+) {
+ companion object {
+ fun isShiftDown() = InputModifiers.current().shift
+
+ fun unbound(): SavedKeyBinding = withoutMods(GenericInputButton.unbound())
+ fun withoutMods(input: GenericInputButton) = SavedKeyBinding(input, InputModifiers.none())
+ fun keyWithoutMods(keyCode: Int): SavedKeyBinding = withoutMods(GenericInputButton.ofKeyCode(keyCode))
+ fun keyWithMods(keyCode: Int, mods: InputModifiers) =
+ SavedKeyBinding(GenericInputButton.ofKeyCode(keyCode), mods)
+ }
+
+ fun isPressed(atLeast: Boolean = false): Boolean {
+ if (!button.isPressed())
+ return false
+ val mods = InputModifiers.current()
+ return mods.matches(this.modifiers, atLeast)
+ }
+
+ override fun toString(): String {
+ return format().string
+ }
+
+ fun format(): Text {
+ val stroke = Text.empty()
+ if (!modifiers.isEmpty()) {
+ stroke.append(modifiers.format())
+ stroke.append(" + ")
+ }
+ stroke.append(button.format())
+ return stroke
+ }
+
+ val isBound: Boolean get() = button.isBound()
+ fun matches(action: GenericInputAction, inputModifiers: InputModifiers, atLeast: Boolean = false): Boolean {
+ return action.matches(button) && this.modifiers.matches(inputModifiers, atLeast)
+ }
}
+
diff --git a/src/main/kotlin/repo/BetterRepoRecipeCache.kt b/src/main/kotlin/repo/BetterRepoRecipeCache.kt
index 91a6b50..6d18223 100644
--- a/src/main/kotlin/repo/BetterRepoRecipeCache.kt
+++ b/src/main/kotlin/repo/BetterRepoRecipeCache.kt
@@ -1,28 +1,31 @@
-
package moe.nea.firmament.repo
import io.github.moulberry.repo.IReloadable
import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.data.NEUNpcShopRecipe
import io.github.moulberry.repo.data.NEURecipe
import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.skyblockId
-class BetterRepoRecipeCache(val essenceRecipeProvider: EssenceRecipeProvider) : IReloadable {
- var usages: Map<SkyblockId, Set<NEURecipe>> = mapOf()
- var recipes: Map<SkyblockId, Set<NEURecipe>> = mapOf()
+class BetterRepoRecipeCache(vararg val extraProviders: ExtraRecipeProvider) : IReloadable {
+ var usages: Map<SkyblockId, Set<NEURecipe>> = mapOf()
+ var recipes: Map<SkyblockId, Set<NEURecipe>> = mapOf()
- override fun reload(repository: NEURepository) {
- val usages = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
- val recipes = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
- val baseRecipes = repository.items.items.values
- .asSequence()
- .flatMap { it.recipes }
- val extraRecipes = essenceRecipeProvider.recipes
- (baseRecipes + extraRecipes)
- .forEach { recipe ->
- recipe.allInputs.forEach { usages.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
- recipe.allOutputs.forEach { recipes.getOrPut(SkyblockId(it.itemId), ::mutableSetOf).add(recipe) }
- }
- this.usages = usages
- this.recipes = recipes
- }
+ override fun reload(repository: NEURepository) {
+ val usages = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
+ val recipes = mutableMapOf<SkyblockId, MutableSet<NEURecipe>>()
+ val baseRecipes = repository.items.items.values
+ .asSequence()
+ .flatMap { it.recipes }
+ (baseRecipes + extraProviders.flatMap { it.provideExtraRecipes() })
+ .forEach { recipe ->
+ if (recipe is NEUNpcShopRecipe) {
+ usages.getOrPut(recipe.isSoldBy.skyblockId, ::mutableSetOf).add(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<NEUIngredient>,
- ) : NEURecipe {
- val essenceIngredient= NEUIngredient.fromString("${essenceType}:$essenceCost")
- val allUpgradeComponents = listOf(essenceIngredient) + extraItems
+class EssenceRecipeProvider : IReloadable, ExtraRecipeProvider {
+ data class EssenceUpgradeRecipe(
+ val itemId: SkyblockId,
+ val starCountAfter: Int,
+ val essenceCost: Int,
+ val essenceType: String, // TODO: replace with proper type
+ val extraItems: List<NEUIngredient>,
+ ) : NEURecipe {
+ val essenceIngredient = NEUIngredient.fromString("${essenceType}:$essenceCost")
+ val allUpgradeComponents = listOf(essenceIngredient) + extraItems
+
+ override fun getAllInputs(): Collection<NEUIngredient> {
+ return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + allUpgradeComponents
+ }
- override fun getAllInputs(): Collection<NEUIngredient> {
- return listOf(NEUIngredient.fromString(itemId.neuItem + ":1")) + allUpgradeComponents
- }
+ override fun getAllOutputs(): Collection<NEUIngredient> {
+ return listOf(NEUIngredient.fromString(itemId.neuItem + ":1"))
+ }
+ }
- override fun getAllOutputs(): Collection<NEUIngredient> {
- return listOf(NEUIngredient.fromString(itemId.neuItem + ":1"))
- }
- }
+ var recipes = listOf<EssenceUpgradeRecipe>()
+ private set
- var recipes = listOf<EssenceUpgradeRecipe>()
- private set
+ override fun provideExtraRecipes(): Iterable<NEURecipe> = recipes
- override fun reload(repository: NEURepository) {
- val recipes = mutableListOf<EssenceUpgradeRecipe>()
- for ((neuId, costs) in repository.constants.essenceCost.costs) {
- // TODO: add dungeonization costs. this is in repo, but not in the repo parser.
- for ((starCountAfter, essenceCost) in costs.essenceCosts.entries) {
- val items = costs.itemCosts[starCountAfter] ?: emptyList()
- recipes.add(
- EssenceUpgradeRecipe(
- SkyblockId(neuId),
- starCountAfter,
- essenceCost,
- "ESSENCE_" + costs.type.uppercase(), // how flimsy
- items.map { NEUIngredient.fromString(it) }))
- }
- }
- this.recipes = recipes
- }
+ override fun reload(repository: NEURepository) {
+ val recipes = mutableListOf<EssenceUpgradeRecipe>()
+ for ((neuId, costs) in repository.constants.essenceCost.costs) {
+ // TODO: add dungeonization costs. this is in repo, but not in the repo parser.
+ for ((starCountAfter, essenceCost) in costs.essenceCosts.entries) {
+ val items = costs.itemCosts[starCountAfter] ?: emptyList()
+ recipes.add(
+ EssenceUpgradeRecipe(
+ SkyblockId(neuId),
+ starCountAfter,
+ essenceCost,
+ "ESSENCE_" + costs.type.uppercase(), // how flimsy
+ items.map { NEUIngredient.fromString(it) }))
+ }
+ }
+ this.recipes = recipes
+ }
}
diff --git a/src/main/kotlin/repo/ExpLadder.kt b/src/main/kotlin/repo/ExpLadder.kt
index fbc9eb8..25a74de 100644
--- a/src/main/kotlin/repo/ExpLadder.kt
+++ b/src/main/kotlin/repo/ExpLadder.kt
@@ -19,7 +19,8 @@ object ExpLadders : IReloadable {
val expInCurrentLevel: Float,
var expTotal: Float,
) {
- val percentageToNextLevel: Float = expInCurrentLevel / expRequiredForNextLevel
+ val percentageToNextLevel: Float = expInCurrentLevel / expRequiredForNextLevel
+ val percentageToMaxLevel: Float = expTotal / expRequiredForMaxLevel
}
data class ExpLadder(
diff --git a/src/main/kotlin/repo/ExpensiveItemCacheApi.kt b/src/main/kotlin/repo/ExpensiveItemCacheApi.kt
new file mode 100644
index 0000000..eef95a6
--- /dev/null
+++ b/src/main/kotlin/repo/ExpensiveItemCacheApi.kt
@@ -0,0 +1,8 @@
+package moe.nea.firmament.repo
+
+/**
+ * Marker for functions that could potentially invoke DFU. Please do not call on a lot of objects at once, or try to make sure the item is cached and fall back to a more gentle function call using [SBItemStack.isWarm] and similar functions.
+ */
+@RequiresOptIn
+@Retention(AnnotationRetention.BINARY)
+annotation class ExpensiveItemCacheApi
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<NEURecipe>
+}
diff --git a/src/main/kotlin/repo/HypixelStaticData.kt b/src/main/kotlin/repo/HypixelStaticData.kt
index 181aa70..b0ada77 100644
--- a/src/main/kotlin/repo/HypixelStaticData.kt
+++ b/src/main/kotlin/repo/HypixelStaticData.kt
@@ -3,21 +3,17 @@ package moe.nea.firmament.repo
import io.ktor.client.call.body
import io.ktor.client.request.get
import org.apache.logging.log4j.LogManager
-import org.lwjgl.glfw.GLFW
import kotlinx.coroutines.async
import kotlinx.coroutines.awaitAll
import kotlinx.coroutines.delay
import kotlinx.coroutines.launch
-import kotlinx.coroutines.withTimeoutOrNull
import kotlinx.serialization.SerialName
import kotlinx.serialization.Serializable
import kotlin.time.Duration.Companion.minutes
import moe.nea.firmament.Firmament
import moe.nea.firmament.apis.CollectionResponse
import moe.nea.firmament.apis.CollectionSkillData
-import moe.nea.firmament.keybindings.IKeyBinding
import moe.nea.firmament.util.SkyblockId
-import moe.nea.firmament.util.async.waitForInput
object HypixelStaticData {
private val logger = LogManager.getLogger("Firmament.HypixelStaticData")
@@ -25,7 +21,13 @@ object HypixelStaticData {
private val hypixelApiBaseUrl = "https://api.hypixel.net"
var lowestBin: Map<SkyblockId, Double> = mapOf()
private set
- var bazaarData: Map<SkyblockId, BazaarData> = mapOf()
+ var avg1dlowestBin: Map<SkyblockId, Double> = mapOf()
+ private set
+ var avg3dlowestBin: Map<SkyblockId, Double> = mapOf()
+ private set
+ var avg7dlowestBin: Map<SkyblockId, Double> = mapOf()
+ private set
+ var bazaarData: Map<SkyblockId.BazaarStock, BazaarData> = mapOf()
private set
var collectionData: Map<String, CollectionSkillData> = mapOf()
private set
@@ -56,9 +58,10 @@ object HypixelStaticData {
val products: Map<SkyblockId.BazaarStock, BazaarData> = mapOf(),
)
- fun getPriceOfItem(item: SkyblockId): Double? = bazaarData[item]?.quickStatus?.buyPrice ?: lowestBin[item]
- fun hasBazaarStock(item: SkyblockId): Boolean {
+ fun getPriceOfItem(item: SkyblockId): Double? = bazaarData[SkyblockId.BazaarStock.fromSkyBlockId(item)]?.quickStatus?.buyPrice ?: lowestBin[item]
+
+ fun hasBazaarStock(item: SkyblockId.BazaarStock): Boolean {
return item in bazaarData
}
@@ -90,6 +93,12 @@ object HypixelStaticData {
private suspend fun fetchPricesFromMoulberry() {
lowestBin = Firmament.httpClient.get("$moulberryBaseUrl/lowestbin.json")
.body<Map<SkyblockId, Double>>()
+ avg1dlowestBin = Firmament.httpClient.get("$moulberryBaseUrl/auction_averages_lbin/1day.json")
+ .body<Map<SkyblockId, Double>>()
+ avg3dlowestBin = Firmament.httpClient.get("$moulberryBaseUrl/auction_averages_lbin/3day.json")
+ .body<Map<SkyblockId, Double>>()
+ avg7dlowestBin = Firmament.httpClient.get("$moulberryBaseUrl/auction_averages_lbin/7day.json")
+ .body<Map<SkyblockId, Double>>()
}
private suspend fun fetchBazaarPrices() {
@@ -97,7 +106,7 @@ object HypixelStaticData {
if (!response.success) {
logger.warn("Retrieved unsuccessful bazaar data")
}
- bazaarData = response.products.mapKeys { it.key.toRepoId() }
+ bazaarData = response.products
}
private suspend fun updateCollectionData() {
diff --git a/src/main/kotlin/repo/ItemCache.kt b/src/main/kotlin/repo/ItemCache.kt
index 014de7d..0aa4a44 100644
--- a/src/main/kotlin/repo/ItemCache.kt
+++ b/src/main/kotlin/repo/ItemCache.kt
@@ -4,16 +4,21 @@ import com.mojang.serialization.Dynamic
import io.github.moulberry.repo.IReloadable
import io.github.moulberry.repo.NEURepository
import io.github.moulberry.repo.data.NEUItem
-import io.github.notenoughupdates.moulconfig.xml.Bind
import java.text.NumberFormat
import java.util.UUID
import java.util.concurrent.ConcurrentHashMap
import org.apache.logging.log4j.LogManager
-import kotlinx.coroutines.Job
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.async
+import kotlinx.coroutines.awaitAll
+import kotlinx.coroutines.cancel
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
+import kotlin.io.path.readText
import kotlin.jvm.optionals.getOrNull
import net.minecraft.SharedConstants
-import net.minecraft.client.resource.language.I18n
import net.minecraft.component.DataComponentTypes
import net.minecraft.component.type.NbtComponent
import net.minecraft.datafixer.Schemas
@@ -24,21 +29,30 @@ import net.minecraft.nbt.NbtCompound
import net.minecraft.nbt.NbtElement
import net.minecraft.nbt.NbtOps
import net.minecraft.nbt.NbtString
+import net.minecraft.nbt.StringNbtReader
+import net.minecraft.text.MutableText
+import net.minecraft.text.Style
import net.minecraft.text.Text
+import net.minecraft.util.Identifier
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.features.debug.ExportedTestConstantMeta
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.MinecraftDispatcher
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.TestUtil
+import moe.nea.firmament.util.directLiteralStringContent
import moe.nea.firmament.util.mc.FirmamentDataComponentTypes
import moe.nea.firmament.util.mc.appendLore
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loadItemFromNbt
+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.skyblockId
+import moe.nea.firmament.util.transformEachRecursively
object ItemCache : IReloadable {
private val cache: MutableMap<String, ItemStack> = ConcurrentHashMap()
@@ -54,13 +68,18 @@ object ItemCache : IReloadable {
putShort("Damage", damage.toShort())
}
- private fun NbtCompound.transformFrom10809ToModern(): NbtCompound? =
+ @ExpensiveItemCacheApi
+ private fun NbtCompound.transformFrom10809ToModern() = convert189ToModern(this@transformFrom10809ToModern)
+ val currentSaveVersion = SharedConstants.getGameVersion().dataVersion().id
+
+ @ExpensiveItemCacheApi
+ fun convert189ToModern(nbtComponent: NbtCompound): NbtCompound? =
try {
df.update(
TypeReferences.ITEM_STACK,
- Dynamic(NbtOps.INSTANCE, this),
+ Dynamic(NbtOps.INSTANCE, nbtComponent),
-1,
- SharedConstants.getGameVersion().saveVersion.id
+ currentSaveVersion
).value as NbtCompound
} catch (e: Exception) {
isFlawless = false
@@ -70,6 +89,12 @@ object ItemCache : IReloadable {
val ItemStack.isBroken
get() = get(FirmamentDataComponentTypes.IS_BROKEN) ?: false
+
+ fun ItemStack.withFallback(fallback: ItemStack?): ItemStack {
+ if (isBroken && fallback != null) return fallback
+ return this
+ }
+
fun brokenItemStack(neuItem: NEUItem?, idHint: SkyblockId? = null): ItemStack {
return ItemStack(Items.PAINTING).apply {
setCustomName(Text.literal(neuItem?.displayName ?: idHint?.neuItem ?: "null"))
@@ -88,16 +113,77 @@ object ItemCache : IReloadable {
}
}
+ fun un189Lore(lore: String): MutableText {
+ 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
+ }
+
+ fun tryFindFromModernFormat(skyblockId: SkyblockId): NbtCompound? {
+ val overlayFile =
+ RepoManager.overlayData.getMostModernReadableOverlay(skyblockId, currentSaveVersion) ?: return null
+ val overlay = StringNbtReader.readCompound(overlayFile.path.readText())
+ val result = ExportedTestConstantMeta.SOURCE_CODEC.decode(
+ NbtOps.INSTANCE, overlay
+ ).result().getOrNull() ?: return null
+ val meta = result.first
+ return df.update(
+ TypeReferences.ITEM_STACK,
+ Dynamic(NbtOps.INSTANCE, result.second),
+ meta.dataVersion,
+ currentSaveVersion
+ ).value as NbtCompound
+ }
+
+ @ExpensiveItemCacheApi
private fun NEUItem.asItemStackNow(): ItemStack {
+
try {
+ var modernItemTag = tryFindFromModernFormat(this.skyblockId)
val oldItemTag = get10809CompoundTag()
- val modernItemTag = oldItemTag.transformFrom10809ToModern()
- ?: return brokenItemStack(this)
+ var usedOldNbt = false
+ if (modernItemTag == null) {
+ usedOldNbt = true
+ modernItemTag = oldItemTag.transformFrom10809ToModern()
+ ?: return brokenItemStack(this)
+ }
val itemInstance =
- ItemStack.fromNbt(MC.defaultRegistries, modernItemTag).getOrNull() ?: return brokenItemStack(this)
- val extraAttributes = oldItemTag.getCompound("tag").getCompound("ExtraAttributes")
- if (extraAttributes != null)
- itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes))
+ loadItemFromNbt( modernItemTag) ?: return brokenItemStack(this)
+ if (usedOldNbt) {
+ val tag = oldItemTag.getCompound("tag")
+ val extraAttributes = tag.flatMap { it.getCompound("ExtraAttributes") }
+ .getOrNull()
+ if (extraAttributes != null)
+ itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes))
+ val itemModel = tag.flatMap { it.getString("ItemModel") }.getOrNull()
+ if (itemModel != null)
+ itemInstance.set(DataComponentTypes.ITEM_MODEL, Identifier.of(itemModel))
+ }
+ itemInstance.loreAccordingToNbt = lore.map { un189Lore(it) }
+ itemInstance.displayNameAccordingToNbt = un189Lore(displayName)
return itemInstance
} catch (e: Exception) {
e.printStackTrace()
@@ -105,6 +191,11 @@ object ItemCache : IReloadable {
}
}
+ fun hasCacheFor(skyblockId: SkyblockId): Boolean {
+ return skyblockId.neuItem in cache
+ }
+
+ @ExpensiveItemCacheApi
fun NEUItem?.asItemStack(idHint: SkyblockId? = null, loreReplacements: Map<String, String>? = null): ItemStack {
if (this == null) return brokenItemStack(null, idHint)
var s = cache[this.skyblockItemId]
@@ -129,65 +220,58 @@ object ItemCache : IReloadable {
}
fun Text.applyLoreReplacements(loreReplacements: Map<String, String>): Text {
- assert(this.siblings.isEmpty())
- var string = this.string
- loreReplacements.forEach { (find, replace) ->
- string = string.replace("{$find}", replace)
+ return this.transformEachRecursively {
+ var string = it.directLiteralStringContent ?: return@transformEachRecursively it
+ loreReplacements.forEach { (find, replace) ->
+ string = string.replace("{$find}", replace)
+ }
+ Text.literal(string).setStyle(it.style)
}
- return Text.literal(string).styled { this.style }
}
- var job: Job? = null
-
- object ReloadProgressHud : MoulConfigHud(
- "repo_reload", HudMeta(HudPosition(0.0, 0.0, 1F), Text.literal("Repo Reload"), 180, 18)) {
-
-
- var isEnabled = false
- override fun shouldRender(): Boolean {
- return isEnabled
- }
-
- @get:Bind("current")
- var current: Double = 0.0
-
- @get:Bind("label")
- var label: String = ""
+ var itemRecacheScope: CoroutineScope? = null
- @get:Bind("max")
- var max: Double = 0.0
+ private var recacheSoonSubmitted = mutableSetOf<SkyblockId>()
- fun reportProgress(label: String, current: Int, max: Int) {
- this.label = label
- this.current = current.toDouble()
- this.max = max.toDouble()
+ @OptIn(ExpensiveItemCacheApi::class)
+ fun recacheSoon(neuItem: NEUItem) {
+ itemRecacheScope?.launch {
+ if (!withContext(MinecraftDispatcher) {
+ recacheSoonSubmitted.add(neuItem.skyblockId)
+ }) {
+ return@launch
+ }
+ neuItem.asItemStack()
}
}
+ @OptIn(ExpensiveItemCacheApi::class)
override fun reload(repository: NEURepository) {
- val j = job
- if (j != null && j.isActive) {
- j.cancel()
- }
+ val j = itemRecacheScope
+ j?.cancel("New reload invoked")
cache.clear()
isFlawless = true
if (TestUtil.isInTest) return
- job = Firmament.coroutineScope.launch {
- val items = repository.items?.items
- if (items == null) {
- ReloadProgressHud.isEnabled = false
- return@launch
- }
- val recacheItems = I18n.translate("firmament.repo.cache")
- ReloadProgressHud.reportProgress(recacheItems, 0, items.size)
- ReloadProgressHud.isEnabled = true
- var i = 0
- items.values.forEach {
- it.asItemStack() // Rebuild cache
- ReloadProgressHud.reportProgress(recacheItems, i++, items.size)
- }
- ReloadProgressHud.isEnabled = false
+ val newScope =
+ CoroutineScope(
+ Firmament.coroutineScope.coroutineContext +
+ SupervisorJob(Firmament.globalJob) +
+ Dispatchers.Default.limitedParallelism(
+ (Runtime.getRuntime().availableProcessors() / 4).coerceAtLeast(1)
+ )
+ )
+ val items = repository.items?.items
+ newScope.launch {
+ val items = items ?: return@launch
+ items.values.chunked(500).map { chunk ->
+ async {
+ chunk.forEach {
+ it.asItemStack() // Rebuild cache
+ }
+ }
+ }.awaitAll()
}
+ itemRecacheScope = newScope
}
fun coinItem(coinAmount: Int): ItemStack {
diff --git a/src/main/kotlin/repo/MiningRepoData.kt b/src/main/kotlin/repo/MiningRepoData.kt
new file mode 100644
index 0000000..a987ab1
--- /dev/null
+++ b/src/main/kotlin/repo/MiningRepoData.kt
@@ -0,0 +1,131 @@
+package moe.nea.firmament.repo
+
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepository
+import java.util.Collections
+import java.util.NavigableMap
+import java.util.TreeMap
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import kotlinx.serialization.serializer
+import kotlin.streams.asSequence
+import net.minecraft.block.Block
+import net.minecraft.item.BlockItem
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.text.Text
+import moe.nea.firmament.repo.ReforgeStore.kJson
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.SkyBlockIsland
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.mc.FirmamentDataComponentTypes
+import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.mc.loadItemFromNbt
+import moe.nea.firmament.util.skyblockId
+
+class MiningRepoData : IReloadable {
+ var customMiningAreas: Map<SkyBlockIsland, CustomMiningArea> = mapOf()
+ private set
+ var customMiningBlocks: List<CustomMiningBlock> = listOf()
+ private set
+ var toolsByBreakingPower: NavigableMap<BreakingPowerKey, SBItemStack> = Collections.emptyNavigableMap()
+ private set
+
+
+ data class BreakingPowerKey(
+ val breakingPower: Int,
+ val itemId: SkyblockId? = null
+ ) {
+ companion object {
+ val COMPARATOR: Comparator<BreakingPowerKey> =
+ Comparator
+ .comparingInt<BreakingPowerKey> { it.breakingPower }
+ .thenComparing(Comparator.comparing(
+ { it.itemId },
+ nullsFirst(Comparator.comparing<SkyblockId, Boolean> { "PICK" in it.neuItem || "BING" in it.neuItem }.thenComparing(Comparator.naturalOrder<SkyblockId>()))))
+ }
+ }
+
+ override fun reload(repo: NEURepository) {
+ customMiningAreas = repo.file("mining/custom_mining_areas.json")
+ ?.kJson(serializer()) ?: mapOf()
+ customMiningBlocks = repo.tree("mining/blocks")
+ .asSequence()
+ .filter { it.path.endsWith(".json") }
+ .map { it.kJson(serializer<CustomMiningBlock>()) }
+ .toList()
+ toolsByBreakingPower = Collections.unmodifiableNavigableMap(
+ repo.items.items
+ .values
+ .asSequence()
+ .map { SBItemStack(it.skyblockId) }
+ .filter { it.breakingPower > 0 }
+ .associateTo(TreeMap<BreakingPowerKey, SBItemStack>(BreakingPowerKey.COMPARATOR)) {
+ BreakingPowerKey(it.breakingPower, it.skyblockId) to it
+ })
+ }
+
+ fun getToolsThatCanBreak(breakingPower: Int): Collection<SBItemStack> {
+ return toolsByBreakingPower.tailMap(BreakingPowerKey(breakingPower, null), true).values
+ }
+
+ @Serializable
+ data class CustomMiningBlock(
+ val breakingPower: Int = 0,
+ val blockStrength: Int = 0,
+ val name: String? = null,
+ val baseDrop: SkyblockId? = null,
+ val blocks189: List<Block189> = emptyList()
+ ) {
+ @Transient
+ val dropItem = baseDrop?.let(::SBItemStack)
+ @OptIn(ExpensiveItemCacheApi::class)
+ private val labeledStack by lazy {
+ dropItem?.asCopiedItemStack()?.also(::markItemStack)
+ }
+
+ private fun markItemStack(itemStack: ItemStack) {
+ itemStack.set(FirmamentDataComponentTypes.CUSTOM_MINING_BLOCK_DATA, this)
+ if (name != null)
+ itemStack.displayNameAccordingToNbt = Text.literal(name)
+ }
+
+ fun getDisplayItem(block: Block): ItemStack {
+ return labeledStack ?: ItemStack(block).also(::markItemStack)
+ }
+ }
+
+ @Serializable
+ data class Block189(
+ val itemId: String,
+ val damage: Short = 0,
+ val onlyIn: List<SkyBlockIsland>? = null,
+ ) {
+ @Transient
+ val block = convertToModernBlock()
+
+ val isCurrentlyActive: Boolean
+ get() = isActiveIn(SBData.skyblockLocation ?: SkyBlockIsland.NIL)
+
+ fun isActiveIn(location: SkyBlockIsland) = onlyIn == null || location in onlyIn
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ private fun convertToModernBlock(): Block? {
+ // TODO: this should be in a shared util, really
+ val newCompound = ItemCache.convert189ToModern(NbtCompound().apply {
+ putString("id", itemId)
+ putShort("Damage", damage)
+ }) ?: return null
+ val itemStack = loadItemFromNbt(newCompound) ?: return null
+ val blockItem = itemStack.item as? BlockItem ?: return null
+ return blockItem.block
+ }
+ }
+
+ @Serializable
+ data class CustomMiningArea(
+ val isSpecialMining: Boolean = true
+ )
+
+
+}
diff --git a/src/main/kotlin/repo/ModernOverlaysData.kt b/src/main/kotlin/repo/ModernOverlaysData.kt
new file mode 100644
index 0000000..543b800
--- /dev/null
+++ b/src/main/kotlin/repo/ModernOverlaysData.kt
@@ -0,0 +1,41 @@
+package moe.nea.firmament.repo
+
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepository
+import java.nio.file.Path
+import kotlin.io.path.extension
+import kotlin.io.path.isDirectory
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.nameWithoutExtension
+import moe.nea.firmament.util.SkyblockId
+
+// TODO: move this over to the repo parser
+class ModernOverlaysData : IReloadable {
+ data class OverlayFile(
+ val version: Int,
+ val path: Path,
+ )
+
+ var overlays: Map<SkyblockId, List<OverlayFile>> = mapOf()
+ override fun reload(repo: NEURepository) {
+ val items = mutableMapOf<SkyblockId, MutableList<OverlayFile>>()
+ repo.baseFolder.resolve("itemsOverlay")
+ .takeIf { it.isDirectory() }
+ ?.listDirectoryEntries()
+ ?.forEach { versionFolder ->
+ val version = versionFolder.fileName.toString().toIntOrNull() ?: return@forEach
+ versionFolder.listDirectoryEntries()
+ .forEach { item ->
+ if (item.extension != "snbt") return@forEach
+ val itemId = item.nameWithoutExtension
+ items.getOrPut(SkyblockId(itemId)) { mutableListOf() }.add(OverlayFile(version, item))
+ }
+ }
+ this.overlays = items
+ }
+
+ fun getOverlayFiles(skyblockId: SkyblockId) = overlays[skyblockId] ?: listOf()
+ fun getMostModernReadableOverlay(skyblockId: SkyblockId, version: Int) = getOverlayFiles(skyblockId)
+ .filter { it.version <= version }
+ .maxByOrNull { it.version }
+}
diff --git a/src/main/kotlin/repo/Reforge.kt b/src/main/kotlin/repo/Reforge.kt
new file mode 100644
index 0000000..dc0d93d
--- /dev/null
+++ b/src/main/kotlin/repo/Reforge.kt
@@ -0,0 +1,160 @@
+package moe.nea.firmament.repo
+
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.builtins.MapSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonDecoder
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.serializer
+import net.minecraft.item.Item
+import net.minecraft.registry.RegistryKey
+import net.minecraft.registry.RegistryKeys
+import net.minecraft.util.Identifier
+import moe.nea.firmament.util.ReforgeId
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.skyblock.ItemType
+import moe.nea.firmament.util.skyblock.Rarity
+
+@Serializable
+data class Reforge(
+ val reforgeName: String,
+ @SerialName("internalName") val reforgeStone: SkyblockId? = null,
+ val nbtModifier: ReforgeId? = null,
+ val requiredRarities: List<Rarity>? = null,
+ val itemTypes: @Serializable(with = ReforgeEligibilityFilter.ItemTypesSerializer::class) List<ReforgeEligibilityFilter>? = null,
+ val allowOn: List<ReforgeEligibilityFilter>? = null,
+ val reforgeCosts: RarityMapped<Double>? = null,
+ val reforgeAbility: RarityMapped<String>? = null,
+ val reforgeStats: RarityMapped<Map<String, Double>>? = null,
+) {
+ val eligibleItems get() = allowOn ?: itemTypes ?: listOf()
+
+ val statUniverse: Set<String> = Rarity.entries.flatMapTo(mutableSetOf()) {
+ reforgeStats?.get(it)?.keys ?: emptySet()
+ }
+
+ @Serializable(with = ReforgeEligibilityFilter.Serializer::class)
+ sealed interface ReforgeEligibilityFilter {
+ object ItemTypesSerializer : KSerializer<List<ReforgeEligibilityFilter>> {
+ override val descriptor: SerialDescriptor
+ get() = JsonElement.serializer().descriptor
+
+ override fun deserialize(decoder: Decoder): List<ReforgeEligibilityFilter> {
+ decoder as JsonDecoder
+ val jsonElement = decoder.decodeJsonElement()
+ if (jsonElement is JsonPrimitive && jsonElement.isString) {
+ return jsonElement.content.split("/").map { AllowsItemType(ItemType.ofName(it)) }
+ }
+ if (jsonElement is JsonArray) {
+ return decoder.json.decodeFromJsonElement(serializer<List<ReforgeEligibilityFilter>>(), jsonElement)
+ }
+ jsonElement as JsonObject
+ val filters = mutableListOf<ReforgeEligibilityFilter>()
+ jsonElement["internalName"]?.let {
+ decoder.json.decodeFromJsonElement(serializer<List<SkyblockId>>(), it).forEach {
+ filters.add(AllowsInternalName(it))
+ }
+ }
+ jsonElement["itemId"]?.let {
+ decoder.json.decodeFromJsonElement(serializer<List<String>>(), it).forEach {
+ val ident = Identifier.tryParse(it)
+ if (ident != null)
+ filters.add(AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM, ident)))
+ }
+ }
+ return filters
+ }
+
+ override fun serialize(encoder: Encoder, value: List<ReforgeEligibilityFilter>) {
+ TODO("Not yet implemented")
+ }
+ }
+
+ object Serializer : KSerializer<ReforgeEligibilityFilter> {
+ override val descriptor: SerialDescriptor
+ get() = serializer<JsonElement>().descriptor
+
+ override fun deserialize(decoder: Decoder): ReforgeEligibilityFilter {
+ val jsonObject = serializer<JsonObject>().deserialize(decoder)
+ jsonObject["internalName"]?.let {
+ return AllowsInternalName(SkyblockId((it as JsonPrimitive).content))
+ }
+ jsonObject["itemType"]?.let {
+ return AllowsItemType(ItemType.ofName((it as JsonPrimitive).content))
+ }
+ jsonObject["minecraftId"]?.let {
+ return AllowsVanillaItemType(RegistryKey.of(RegistryKeys.ITEM,
+ Identifier.of((it as JsonPrimitive).content)))
+ }
+ error("Unknown item type")
+ }
+
+ override fun serialize(encoder: Encoder, value: ReforgeEligibilityFilter) {
+ TODO("Not yet implemented")
+ }
+
+ }
+
+ data class AllowsItemType(val itemType: ItemType) : ReforgeEligibilityFilter
+ data class AllowsInternalName(val internalName: SkyblockId) : ReforgeEligibilityFilter
+ data class AllowsVanillaItemType(val minecraftId: RegistryKey<Item>) : ReforgeEligibilityFilter
+ }
+
+
+ val reforgeId get() = nbtModifier ?: ReforgeId(reforgeName.lowercase())
+
+ @Serializable(with = RarityMapped.Serializer::class)
+ sealed interface RarityMapped<T> {
+ fun get(rarity: Rarity?): T?
+
+ class Serializer<T>(
+ val values: KSerializer<T>
+ ) : KSerializer<RarityMapped<T>> {
+ override val descriptor: SerialDescriptor
+ get() = JsonElement.serializer().descriptor
+
+ val indirect = MapSerializer(Rarity.serializer(), values)
+ override fun deserialize(decoder: Decoder): RarityMapped<T> {
+ decoder as JsonDecoder
+ val element = decoder.decodeJsonElement()
+ if (element is JsonObject) {
+ return PerRarity(decoder.json.decodeFromJsonElement(indirect, element))
+ } else {
+ return Direct(decoder.json.decodeFromJsonElement(values, element))
+ }
+ }
+
+ override fun serialize(encoder: Encoder, value: RarityMapped<T>) {
+ when (value) {
+ is Direct<T> ->
+ values.serialize(encoder, value.value)
+
+ is PerRarity<T> ->
+ indirect.serialize(encoder, value.values)
+ }
+ }
+ }
+
+ @Serializable
+ data class Direct<T>(val value: T) : RarityMapped<T> {
+ override fun get(rarity: Rarity?): T {
+ return value
+ }
+ }
+
+ @Serializable
+ data class PerRarity<T>(val values: Map<Rarity, T>) : RarityMapped<T> {
+ override fun get(rarity: Rarity?): T? {
+ return values[rarity]
+ }
+ }
+ }
+
+}
diff --git a/src/main/kotlin/repo/ReforgeStore.kt b/src/main/kotlin/repo/ReforgeStore.kt
new file mode 100644
index 0000000..4c01974
--- /dev/null
+++ b/src/main/kotlin/repo/ReforgeStore.kt
@@ -0,0 +1,125 @@
+package moe.nea.firmament.repo
+
+import com.google.gson.JsonElement
+import com.mojang.serialization.JsonOps
+import io.github.moulberry.repo.IReloadable
+import io.github.moulberry.repo.NEURepoFile
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.NEURepositoryException
+import io.github.moulberry.repo.data.NEURecipe
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.serializer
+import net.minecraft.item.Item
+import net.minecraft.registry.RegistryKey
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.util.ReforgeId
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.json.KJsonOps
+import moe.nea.firmament.util.skyblock.ItemType
+
+object ReforgeStore : ExtraRecipeProvider, IReloadable {
+ override fun provideExtraRecipes(): Iterable<NEURecipe> {
+ return emptyList()
+ }
+
+ var byType: Map<ItemType, List<Reforge>> = mapOf()
+ var byVanilla: Map<RegistryKey<Item>, List<Reforge>> = mapOf()
+ var byInternalName: Map<SkyblockId, List<Reforge>> = mapOf()
+ var modifierLut = mapOf<ReforgeId, Reforge>()
+ var byReforgeStone = mapOf<SkyblockId, Reforge>()
+ var allReforges = listOf<Reforge>()
+
+ fun findEligibleForItem(itemType: ItemType): List<Reforge> {
+ return byType[itemType] ?: listOf()
+ }
+
+ fun findEligibleForInternalName(internalName: SkyblockId): List<Reforge> {
+ return byInternalName[internalName] ?: listOf()
+ }
+
+ //TODO: return byVanillla
+ override fun reload(repo: NEURepository) {
+ val basicReforges =
+ repo.file("constants/reforges.json")
+ ?.kJson(serializer<Map<String, Reforge>>())
+ ?.values ?: emptyList()
+ val advancedReforges =
+ repo.file("constants/reforgestones.json")
+ ?.kJson(serializer<Map<String, Reforge>>())
+ ?.values ?: emptyList()
+ val allReforges = (basicReforges + advancedReforges)
+ modifierLut = allReforges.associateBy { it.reforgeId }
+ byReforgeStone = allReforges.filter { it.reforgeStone != null }
+ .associateBy { it.reforgeStone!! }
+ val byType = mutableMapOf<ItemType, MutableList<Reforge>>()
+ val byVanilla = mutableMapOf<RegistryKey<Item>, MutableList<Reforge>>()
+ val byInternalName = mutableMapOf<SkyblockId, MutableList<Reforge>>()
+ this.byType = byType
+ this.byVanilla = byVanilla
+ this.byInternalName = byInternalName
+ for (reforge in allReforges) {
+ for (eligibleItem in reforge.eligibleItems) {
+ when (eligibleItem) {
+ is Reforge.ReforgeEligibilityFilter.AllowsInternalName -> {
+ byInternalName.getOrPut(eligibleItem.internalName, ::mutableListOf).add(reforge)
+ }
+
+ is Reforge.ReforgeEligibilityFilter.AllowsItemType -> {
+ val actualItemTypes = resolveItemType(eligibleItem.itemType)
+ for (itemType in actualItemTypes) {
+ byType.getOrPut(itemType, ::mutableListOf).add(reforge)
+ byType.getOrPut(itemType.dungeonVariant, ::mutableListOf).add(reforge)
+ }
+ }
+
+ is Reforge.ReforgeEligibilityFilter.AllowsVanillaItemType -> {
+ byVanilla.getOrPut(eligibleItem.minecraftId, ::mutableListOf).add(reforge)
+ }
+ }
+ }
+ }
+ this.allReforges = allReforges
+ }
+
+ fun resolveItemType(itemType: ItemType): List<ItemType> {
+ if (ItemType.SWORD == itemType) {
+ return listOf(
+ ItemType.SWORD,
+ ItemType.GAUNTLET,
+ ItemType.LONGSWORD,// TODO: check name
+ ItemType.FISHING_WEAPON,// TODO: check name
+ )
+ }
+ if (itemType == ItemType.ofName("ARMOR")) {
+ return listOf(
+ ItemType.CHESTPLATE,
+ ItemType.LEGGINGS,
+ ItemType.HELMET,
+ ItemType.BOOTS,
+ )
+ }
+ if (itemType == ItemType.EQUIPMENT) {
+ return listOf(
+ ItemType.CLOAK,
+ ItemType.BRACELET,
+ ItemType.NECKLACE,
+ ItemType.BELT,
+ ItemType.GLOVES,
+ )
+ }
+ if (itemType == ItemType.ROD) {
+ return listOf(ItemType.FISHING_ROD, ItemType.FISHING_WEAPON)
+ }
+ return listOf(itemType)
+ }
+
+ fun <T> NEURepoFile.kJson(serializer: KSerializer<T>): T {
+ val rawJson = json(JsonElement::class.java)
+ try {
+ val kJsonElement = JsonOps.INSTANCE.convertTo(KJsonOps.INSTANCE, rawJson)
+ return Firmament.json.decodeFromJsonElement(serializer, kJsonElement)
+ } catch (ex: Exception) {
+ throw NEURepositoryException(path, "Could not decode kotlin JSON element", ex)
+ }
+ }
+}
diff --git a/src/main/kotlin/repo/RepoDownloadManager.kt b/src/main/kotlin/repo/RepoDownloadManager.kt
index 3efd83b..150a9ca 100644
--- a/src/main/kotlin/repo/RepoDownloadManager.kt
+++ b/src/main/kotlin/repo/RepoDownloadManager.kt
@@ -1,5 +1,3 @@
-
-
package moe.nea.firmament.repo
import io.ktor.client.call.body
@@ -23,106 +21,108 @@ import kotlin.io.path.readText
import kotlin.io.path.writeText
import moe.nea.firmament.Firmament
import moe.nea.firmament.Firmament.logger
+import moe.nea.firmament.repo.RepoDownloadManager.latestSavedVersionHash
import moe.nea.firmament.util.iterate
object RepoDownloadManager {
- val repoSavedLocation = Firmament.DATA_DIR.resolve("repo-extracted")
- val repoMetadataLocation = Firmament.DATA_DIR.resolve("loaded-repo-sha.txt")
-
- private fun loadSavedVersionHash(): String? =
- if (repoSavedLocation.exists()) {
- if (repoMetadataLocation.exists()) {
- try {
- repoMetadataLocation.readText().trim()
- } catch (e: IOException) {
- null
- }
- } else {
- null
- }
- } else null
-
- private fun saveVersionHash(versionHash: String) {
- latestSavedVersionHash = versionHash
- repoMetadataLocation.writeText(versionHash)
- }
-
- var latestSavedVersionHash: String? = loadSavedVersionHash()
- private set
-
- @Serializable
- private class GithubCommitsResponse(val sha: String)
-
- private suspend fun requestLatestGithubSha(): String? {
- if (RepoManager.Config.branch == "prerelease") {
- RepoManager.Config.branch = "master"
- }
- val response =
- Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.Config.username}/${RepoManager.Config.reponame}/commits/${RepoManager.Config.branch}")
- if (response.status.value != 200) {
- return null
- }
- return response.body<GithubCommitsResponse>().sha
- }
-
- private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) {
- val response = Firmament.httpClient.get(url)
- val targetFile = Files.createTempFile("firmament-repo", ".zip")
- val outputChannel = Files.newByteChannel(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE)
- response.bodyAsChannel().copyTo(outputChannel)
- targetFile
- }
-
- /**
- * Downloads the latest repository from github, setting [latestSavedVersionHash].
- * @return true, if an update was performed, false, otherwise (no update needed, or wasn't able to complete update)
- */
- suspend fun downloadUpdate(force: Boolean): Boolean = withContext(CoroutineName("Repo Update Check")) {
- val latestSha = requestLatestGithubSha()
- if (latestSha == null) {
- logger.warn("Could not request github API to retrieve latest REPO sha.")
- return@withContext false
- }
- val currentSha = loadSavedVersionHash()
- if (latestSha != currentSha || force) {
- val requestUrl =
- "https://github.com/${RepoManager.Config.username}/${RepoManager.Config.reponame}/archive/$latestSha.zip"
- logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl")
- val zipFile = downloadGithubArchive(requestUrl)
- logger.info("Download repository zip file to $zipFile. Deleting old repository")
- withContext(IO) { repoSavedLocation.toFile().deleteRecursively() }
- logger.info("Extracting new repository")
- withContext(IO) { extractNewRepository(zipFile) }
- logger.info("Repository loaded on disk.")
- saveVersionHash(latestSha)
- return@withContext true
- } else {
- logger.debug("Repository on latest sha $currentSha. Not performing update")
- return@withContext false
- }
- }
-
- private fun extractNewRepository(zipFile: Path) {
- repoSavedLocation.createDirectories()
- ZipInputStream(zipFile.inputStream()).use { cis ->
- while (true) {
- val entry = cis.nextEntry ?: break
- if (entry.isDirectory) continue
- val extractedLocation =
- repoSavedLocation.resolve(
- entry.name.substringAfter('/', missingDelimiterValue = "")
- )
- if (repoSavedLocation !in extractedLocation.iterate { it.parent }) {
- logger.error("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.")
- throw RuntimeException("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.")
- }
- extractedLocation.parent.createDirectories()
- extractedLocation.outputStream().use { cis.copyTo(it) }
- }
- }
- }
+ val repoSavedLocation = Firmament.DATA_DIR.resolve("repo-extracted")
+ val repoMetadataLocation = Firmament.DATA_DIR.resolve("loaded-repo-sha.txt")
+
+ private fun loadSavedVersionHash(): String? =
+ if (repoSavedLocation.exists()) {
+ if (repoMetadataLocation.exists()) {
+ try {
+ repoMetadataLocation.readText().trim()
+ } catch (e: IOException) {
+ null
+ }
+ } else {
+ null
+ }
+ } else null
+
+ private fun saveVersionHash(versionHash: String) {
+ latestSavedVersionHash = versionHash
+ repoMetadataLocation.writeText(versionHash)
+ }
+
+ var latestSavedVersionHash: String? = loadSavedVersionHash()
+ private set
+
+ @Serializable
+ private class GithubCommitsResponse(val sha: String)
+
+ private suspend fun requestLatestGithubSha(branchOverride: String?): String? {
+ if (RepoManager.TConfig.branch == "prerelease") {
+ RepoManager.TConfig.branch = "master"
+ }
+ val response =
+ Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.TConfig.username}/${RepoManager.TConfig.reponame}/commits/${branchOverride ?: RepoManager.TConfig.branch}")
+ if (response.status.value != 200) {
+ return null
+ }
+ return response.body<GithubCommitsResponse>().sha
+ }
+
+ private suspend fun downloadGithubArchive(url: String): Path = withContext(IO) {
+ val response = Firmament.httpClient.get(url)
+ val targetFile = Files.createTempFile("firmament-repo", ".zip")
+ val outputChannel = Files.newByteChannel(targetFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE)
+ response.bodyAsChannel().copyTo(outputChannel)
+ targetFile
+ }
+
+ /**
+ * Downloads the latest repository from github, setting [latestSavedVersionHash].
+ * @return true, if an update was performed, false, otherwise (no update needed, or wasn't able to complete update)
+ */
+ suspend fun downloadUpdate(force: Boolean, branch: String? = null): Boolean =
+ withContext(CoroutineName("Repo Update Check")) {
+ val latestSha = requestLatestGithubSha(branch)
+ if (latestSha == null) {
+ logger.warn("Could not request github API to retrieve latest REPO sha.")
+ return@withContext false
+ }
+ val currentSha = loadSavedVersionHash()
+ if (latestSha != currentSha || force) {
+ val requestUrl =
+ "https://github.com/${RepoManager.TConfig.username}/${RepoManager.TConfig.reponame}/archive/$latestSha.zip"
+ logger.info("Planning to upgrade repository from $currentSha to $latestSha from $requestUrl")
+ val zipFile = downloadGithubArchive(requestUrl)
+ logger.info("Download repository zip file to $zipFile. Deleting old repository")
+ withContext(IO) { repoSavedLocation.toFile().deleteRecursively() }
+ logger.info("Extracting new repository")
+ withContext(IO) { extractNewRepository(zipFile) }
+ logger.info("Repository loaded on disk.")
+ saveVersionHash(latestSha)
+ return@withContext true
+ } else {
+ logger.debug("Repository on latest sha $currentSha. Not performing update")
+ return@withContext false
+ }
+ }
+
+ private fun extractNewRepository(zipFile: Path) {
+ repoSavedLocation.createDirectories()
+ ZipInputStream(zipFile.inputStream()).use { cis ->
+ while (true) {
+ val entry = cis.nextEntry ?: break
+ if (entry.isDirectory) continue
+ val extractedLocation =
+ repoSavedLocation.resolve(
+ entry.name.substringAfter('/', missingDelimiterValue = "")
+ )
+ if (repoSavedLocation !in extractedLocation.iterate { it.parent }) {
+ logger.error("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.")
+ throw RuntimeException("Firmament detected an invalid zip file. This is a potential security risk, please report this in the Firmament discord.")
+ }
+ extractedLocation.parent.createDirectories()
+ extractedLocation.outputStream().use { cis.copyTo(it) }
+ }
+ }
+ }
}
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<ItemType?, List<NEUItem>> = 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..43d6db8 100644
--- a/src/main/kotlin/repo/RepoManager.kt
+++ b/src/main/kotlin/repo/RepoManager.kt
@@ -7,23 +7,28 @@ import io.github.moulberry.repo.data.NEURecipe
import io.github.moulberry.repo.data.Rarity
import java.nio.file.Path
import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientTickEvents
+import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
import net.minecraft.client.MinecraftClient
import net.minecraft.network.packet.s2c.play.SynchronizeRecipesS2CPacket
import net.minecraft.recipe.display.CuttingRecipeDisplay
+import net.minecraft.util.StringIdentifiable
import moe.nea.firmament.Firmament
import moe.nea.firmament.Firmament.logger
import moe.nea.firmament.events.ReloadRegistrationEvent
-import moe.nea.firmament.gui.config.ManagedConfig
import moe.nea.firmament.util.ErrorUtil
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.MinecraftDispatcher
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.TestUtil
+import moe.nea.firmament.util.data.Config
+import moe.nea.firmament.util.data.ManagedConfig
import moe.nea.firmament.util.tr
object RepoManager {
- object Config : ManagedConfig("repo", Category.META) {
+ @Config
+ object TConfig : ManagedConfig("repo", Category.META) {
var username by string("username") { "NotEnoughUpdates" }
var reponame by string("reponame") { "NotEnoughUpdates-REPO" }
var branch by string("branch") { "master" }
@@ -32,20 +37,35 @@ object RepoManager {
username = "NotEnoughUpdates"
reponame = "NotEnoughUpdates-REPO"
branch = "master"
- save()
+ markDirty()
}
-
+ val enableREI by toggle("enable-rei") { true }
val disableItemGroups by toggle("disable-item-groups") { true }
val reload by button("reload") {
- save()
- RepoManager.reload()
+ markDirty()
+ Firmament.coroutineScope.launch {
+ RepoManager.reload()
+ }
}
val redownload by button("redownload") {
- save()
+ markDirty()
RepoManager.launchAsyncUpdate(true)
}
val alwaysSuperCraft by toggle("enable-super-craft") { true }
var warnForMissingItemListMod by toggle("warn-for-missing-item-list-mod") { true }
+ val perfectRenders by choice("perfect-renders") { PerfectRender.RENDER }
+ }
+
+ enum class PerfectRender(val label: String) : StringIdentifiable {
+ NOTHING("nothing"),
+ RENDER("render"),
+ RENDER_AND_TEXT("text"),
+ ;
+
+ fun rendersPerfectText() = this == RENDER_AND_TEXT
+ fun rendersPerfectVisuals() = this == RENDER || this == RENDER_AND_TEXT
+
+ override fun asString(): String? = label
}
val currentDownloadedSha by RepoDownloadManager::latestSavedVersionHash
@@ -53,13 +73,21 @@ object RepoManager {
var recentlyFailedToUpdateItemList = false
val essenceRecipeProvider = EssenceRecipeProvider()
- val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider)
+ val recipeCache = BetterRepoRecipeCache(essenceRecipeProvider, ReforgeStore)
+ val miningData = MiningRepoData()
+ val overlayData = ModernOverlaysData()
fun makeNEURepository(path: Path): NEURepository {
return NEURepository.of(path).apply {
+ registerReloadListener(overlayData)
registerReloadListener(ItemCache)
+ registerReloadListener(RepoItemTypeCache)
registerReloadListener(ExpLadders)
registerReloadListener(ItemNameLookup)
+ registerReloadListener(ReforgeStore)
+ registerReloadListener(essenceRecipeProvider)
+ registerReloadListener(recipeCache)
+ registerReloadListener(miningData)
ReloadRegistrationEvent.publish(ReloadRegistrationEvent(this))
registerReloadListener {
if (TestUtil.isInTest) return@registerReloadListener
@@ -70,8 +98,6 @@ object RepoManager {
}
}
}
- registerReloadListener(essenceRecipeProvider)
- registerReloadListener(recipeCache)
}
}
@@ -98,47 +124,45 @@ object RepoManager {
fun getNEUItem(skyblockId: SkyblockId): NEUItem? = neuRepo.items.getItemBySkyblockId(skyblockId.neuItem)
+ fun downloadOverridenBranch(branch: String) {
+ Firmament.coroutineScope.launch {
+ RepoDownloadManager.downloadUpdate(true, branch)
+ reload()
+ }
+ }
+
fun launchAsyncUpdate(force: Boolean = false) {
Firmament.coroutineScope.launch {
- ItemCache.ReloadProgressHud.reportProgress("Downloading", 0, -1) // TODO: replace with a proper bouncy bar
- ItemCache.ReloadProgressHud.isEnabled = true
- try {
- RepoDownloadManager.downloadUpdate(force)
- ItemCache.ReloadProgressHud.reportProgress("Download complete", 1, 1)
- } finally {
- ItemCache.ReloadProgressHud.isEnabled = false
- }
+ RepoDownloadManager.downloadUpdate(force)
reload()
}
}
fun reloadForTest(from: Path) {
neuRepo = makeNEURepository(from)
- reload()
+ reloadSync()
}
- fun reload() {
- if (!TestUtil.isInTest && !MC.instance.isOnThread) {
- MC.instance.send {
- reload()
- }
- return
+
+ suspend fun reload() {
+ withContext(Dispatchers.IO) {
+ reloadSync()
}
+ }
+
+ fun reloadSync() {
try {
- ItemCache.ReloadProgressHud.reportProgress("Reloading from Disk",
- 0,
- -1) // TODO: replace with a proper bouncy bar
- ItemCache.ReloadProgressHud.isEnabled = true
logger.info("Repo reload started.")
neuRepo.reload()
logger.info("Repo reload completed.")
} catch (exc: NEURepositoryException) {
ErrorUtil.softError("Failed to reload repository", exc)
MC.sendChat(
- tr("firmament.repo.reloadfail",
- "Failed to reload repository. This will result in some mod features not working.")
+ tr(
+ "firmament.repo.reloadfail",
+ "Failed to reload repository. This will result in some mod features not working."
+ )
)
- ItemCache.ReloadProgressHud.isEnabled = false
}
}
@@ -151,10 +175,12 @@ object RepoManager {
return
}
neuRepo = makeNEURepository(RepoDownloadManager.repoSavedLocation)
- if (Config.autoUpdate) {
+ if (TConfig.autoUpdate) {
launchAsyncUpdate()
} else {
- reload()
+ Firmament.coroutineScope.launch {
+ reload()
+ }
}
}
@@ -180,6 +206,8 @@ object RepoManager {
}
fun getRepoRef(): String {
- return "${Config.username}/${Config.reponame}#${Config.branch}"
+ return "${TConfig.username}/${TConfig.reponame}#${TConfig.branch}"
}
+
+ fun shouldLoadREI(): Boolean = TConfig.enableREI
}
diff --git a/src/main/kotlin/repo/RepoModResourcePack.kt b/src/main/kotlin/repo/RepoModResourcePack.kt
index f92fe4f..6868711 100644
--- a/src/main/kotlin/repo/RepoModResourcePack.kt
+++ b/src/main/kotlin/repo/RepoModResourcePack.kt
@@ -1,11 +1,11 @@
-
package moe.nea.firmament.repo
import java.io.InputStream
import java.nio.file.Files
import java.nio.file.Path
-import java.util.*
+import java.util.Optional
import net.fabricmc.fabric.api.resource.ModResourcePack
+import net.fabricmc.fabric.impl.resource.loader.ModResourcePackSorter
import net.fabricmc.loader.api.FabricLoader
import net.fabricmc.loader.api.metadata.ModMetadata
import kotlin.io.path.exists
@@ -21,86 +21,86 @@ import net.minecraft.resource.ResourcePackInfo
import net.minecraft.resource.ResourcePackSource
import net.minecraft.resource.ResourceType
import net.minecraft.resource.metadata.ResourceMetadata
-import net.minecraft.resource.metadata.ResourceMetadataReader
+import net.minecraft.resource.metadata.ResourceMetadataSerializer
import net.minecraft.text.Text
import net.minecraft.util.Identifier
-import net.minecraft.util.PathUtil
+import net.minecraft.util.path.PathUtil
import moe.nea.firmament.Firmament
class RepoModResourcePack(val basePath: Path) : ModResourcePack {
- companion object {
- fun append(packs: MutableList<in ModResourcePack>) {
- Firmament.logger.info("Registering mod resource pack")
- packs.add(RepoModResourcePack(RepoDownloadManager.repoSavedLocation))
- }
+ companion object {
+ fun append(packs: ModResourcePackSorter) {
+ Firmament.logger.info("Registering mod resource pack")
+ packs.addPack(RepoModResourcePack(RepoDownloadManager.repoSavedLocation))
+ }
- fun createResourceDirectly(identifier: Identifier): Optional<Resource> {
- val pack = RepoModResourcePack(RepoDownloadManager.repoSavedLocation)
- return Optional.of(
- Resource(
- pack,
- pack.open(ResourceType.CLIENT_RESOURCES, identifier) ?: return Optional.empty()
- ) {
- val base =
- pack.open(ResourceType.CLIENT_RESOURCES, identifier.withPath(identifier.path + ".mcmeta"))
- if (base == null)
- ResourceMetadata.NONE
- else
- NamespaceResourceManager.loadMetadata(base)
- }
- )
- }
- }
+ fun createResourceDirectly(identifier: Identifier): Optional<Resource> {
+ val pack = RepoModResourcePack(RepoDownloadManager.repoSavedLocation)
+ return Optional.of(
+ Resource(
+ pack,
+ pack.open(ResourceType.CLIENT_RESOURCES, identifier) ?: return Optional.empty()
+ ) {
+ val base =
+ pack.open(ResourceType.CLIENT_RESOURCES, identifier.withPath(identifier.path + ".mcmeta"))
+ if (base == null)
+ ResourceMetadata.NONE
+ else
+ NamespaceResourceManager.loadMetadata(base)
+ }
+ )
+ }
+ }
- override fun close() {
- }
+ override fun close() {
+ }
- override fun openRoot(vararg segments: String): InputSupplier<InputStream>? {
- return getFile(segments)?.let { InputSupplier.create(it) }
- }
+ override fun openRoot(vararg segments: String): InputSupplier<InputStream>? {
+ return getFile(segments)?.let { InputSupplier.create(it) }
+ }
- fun getFile(segments: Array<out String>): Path? {
- PathUtil.validatePath(*segments)
- val path = segments.fold(basePath, Path::resolve)
- if (!path.isRegularFile()) return null
- return path
- }
+ fun getFile(segments: Array<out String>): Path? {
+ PathUtil.validatePath(*segments)
+ val path = segments.fold(basePath, Path::resolve)
+ if (!path.isRegularFile()) return null
+ return path
+ }
- override fun open(type: ResourceType?, id: Identifier): InputSupplier<InputStream>? {
- if (type != ResourceType.CLIENT_RESOURCES) return null
- if (id.namespace != "neurepo") return null
- val file = getFile(id.path.split("/").toTypedArray())
- return file?.let { InputSupplier.create(it) }
- }
+ override fun open(type: ResourceType?, id: Identifier): InputSupplier<InputStream>? {
+ if (type != ResourceType.CLIENT_RESOURCES) return null
+ if (id.namespace != "neurepo") return null
+ val file = getFile(id.path.split("/").toTypedArray())
+ return file?.let { InputSupplier.create(it) }
+ }
- override fun findResources(
- type: ResourceType?,
- namespace: String,
- prefix: String,
- consumer: ResourcePack.ResultConsumer
- ) {
- if (namespace != "neurepo") return
- if (type != ResourceType.CLIENT_RESOURCES) return
+ override fun findResources(
+ type: ResourceType?,
+ namespace: String,
+ prefix: String,
+ consumer: ResourcePack.ResultConsumer
+ ) {
+ if (namespace != "neurepo") return
+ if (type != ResourceType.CLIENT_RESOURCES) return
- val prefixPath = basePath.resolve(prefix)
- if (!prefixPath.exists())
- return
- Files.walk(prefixPath)
- .asSequence()
- .map { it.relativeTo(basePath) }
- .forEach {
- consumer.accept(Identifier.of("neurepo", it.toString()), InputSupplier.create(it))
- }
- }
+ val prefixPath = basePath.resolve(prefix)
+ if (!prefixPath.exists())
+ return
+ Files.walk(prefixPath)
+ .asSequence()
+ .map { it.relativeTo(basePath) }
+ .forEach {
+ consumer.accept(Identifier.of("neurepo", it.toString()), InputSupplier.create(it))
+ }
+ }
- override fun getNamespaces(type: ResourceType?): Set<String> {
- if (type != ResourceType.CLIENT_RESOURCES) return emptySet()
- return setOf("neurepo")
- }
+ override fun getNamespaces(type: ResourceType?): Set<String> {
+ if (type != ResourceType.CLIENT_RESOURCES) return emptySet()
+ return setOf("neurepo")
+ }
- override fun <T> parseMetadata(metaReader: ResourceMetadataReader<T>): T? {
- return AbstractFileResourcePack.parseMetadata(
- metaReader, """
+ override fun <T : Any?> parseMetadata(metadataSerializer: ResourceMetadataSerializer<T>?): T? {
+ return AbstractFileResourcePack.parseMetadata(
+ metadataSerializer, """
{
"pack": {
"pack_format": 12,
@@ -108,19 +108,20 @@ class RepoModResourcePack(val basePath: Path) : ModResourcePack {
}
}
""".trimIndent().byteInputStream()
- )
- }
+ )
+ }
- override fun getInfo(): ResourcePackInfo {
- return ResourcePackInfo("neurepo", Text.literal("NEU Repo"), ResourcePackSource.BUILTIN, Optional.empty())
- }
- override fun getFabricModMetadata(): ModMetadata {
- return FabricLoader.getInstance().getModContainer("firmament")
- .get().metadata
- }
+ override fun getInfo(): ResourcePackInfo {
+ return ResourcePackInfo("neurepo", Text.literal("NEU Repo"), ResourcePackSource.BUILTIN, Optional.empty())
+ }
- override fun createOverlay(overlay: String): ModResourcePack {
- return RepoModResourcePack(basePath.resolve(overlay))
- }
+ override fun getFabricModMetadata(): ModMetadata {
+ return FabricLoader.getInstance().getModContainer("firmament")
+ .get().metadata
+ }
+
+ override fun createOverlay(overlay: String): ModResourcePack {
+ return RepoModResourcePack(basePath.resolve(overlay))
+ }
}
diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt
index 18126ee..01d1c4d 100644
--- a/src/main/kotlin/repo/SBItemStack.kt
+++ b/src/main/kotlin/repo/SBItemStack.kt
@@ -9,17 +9,38 @@ import net.minecraft.item.ItemStack
import net.minecraft.network.RegistryByteBuf
import net.minecraft.network.codec.PacketCodec
import net.minecraft.network.codec.PacketCodecs
+import net.minecraft.text.Style
import net.minecraft.text.Text
+import net.minecraft.text.TextColor
import net.minecraft.util.Formatting
import moe.nea.firmament.repo.ItemCache.asItemStack
+import moe.nea.firmament.repo.ItemCache.withFallback
import moe.nea.firmament.util.FirmFormatters
import moe.nea.firmament.util.LegacyFormattingCode
+import moe.nea.firmament.util.MC
+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.mc.modifyLore
+import moe.nea.firmament.util.modifyExtraAttributes
import moe.nea.firmament.util.petData
+import moe.nea.firmament.util.prepend
+import moe.nea.firmament.util.reconstitute
+import moe.nea.firmament.util.removeColorCodes
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.unformattedString
+import moe.nea.firmament.util.useMatch
import moe.nea.firmament.util.withColor
data class SBItemStack constructor(
@@ -28,8 +49,9 @@ data class SBItemStack constructor(
private var stackSize: Int,
private var petData: PetData?,
val extraLore: List<Text> = emptyList(),
- // TODO: grab this star data from nbt if possible
val stars: Int = 0,
+ val fallback: ItemStack? = null,
+ val reforge: ReforgeId? = null,
) {
fun getStackSize() = stackSize
@@ -60,13 +82,16 @@ data class SBItemStack constructor(
}
val EMPTY = SBItemStack(SkyblockId.NULL, 0)
+ private val BREAKING_POWER_REGEX = "Breaking Power (?<power>[0-9]+)".toPattern()
operator fun invoke(itemStack: ItemStack): SBItemStack {
val skyblockId = itemStack.skyBlockId ?: SkyblockId.NULL
return SBItemStack(
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()
)
}
@@ -77,6 +102,175 @@ data class SBItemStack constructor(
}
return SBItemStack(neuIngredient.skyblockId, neuIngredient.amount.toInt())
}
+
+ fun passthrough(itemStack: ItemStack): SBItemStack {
+ return SBItemStack(SkyblockId.NULL, null, itemStack.count, null, fallback = itemStack)
+ }
+
+ fun parseStatBlock(itemStack: ItemStack): List<StatLine> {
+ return itemStack.loreAccordingToNbt
+ .map { parseStatLine(it) }
+ .takeWhile { it != null }
+ .filterNotNull()
+ }
+
+ fun appendEnhancedStats(
+ itemStack: ItemStack,
+ reforgeStats: Map<String, Double>,
+ buffKind: BuffKind,
+ ) {
+ val namedReforgeStats = reforgeStats
+ .mapKeysTo(mutableMapOf()) { statIdToName(it.key) }
+ val loreMut = itemStack.loreAccordingToNbt.toMutableList()
+ var statBlockLastIndex = -1
+ for (i in loreMut.indices) {
+ val statLine = parseStatLine(loreMut[i])
+ if (statLine == null && statBlockLastIndex >= 0) {
+ break
+ }
+ if (statLine == null) {
+ continue
+ }
+ statBlockLastIndex = i
+ val statBuff = namedReforgeStats.remove(statLine.statName) ?: continue
+ loreMut[i] = statLine.addStat(statBuff, buffKind).reconstitute()
+ }
+ if (namedReforgeStats.isNotEmpty() && statBlockLastIndex == -1) {
+ loreMut.add(0, Text.literal(""))
+ }
+ // If there is no stat block the statBlockLastIndex falls through to -1
+ // TODO: this is good enough for some items. some other items might have their stats at a different place.
+ for ((statName, statBuff) in namedReforgeStats) {
+ val statLine = StatLine(statName, null).addStat(statBuff, buffKind)
+ loreMut.add(statBlockLastIndex + 1, statLine.reconstitute())
+ }
+ itemStack.loreAccordingToNbt = loreMut
+ }
+
+ data class StatFormatting(
+ val postFix: String,
+ val color: Formatting,
+ val isStarAffected: Boolean = true,
+ )
+
+ 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.GREEN, false),
+ "Ability Damage" to StatFormatting("%", Formatting.RED),
+ "Crit Damage" to StatFormatting("%", Formatting.RED),
+ "Crit Chance" to StatFormatting("%", Formatting.RED),
+ "Ability Damage" to StatFormatting("%", Formatting.RED),
+ "Trophy Fish Chance" to StatFormatting("%", Formatting.GREEN),
+ "Health" to StatFormatting("", Formatting.GREEN),
+ "Defense" to StatFormatting("", Formatting.GREEN),
+ "Fishing Speed" to StatFormatting("", Formatting.GREEN),
+ "Double Hook Chance" to StatFormatting("%", Formatting.GREEN),
+ "Mining Speed" to StatFormatting("", Formatting.GREEN),
+ "Mining Fortune" to StatFormatting("", Formatting.GREEN),
+ "Heat Resistance" to StatFormatting("", Formatting.GREEN),
+ "Swing Range" to StatFormatting("", Formatting.GREEN),
+ "Rift Time" to StatFormatting("", Formatting.GREEN),
+ "Speed" to StatFormatting("", Formatting.GREEN),
+ "Farming Fortune" to StatFormatting("", Formatting.GREEN),
+ "True Defense" to StatFormatting("", Formatting.GREEN),
+ "Mending" to StatFormatting("", Formatting.GREEN),
+ "Foraging Wisdom" to StatFormatting("", Formatting.GREEN),
+ "Farming Wisdom" to StatFormatting("", Formatting.GREEN),
+ "Foraging Fortune" to StatFormatting("", Formatting.GREEN),
+ "Magic Find" to StatFormatting("", Formatting.GREEN),
+ "Ferocity" to StatFormatting("", Formatting.GREEN),
+ "Bonus Pest Chance" to StatFormatting("%", Formatting.GREEN),
+ "Cold Resistance" to StatFormatting("", Formatting.GREEN),
+ "Pet Luck" to StatFormatting("", Formatting.GREEN),
+ "Fear" to StatFormatting("", Formatting.GREEN),
+ "Mana Regen" to StatFormatting("%", Formatting.GREEN),
+ "Rift Damage" to StatFormatting("", Formatting.GREEN),
+ "Hearts" to StatFormatting("", Formatting.GREEN),
+ "Vitality" to StatFormatting("", Formatting.GREEN),
+ // TODO: make this a repo json
+ )
+
+
+ private val statLabelRegex = "(?<statName>.*): ".toPattern()
+
+ enum class BuffKind(
+ val color: Formatting,
+ val prefix: String,
+ val postFix: String,
+ val isHidden: Boolean,
+ ) {
+ REFORGE(Formatting.BLUE, "(", ")", false),
+ STAR_BUFF(Formatting.RESET, "", "", true),
+ CATA_STAR_BUFF(Formatting.DARK_GRAY, "(", ")", false),
+ ;
+ }
+
+ data class StatLine(
+ val statName: String,
+ val value: Text?,
+ val rest: List<Text> = listOf(),
+ val valueNum: Double? = value?.directLiteralStringContent?.trim(' ', 's', '%', '+')?.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 +
+ if (buffKind.isHidden) emptyList()
+ else listOf(
+ Text.literal(
+ buffKind.prefix + formattedAmount +
+ statFormatting.postFix +
+ buffKind.postFix + " "
+ )
+ .withColor(buffKind.color)
+ )
+ )
+ }
+
+ fun formatValue() =
+ Text.literal(
+ FirmFormatters.formatCommas(
+ valueNum ?: 0.0,
+ 1,
+ includeSign = true
+ ) + statFormatting.postFix + " "
+ )
+ .setStyle(Style.EMPTY.withColor(statFormatting.color))
+
+ val statFormatting = formattingOverrides[statName] ?: StatFormatting("", Formatting.GREEN)
+ private fun abbreviate(abbreviateTo: Int): String {
+ if (abbreviateTo >= statName.length) return statName
+ val segments = statName.split(" ")
+ return segments.joinToString(" ") {
+ it.substring(0, maxOf(1, abbreviateTo / segments.size))
+ }
+ }
+
+ fun reconstitute(abbreviateTo: Int = Int.MAX_VALUE): Text =
+ Text.literal("").setStyle(Style.EMPTY.withItalic(false))
+ .append(Text.literal("${abbreviate(abbreviateTo)}: ").grey())
+ .append(value ?: formatValue())
+ .also { rest.forEach(it::append) }
+ }
+
+ fun statIdToName(statId: String): String {
+ val segments = statId.split("_")
+ return segments.joinToString(" ") { it.replaceFirstChar { it.uppercaseChar() } }
+ }
+
+ 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(
@@ -127,8 +321,52 @@ data class SBItemStack constructor(
}
+ private fun appendReforgeInfo(
+ itemStack: ItemStack,
+ ) {
+ val rarity = Rarity.fromItem(itemStack) ?: return
+ val reforgeId = this.reforge ?: return
+ val reforge = ReforgeStore.modifierLut[reforgeId] ?: return
+ val reforgeStats = reforge.reforgeStats?.get(rarity) ?: mapOf()
+ itemStack.displayNameAccordingToNbt = itemStack.displayNameAccordingToNbt.copy()
+ .prepend(Text.literal(reforge.reforgeName + " ").formatted(Rarity.colourMap[rarity] ?: Formatting.WHITE))
+ val data = itemStack.extraAttributes.copy()
+ data.putString("modifier", reforgeId.id)
+ itemStack.extraAttributes = data
+ appendEnhancedStats(itemStack, reforgeStats, BuffKind.REFORGE)
+ reforge.reforgeAbility?.get(rarity)?.let { reforgeAbility ->
+ val formattedReforgeAbility = ItemCache.un189Lore(reforgeAbility)
+ .grey()
+ itemStack.modifyLore {
+ val lastBlank = it.indexOfLast { it.unformattedString.isBlank() }
+ val newList = mutableListOf<Text>()
+ newList.addAll(it.subList(0, lastBlank))
+ newList.add(Text.literal(""))
+ newList.add(Text.literal("${reforge.reforgeName} Bonus").blue())
+ MC.font.textHandler.wrapLines(formattedReforgeAbility, 180, Style.EMPTY).mapTo(newList) {
+ it.reconstitute()
+ }
+ newList.addAll(it.subList(lastBlank, it.size))
+ return@modifyLore newList
+ }
+ }
+ }
+
+ // TODO: avoid instantiating the item stack here
+ @ExpensiveItemCacheApi
+ val itemType: ItemType? get() = ItemType.fromItemStack(asImmutableItemStack())
+ @ExpensiveItemCacheApi
+ val rarity: Rarity? get() = Rarity.fromItem(asImmutableItemStack())
+
private var itemStack_: ItemStack? = null
+ val breakingPower: Int
+ get() =
+ BREAKING_POWER_REGEX.useMatch(neuItem?.lore?.firstOrNull()?.removeColorCodes()) {
+ group("power").toInt()
+ } ?: 0
+
+ @ExpensiveItemCacheApi
private val itemStack: ItemStack
get() {
val itemStack = itemStack_ ?: run {
@@ -138,10 +376,14 @@ data class SBItemStack constructor(
return@run ItemStack.EMPTY
val replacementData = mutableMapOf<String, String>()
injectReplacementDataForPets(replacementData)
- return@run neuItem.asItemStack(idHint = skyblockId, replacementData)
+ val baseItem = neuItem.asItemStack(idHint = skyblockId, replacementData)
+ .withFallback(fallback)
.copyWithCount(stackSize)
- .also { it.appendLore(extraLore) }
- .also { enhanceStatsByStars(it, stars) }
+ val baseStats = parseStatBlock(baseItem)
+ appendReforgeInfo(baseItem)
+ baseItem.appendLore(extraLore)
+ enhanceStatsByStars(baseItem, stars, baseStats)
+ return@run baseItem
}
if (itemStack_ == null)
itemStack_ = itemStack
@@ -151,6 +393,7 @@ data class SBItemStack constructor(
private fun starString(stars: Int): Text {
if (stars <= 0) return Text.empty()
+ // TODO: idk master stars
val tiers = listOf(
LegacyFormattingCode.GOLD,
LegacyFormattingCode.LIGHT_PURPLE,
@@ -170,17 +413,45 @@ data class SBItemStack constructor(
return starString
}
- private fun enhanceStatsByStars(itemStack: ItemStack, stars: Int) {
+ private fun enhanceStatsByStars(itemStack: ItemStack, stars: Int, baseStats: List<StatLine>) {
if (stars == 0) return
// TODO: increase stats and add the star level into the nbt data so star displays work
+ itemStack.modifyExtraAttributes {
+ it.putInt("upgrade_level", stars)
+ }
itemStack.displayNameAccordingToNbt = itemStack.displayNameAccordingToNbt.copy()
.append(starString(stars))
+ val isDungeon = ItemType.fromItemStack(itemStack)?.isDungeon ?: true
+ val truncatedStarCount = if (isDungeon) minOf(5, stars) else stars
+ appendEnhancedStats(
+ itemStack,
+ baseStats
+ .filter { it.statFormatting.isStarAffected }
+ .associate {
+ it.statName to ((it.valueNum ?: 0.0) * (truncatedStarCount * 0.02))
+ },
+ BuffKind.STAR_BUFF
+ )
+ }
+
+ fun isWarm(): Boolean {
+ if (itemStack_ != null) return true
+ if (ItemCache.hasCacheFor(skyblockId)) return true
+ return false
+ }
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ fun asLazyImmutableItemStack(): ItemStack? {
+ if (isWarm()) return asImmutableItemStack()
+ return null
}
- fun asImmutableItemStack(): ItemStack {
+ @ExpensiveItemCacheApi
+ fun asImmutableItemStack(): ItemStack { // TODO: add a "or fallback to painting" option to asLazyImmutableItemStack to be used in more places.
return itemStack
}
+ @ExpensiveItemCacheApi
fun asCopiedItemStack(): ItemStack {
return itemStack.copy()
}
diff --git a/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt b/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt
index 9a1aea5..3774f26 100644
--- a/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt
+++ b/src/main/kotlin/repo/recipes/GenericRecipeRenderer.kt
@@ -9,11 +9,13 @@ import net.minecraft.util.Identifier
import moe.nea.firmament.repo.SBItemStack
interface GenericRecipeRenderer<T : NEURecipe> {
- fun render(recipe: T, bounds: Rectangle, layouter: RecipeLayouter)
+ fun render(recipe: T, bounds: Rectangle, layouter: RecipeLayouter, mainItem: SBItemStack?)
fun getInputs(recipe: T): Collection<SBItemStack>
fun getOutputs(recipe: T): Collection<SBItemStack>
val icon: ItemStack
val title: Text
val identifier: Identifier
fun findAllRecipes(neuRepository: NEURepository): Iterable<T>
+ val displayHeight: Int get() = 66
+ val typ: Class<T>
}
diff --git a/src/main/kotlin/repo/recipes/RecipeLayouter.kt b/src/main/kotlin/repo/recipes/RecipeLayouter.kt
index 109bff5..ed0dca2 100644
--- a/src/main/kotlin/repo/recipes/RecipeLayouter.kt
+++ b/src/main/kotlin/repo/recipes/RecipeLayouter.kt
@@ -1,6 +1,8 @@
package moe.nea.firmament.repo.recipes
import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
import net.minecraft.text.Text
import moe.nea.firmament.repo.SBItemStack
@@ -21,13 +23,16 @@ interface RecipeLayouter {
slotKind: SlotKind,
)
+ fun createTooltip(rectangle: Rectangle, label: Text)
+
fun createLabel(
x: Int, y: Int,
text: Text
)
- fun createArrow(x: Int, y: Int)
+ fun createArrow(x: Int, y: Int): Rectangle
fun createMoulConfig(x: Int, y: Int, w: Int, h: Int, component: GuiComponent)
+ fun createFire(ingredientsCenter: Point, animationTicks: Int)
}
diff --git a/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt
index 679aec8..37994ca 100644
--- a/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt
+++ b/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt
@@ -12,17 +12,32 @@ import moe.nea.firmament.Firmament
import moe.nea.firmament.repo.SBItemStack
import moe.nea.firmament.util.tr
-class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> {
- override fun render(recipe: NEUCraftingRecipe, bounds: Rectangle, layouter: RecipeLayouter) {
+object SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> {
+ override fun render(
+ recipe: NEUCraftingRecipe,
+ bounds: Rectangle,
+ layouter: RecipeLayouter,
+ mainItem: SBItemStack?,
+ ) {
val point = Point(bounds.centerX - 58, bounds.centerY - 27)
- layouter.createArrow(point.x + 60, point.y + 18)
+ val arrow = layouter.createArrow(point.x + 60, point.y + 18)
+
+ if (recipe.extraText != null && recipe.extraText!!.isNotBlank()) {
+ layouter.createTooltip(
+ arrow,
+ Text.of(recipe.extraText!!),
+ )
+ }
+
for (i in 0 until 3) {
for (j in 0 until 3) {
val item = recipe.inputs[i + j * 3]
- layouter.createItemSlot(point.x + 1 + i * 18,
- point.y + 1 + j * 18,
- SBItemStack(item),
- RecipeLayouter.SlotKind.SMALL_INPUT)
+ layouter.createItemSlot(
+ point.x + 1 + i * 18,
+ point.y + 1 + j * 18,
+ SBItemStack(item),
+ RecipeLayouter.SlotKind.SMALL_INPUT
+ )
}
}
layouter.createItemSlot(
@@ -32,6 +47,9 @@ class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> {
)
}
+ override val typ: Class<NEUCraftingRecipe>
+ get() = NEUCraftingRecipe::class.java
+
override fun getInputs(recipe: NEUCraftingRecipe): Collection<SBItemStack> {
return recipe.allInputs.mapNotNull { SBItemStack(it) }
}
@@ -45,6 +63,6 @@ class SBCraftingRecipeRenderer : GenericRecipeRenderer<NEUCraftingRecipe> {
}
override val icon: ItemStack = ItemStack(Blocks.CRAFTING_TABLE)
- override val title: Text = tr("firmament.category.crafting", "SkyBlock Crafting") // TODO: fix tr not being included in jars
+ override val title: Text = tr("firmament.category.crafting", "SkyBlock Crafting")
override val identifier: Identifier = Firmament.identifier("crafting_recipe")
}
diff --git a/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt
new file mode 100644
index 0000000..90a6de4
--- /dev/null
+++ b/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt
@@ -0,0 +1,74 @@
+package moe.nea.firmament.repo.recipes
+
+import io.github.moulberry.repo.NEURepository
+import me.shedaniel.math.Rectangle
+import net.minecraft.item.ItemStack
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.repo.EssenceRecipeProvider
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.tr
+
+object SBEssenceUpgradeRecipeRenderer : GenericRecipeRenderer<EssenceRecipeProvider.EssenceUpgradeRecipe> {
+ override fun render(
+ recipe: EssenceRecipeProvider.EssenceUpgradeRecipe,
+ bounds: Rectangle,
+ layouter: RecipeLayouter,
+ mainItem: SBItemStack?
+ ) {
+ val sourceItem = mainItem ?: SBItemStack(recipe.itemId)
+ layouter.createItemSlot(
+ bounds.minX + 12,
+ bounds.centerY - 8 - 18 / 2,
+ sourceItem.copy(stars = recipe.starCountAfter - 1),
+ RecipeLayouter.SlotKind.SMALL_INPUT
+ )
+ layouter.createItemSlot(
+ bounds.minX + 12, bounds.centerY - 8 + 18 / 2,
+ SBItemStack(recipe.essenceIngredient),
+ RecipeLayouter.SlotKind.SMALL_INPUT
+ )
+ layouter.createItemSlot(
+ bounds.maxX - 12 - 16, bounds.centerY - 8,
+ sourceItem.copy(stars = recipe.starCountAfter),
+ RecipeLayouter.SlotKind.SMALL_OUTPUT
+ )
+ val extraItems = recipe.extraItems
+ layouter.createArrow(
+ bounds.centerX - 24 / 2,
+ if (extraItems.isEmpty()) bounds.centerY - 17 / 2
+ else bounds.centerY + 18 / 2
+ )
+ for ((index, item) in extraItems.withIndex()) {
+ layouter.createItemSlot(
+ bounds.centerX - extraItems.size * 16 / 2 - 2 / 2 + index * 18,
+ bounds.centerY - 18 / 2,
+ SBItemStack(item),
+ RecipeLayouter.SlotKind.SMALL_INPUT,
+ )
+ }
+ }
+
+ override fun getInputs(recipe: EssenceRecipeProvider.EssenceUpgradeRecipe): Collection<SBItemStack> {
+ return recipe.allInputs.mapNotNull { SBItemStack(it) }
+ }
+
+ override fun getOutputs(recipe: EssenceRecipeProvider.EssenceUpgradeRecipe): Collection<SBItemStack> {
+ return listOfNotNull(SBItemStack(recipe.itemId))
+ }
+
+ @OptIn(ExpensiveItemCacheApi::class)
+ override val icon: ItemStack get() = SBItemStack(SkyblockId("ESSENCE_WITHER")).asImmutableItemStack()
+ override val title: Text = tr("firmament.category.essence", "Essence Upgrades")
+ override val identifier: Identifier = Firmament.identifier("essence_upgrade")
+ override fun findAllRecipes(neuRepository: NEURepository): Iterable<EssenceRecipeProvider.EssenceUpgradeRecipe> {
+ return RepoManager.essenceRecipeProvider.recipes
+ }
+
+ override val typ: Class<EssenceRecipeProvider.EssenceUpgradeRecipe>
+ get() = EssenceRecipeProvider.EssenceUpgradeRecipe::class.java
+}
diff --git a/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt b/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt
new file mode 100644
index 0000000..343493a
--- /dev/null
+++ b/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt
@@ -0,0 +1,88 @@
+package moe.nea.firmament.repo.recipes
+
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.data.NEUForgeRecipe
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import kotlin.math.cos
+import kotlin.math.sin
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.block.Blocks
+import net.minecraft.item.ItemStack
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.util.tr
+
+object SBForgeRecipeRenderer : GenericRecipeRenderer<NEUForgeRecipe> {
+ override fun render(
+ recipe: NEUForgeRecipe,
+ bounds: Rectangle,
+ layouter: RecipeLayouter,
+ mainItem: SBItemStack?,
+ ) {
+ val arrow = layouter.createArrow(bounds.minX + 90, bounds.minY + 54 - 18 / 2)
+ val tooltip = Text.empty()
+ .append(Text.stringifiedTranslatable(
+ "firmament.recipe.forge.time",
+ recipe.duration.seconds,
+ ))
+
+ if (recipe.extraText != null && recipe.extraText!!.isNotBlank()) {
+ tooltip
+ .append(Text.of("\n"))
+ .append(Text.of(recipe.extraText))
+ }
+
+ layouter.createTooltip(arrow, tooltip)
+
+ val ingredientsCenter = Point(bounds.minX + 49 - 8, bounds.minY + 54 - 8)
+ layouter.createFire(ingredientsCenter, 25)
+ val count = recipe.inputs.size
+ if (count == 1) {
+ layouter.createItemSlot(
+ ingredientsCenter.x, ingredientsCenter.y - 18,
+ SBItemStack(recipe.inputs.single()),
+ RecipeLayouter.SlotKind.SMALL_INPUT,
+ )
+ } else {
+ recipe.inputs.forEachIndexed { idx, ingredient ->
+ val rad = Math.PI * 2 * idx / count
+ layouter.createItemSlot(
+ (ingredientsCenter.x + cos(rad) * 30).toInt(), (ingredientsCenter.y + sin(rad) * 30).toInt(),
+ SBItemStack(ingredient),
+ RecipeLayouter.SlotKind.SMALL_INPUT,
+ )
+ }
+ }
+ layouter.createItemSlot(
+ bounds.minX + 124, bounds.minY + 46,
+ SBItemStack(recipe.outputStack),
+ RecipeLayouter.SlotKind.BIG_OUTPUT
+ )
+ }
+
+ override val displayHeight: Int
+ get() = 104
+
+ override fun getInputs(recipe: NEUForgeRecipe): Collection<SBItemStack> {
+ return recipe.inputs.mapNotNull { SBItemStack(it) }
+ }
+
+ override fun getOutputs(recipe: NEUForgeRecipe): Collection<SBItemStack> {
+ return listOfNotNull(SBItemStack(recipe.outputStack))
+ }
+
+ override val icon: ItemStack = ItemStack(Blocks.ANVIL)
+ override val title: Text = tr("firmament.category.forge", "Forge Recipes")
+ override val identifier: Identifier = Firmament.identifier("forge_recipe")
+
+ override fun findAllRecipes(neuRepository: NEURepository): Iterable<NEUForgeRecipe> {
+ // TODO: theres gotta be an index for these tbh.
+ return neuRepository.items.items.values.flatMap { it.recipes }.filterIsInstance<NEUForgeRecipe>()
+ }
+
+ override val typ: Class<NEUForgeRecipe>
+ get() = NEUForgeRecipe::class.java
+}
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/Base64Util.kt b/src/main/kotlin/util/Base64Util.kt
index 44bcdfd..c39c601 100644
--- a/src/main/kotlin/util/Base64Util.kt
+++ b/src/main/kotlin/util/Base64Util.kt
@@ -1,7 +1,14 @@
package moe.nea.firmament.util
+import java.util.Base64
+
object Base64Util {
+ fun decodeString(str: String): String {
+ return Base64.getDecoder().decode(str.padToValidBase64())
+ .decodeToString()
+ }
+
fun String.padToValidBase64(): String {
val align = this.length % 4
if (align == 0) return this
diff --git a/src/main/kotlin/util/BazaarPriceStrategy.kt b/src/main/kotlin/util/BazaarPriceStrategy.kt
index 002eedb..13b6d95 100644
--- a/src/main/kotlin/util/BazaarPriceStrategy.kt
+++ b/src/main/kotlin/util/BazaarPriceStrategy.kt
@@ -9,7 +9,7 @@ enum class BazaarPriceStrategy {
NPC_SELL;
fun getSellPrice(skyblockId: SkyblockId): Double {
- val bazaarEntry = HypixelStaticData.bazaarData[skyblockId] ?: return 0.0
+ val bazaarEntry = HypixelStaticData.bazaarData[skyblockId.asBazaarStock] ?: return 0.0
return when (this) {
BUY_ORDER -> bazaarEntry.quickStatus.sellPrice
SELL_ORDER -> bazaarEntry.quickStatus.buyPrice
diff --git a/src/main/kotlin/util/ChromaColourUtil.kt b/src/main/kotlin/util/ChromaColourUtil.kt
new file mode 100644
index 0000000..0130326
--- /dev/null
+++ b/src/main/kotlin/util/ChromaColourUtil.kt
@@ -0,0 +1,10 @@
+package moe.nea.firmament.util
+
+import io.github.notenoughupdates.moulconfig.ChromaColour
+import java.awt.Color
+
+fun ChromaColour.getRGBAWithoutAnimation() =
+ Color(ChromaColour.specialToSimpleRGB(toLegacyString()), true)
+
+fun Color.toChromaWithoutAnimation(timeForFullRotationInMillis: Int = 0) =
+ ChromaColour.fromRGB(red, green, blue, timeForFullRotationInMillis, alpha)
diff --git a/src/main/kotlin/util/ErrorUtil.kt b/src/main/kotlin/util/ErrorUtil.kt
index 190381d..3db4ecd 100644
--- a/src/main/kotlin/util/ErrorUtil.kt
+++ b/src/main/kotlin/util/ErrorUtil.kt
@@ -29,15 +29,31 @@ object ErrorUtil {
inline fun softError(message: String, exception: Throwable) {
if (aggressiveErrors) throw IllegalStateException(message, exception)
- else Firmament.logger.error(message, exception)
+ else logError(message, exception)
+ }
+
+ fun logError(message: String, exception: Throwable) {
+ Firmament.logger.error(message, exception)
+ }
+ fun logError(message: String) {
+ Firmament.logger.error(message)
}
inline fun softError(message: String) {
if (aggressiveErrors) error(message)
- else Firmament.logger.error(message)
+ else logError(message)
+ }
+
+ fun <T> Result<T>.intoCatch(message: String): Catch<T> {
+ return this.map { Catch.succeed(it) }.getOrElse {
+ softError(message, it)
+ Catch.fail(it)
+ }
}
class Catch<T> private constructor(val value: T?, val exc: Throwable?) {
+ fun orNull(): T? = value
+
inline fun or(block: (exc: Throwable) -> T): T {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
@@ -73,4 +89,9 @@ object ErrorUtil {
return nullable
}
+ fun softUserError(string: String) {
+ if (TestUtil.isInTest)
+ error(string)
+ MC.sendChat(tr("firmament.usererror", "Firmament encountered a user caused error: $string"))
+ }
}
diff --git a/src/main/kotlin/util/FirmFormatters.kt b/src/main/kotlin/util/FirmFormatters.kt
index 92fb9e5..03dafc5 100644
--- a/src/main/kotlin/util/FirmFormatters.kt
+++ b/src/main/kotlin/util/FirmFormatters.kt
@@ -9,27 +9,60 @@ import kotlin.io.path.isReadable
import kotlin.io.path.isRegularFile
import kotlin.io.path.listDirectoryEntries
import kotlin.math.absoluteValue
+import kotlin.math.roundToInt
import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import net.minecraft.text.Text
+import net.minecraft.util.math.BlockPos
object FirmFormatters {
+
+ private inline fun shortIf(
+ value: Double, breakpoint: Double, char: String,
+ return_: (String) -> Nothing
+ ) {
+ if (value >= breakpoint) {
+ val broken = (value / breakpoint * 10).roundToInt()
+ if (broken > 99)
+ return_((broken / 10).toString() + char)
+ val decimals = broken.toString()
+ decimals.singleOrNull()?.let {
+ return_("0.$it$char")
+ }
+ return_("${decimals[0]}.${decimals[1]}$char")
+ }
+ }
+
+ fun shortFormat(double: Double): String {
+ if (double < 0) return "-" + shortFormat(-double)
+ shortIf(double, 1_000_000_000_000.0, "t") { return it }
+ shortIf(double, 1_000_000_000.0, "b") { return it }
+ shortIf(double, 1_000_000.0, "m") { return it }
+ shortIf(double, 1_000.0, "k") { return it }
+ shortIf(double, 1.0, "") { return it }
+ return double.toString()
+ }
+
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 {
@@ -99,4 +132,11 @@ object FirmFormatters {
return if (boolean == trueIsGood) text.lime() else text.red()
}
+ fun formatPosition(position: BlockPos): Text {
+ return Text.literal("x: ${position.x}, y: ${position.y}, z: ${position.z}")
+ }
+
+ fun formatPercent(value: Double, decimals: Int = 1): String {
+ return "%.${decimals}f%%".format(value * 100)
+ }
}
diff --git a/src/main/kotlin/util/FragmentGuiScreen.kt b/src/main/kotlin/util/FragmentGuiScreen.kt
index 5e13d51..de53ac0 100644
--- a/src/main/kotlin/util/FragmentGuiScreen.kt
+++ b/src/main/kotlin/util/FragmentGuiScreen.kt
@@ -19,13 +19,9 @@ abstract class FragmentGuiScreen(
popup = MoulConfigFragment(context, position) { popup = null }
}
- override fun render(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
- super.render(context, mouseX, mouseY, delta)
- context.matrices.push()
- context.matrices.translate(0f, 0f, 1000f)
- popup?.render(context, mouseX, mouseY, delta)
- context.matrices.pop()
- }
+ fun renderPopup(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ popup?.render(context, mouseX, mouseY, delta)
+ }
private inline fun ifPopup(ifYes: (MoulConfigFragment) -> Unit): Boolean {
val p = popup ?: return false
@@ -65,7 +61,7 @@ abstract class FragmentGuiScreen(
return ifPopup {
if (!Rectangle(
it.position,
- Dimension(it.context.root.width, it.context.root.height)
+ Dimension(it.guiContext.root.width, it.guiContext.root.height)
).contains(Point(mouseX, mouseY))
&& dismissOnOutOfBounds
) {
diff --git a/src/main/kotlin/util/HoveredItemStack.kt b/src/main/kotlin/util/HoveredItemStack.kt
index a2e4ad2..1b54562 100644
--- a/src/main/kotlin/util/HoveredItemStack.kt
+++ b/src/main/kotlin/util/HoveredItemStack.kt
@@ -6,22 +6,33 @@ import net.minecraft.item.ItemStack
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.compatloader.CompatLoader
-interface HoveredItemStackProvider {
+interface HoveredItemStackProvider : Comparable<HoveredItemStackProvider> {
fun provideHoveredItemStack(screen: HandledScreen<*>): ItemStack?
+ override fun compareTo(other: HoveredItemStackProvider): Int {
+ return compareValues(this.prio, other.prio)
+ }
+
+ val prio: Int get() = 0
- companion object : CompatLoader<HoveredItemStackProvider>(HoveredItemStackProvider::class)
+ companion object : CompatLoader<HoveredItemStackProvider>(HoveredItemStackProvider::class) {
+ val sorted = HoveredItemStackProvider.allValidInstances.sorted()
+ }
}
@AutoService(HoveredItemStackProvider::class)
class VanillaScreenProvider : HoveredItemStackProvider {
+
override fun provideHoveredItemStack(screen: HandledScreen<*>): ItemStack? {
screen as AccessorHandledScreen
val vanillaSlot = screen.focusedSlot_Firmament?.stack
return vanillaSlot
}
+
+ override val prio: Int
+ get() = -1
}
val HandledScreen<*>.focusedItemStack: ItemStack?
get() =
- HoveredItemStackProvider.allValidInstances
- .firstNotNullOfOrNull { it.provideHoveredItemStack(this) }
+ HoveredItemStackProvider.sorted
+ .firstNotNullOfOrNull { it.provideHoveredItemStack(this)?.takeIf { !it.isEmpty } }
diff --git a/src/main/kotlin/util/IntUtil.kt b/src/main/kotlin/util/IntUtil.kt
new file mode 100644
index 0000000..2695906
--- /dev/null
+++ b/src/main/kotlin/util/IntUtil.kt
@@ -0,0 +1,12 @@
+package moe.nea.firmament.util
+
+object IntUtil {
+ data class RGBA(val r: Int, val g: Int, val b: Int, val a: Int)
+
+ fun Int.toRGBA(): RGBA {
+ return RGBA(
+ r = (this shr 16) and 0xFF, g = (this shr 8) and 0xFF, b = this and 0xFF, a = (this shr 24) and 0xFF
+ )
+ }
+
+}
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/LegacyTagParser.kt b/src/main/kotlin/util/LegacyTagParser.kt
index 4e08da1..5a26335 100644
--- a/src/main/kotlin/util/LegacyTagParser.kt
+++ b/src/main/kotlin/util/LegacyTagParser.kt
@@ -2,7 +2,7 @@
package moe.nea.firmament.util
-import java.util.*
+import java.util.Stack
import net.minecraft.nbt.AbstractNbtNumber
import net.minecraft.nbt.NbtByte
import net.minecraft.nbt.NbtCompound
diff --git a/src/main/kotlin/util/LegacyTagWriter.kt b/src/main/kotlin/util/LegacyTagWriter.kt
new file mode 100644
index 0000000..9889b2c
--- /dev/null
+++ b/src/main/kotlin/util/LegacyTagWriter.kt
@@ -0,0 +1,103 @@
+package moe.nea.firmament.util
+
+import kotlinx.serialization.json.JsonPrimitive
+import net.minecraft.nbt.AbstractNbtList
+import net.minecraft.nbt.NbtByte
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtDouble
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtEnd
+import net.minecraft.nbt.NbtFloat
+import net.minecraft.nbt.NbtInt
+import net.minecraft.nbt.NbtLong
+import net.minecraft.nbt.NbtShort
+import net.minecraft.nbt.NbtString
+import moe.nea.firmament.util.mc.SNbtFormatter.Companion.SIMPLE_NAME
+
+class LegacyTagWriter(val compact: Boolean) {
+ companion object {
+ fun stringify(nbt: NbtElement, compact: Boolean): String {
+ return LegacyTagWriter(compact).also { it.writeElement(nbt) }
+ .stringWriter.toString()
+ }
+
+ fun NbtElement.toLegacyString(pretty: Boolean = false): String {
+ return stringify(this, !pretty)
+ }
+ }
+
+ val stringWriter = StringBuilder()
+ var indent = 0
+ fun newLine() {
+ if (compact) return
+ stringWriter.append('\n')
+ repeat(indent) {
+ stringWriter.append(" ")
+ }
+ }
+
+ fun writeElement(nbt: NbtElement) {
+ when (nbt) {
+ is NbtInt -> stringWriter.append(nbt.value.toString())
+ is NbtString -> stringWriter.append(escapeString(nbt.value))
+ is NbtFloat -> stringWriter.append(nbt.value).append('F')
+ is NbtDouble -> stringWriter.append(nbt.value).append('D')
+ is NbtByte -> stringWriter.append(nbt.value).append('B')
+ is NbtLong -> stringWriter.append(nbt.value).append('L')
+ is NbtShort -> stringWriter.append(nbt.value).append('S')
+ is NbtCompound -> writeCompound(nbt)
+ is NbtEnd -> {}
+ is AbstractNbtList -> writeArray(nbt)
+ }
+ }
+
+ fun writeArray(nbt: AbstractNbtList) {
+ stringWriter.append('[')
+ indent++
+ newLine()
+ nbt.forEachIndexed { index, element ->
+ writeName(index.toString())
+ writeElement(element)
+ if (index != nbt.size() - 1) {
+ stringWriter.append(',')
+ newLine()
+ }
+ }
+ indent--
+ if (nbt.size() != 0)
+ newLine()
+ stringWriter.append(']')
+ }
+
+ fun writeCompound(nbt: NbtCompound) {
+ stringWriter.append('{')
+ indent++
+ newLine()
+ val entries = nbt.entrySet().sortedBy { it.key }
+ entries.forEachIndexed { index, it ->
+ writeName(it.key)
+ writeElement(it.value)
+ if (index != entries.lastIndex) {
+ stringWriter.append(',')
+ newLine()
+ }
+ }
+ indent--
+ if (nbt.size != 0)
+ newLine()
+ stringWriter.append('}')
+ }
+
+ fun escapeString(string: String): String {
+ return JsonPrimitive(string).toString()
+ }
+
+ fun escapeName(key: String): String =
+ if (key.matches(SIMPLE_NAME)) key else escapeString(key)
+
+ fun writeName(key: String) {
+ stringWriter.append(escapeName(key))
+ stringWriter.append(':')
+ if (!compact) stringWriter.append(' ')
+ }
+}
diff --git a/src/main/kotlin/util/MC.kt b/src/main/kotlin/util/MC.kt
index a60d5c4..a6e3205 100644
--- a/src/main/kotlin/util/MC.kt
+++ b/src/main/kotlin/util/MC.kt
@@ -1,23 +1,33 @@
package moe.nea.firmament.util
import io.github.moulberry.repo.data.Coordinate
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigScreenComponent
import java.util.concurrent.ConcurrentLinkedQueue
+import kotlin.jvm.optionals.getOrNull
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.hud.InGameHud
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.gui.screen.ingame.HandledScreen
import net.minecraft.client.network.ClientPlayerEntity
+import net.minecraft.client.render.GameRenderer
import net.minecraft.client.render.WorldRenderer
import net.minecraft.client.render.item.ItemRenderer
import net.minecraft.client.world.ClientWorld
import net.minecraft.entity.Entity
import net.minecraft.item.Item
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtOps
import net.minecraft.network.packet.c2s.play.CommandExecutionC2SPacket
import net.minecraft.registry.BuiltinRegistries
+import net.minecraft.registry.Registry
+import net.minecraft.registry.RegistryKey
import net.minecraft.registry.RegistryKeys
+import net.minecraft.registry.RegistryOps
import net.minecraft.registry.RegistryWrapper
import net.minecraft.resource.ReloadableResourceManagerImpl
import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import net.minecraft.util.Util
import net.minecraft.util.math.BlockPos
import net.minecraft.world.World
import moe.nea.firmament.events.TickEvent
@@ -64,7 +74,9 @@ object MC {
}
fun sendCommand(command: String) {
- player?.networkHandler?.sendCommand(command)
+ // TODO: add a queue to this and sendServerChat
+ ErrorUtil.softCheck("Server commands have an implied /", !command.startsWith("/"))
+ player?.networkHandler?.sendChatCommand(command)
}
fun onMainThread(block: () -> Unit) {
@@ -83,6 +95,7 @@ object MC {
inline val resourceManager get() = (instance.resourceManager as ReloadableResourceManagerImpl)
inline val itemRenderer: ItemRenderer get() = instance.itemRenderer
inline val worldRenderer: WorldRenderer get() = instance.worldRenderer
+ inline val gameRenderer: GameRenderer get() = instance.gameRenderer
inline val networkHandler get() = player?.networkHandler
inline val instance get() = MinecraftClient.getInstance()
inline val keyboard get() = instance.keyboard
@@ -92,26 +105,49 @@ object MC {
inline val inGameHud: InGameHud get() = instance.inGameHud
inline val font get() = instance.textRenderer
inline val soundManager get() = instance.soundManager
- inline val player: ClientPlayerEntity? get() = instance.player
+ inline val player: ClientPlayerEntity? get() = TestUtil.unlessTesting { instance.player }
inline val camera: Entity? get() = instance.cameraEntity
+ inline val stackInHand: ItemStack get() = player?.mainHandStack ?: ItemStack.EMPTY
inline val guiAtlasManager get() = instance.guiAtlasManager
- inline val world: ClientWorld? get() = instance.world
+ inline val world: ClientWorld? get() = TestUtil.unlessTesting { instance.world }
+ inline val playerName: String get() = player?.name?.unformattedString ?: MC.instance.session.username
inline var screen: Screen?
- get() = instance.currentScreen
+ get() = TestUtil.unlessTesting { instance.currentScreen }
set(value) = instance.setScreen(value)
val screenName get() = screen?.title?.unformattedString?.trim()
inline val handledScreen: HandledScreen<*>? get() = instance.currentScreen as? HandledScreen<*>
inline val window get() = instance.window
inline val currentRegistries: RegistryWrapper.WrapperLookup? get() = world?.registryManager
val defaultRegistries: RegistryWrapper.WrapperLookup by lazy { BuiltinRegistries.createWrapperLookup() }
+ val defaultRegistryNbtOps by lazy { RegistryOps.of(NbtOps.INSTANCE, defaultRegistries) }
inline val currentOrDefaultRegistries get() = currentRegistries ?: defaultRegistries
val defaultItems: RegistryWrapper.Impl<Item> by lazy { defaultRegistries.getOrThrow(RegistryKeys.ITEM) }
+ var currentTick = 0
var lastWorld: World? = null
get() {
field = world ?: field
return field
}
private set
+
+ val currentMoulConfigContext
+ get() = (screen as? MoulConfigScreenComponent)?.guiContext
+
+ fun openUrl(uri: String) {
+ Util.getOperatingSystem().open(uri)
+ }
+
+ fun <T> unsafeGetRegistryEntry(registry: RegistryKey<out Registry<T>>, identifier: Identifier) =
+ unsafeGetRegistryEntry(RegistryKey.of(registry, identifier))
+
+
+ fun <T> unsafeGetRegistryEntry(registryKey: RegistryKey<T>): T? {
+ return currentOrDefaultRegistries
+ .getOrThrow(registryKey.registryRef)
+ .getOptional(registryKey)
+ .getOrNull()
+ ?.value()
+ }
}
diff --git a/src/main/kotlin/util/MoulConfigFragment.kt b/src/main/kotlin/util/MoulConfigFragment.kt
index 36132cd..7e7f5db 100644
--- a/src/main/kotlin/util/MoulConfigFragment.kt
+++ b/src/main/kotlin/util/MoulConfigFragment.kt
@@ -1,44 +1,43 @@
-
-
package moe.nea.firmament.util
-import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
import io.github.notenoughupdates.moulconfig.gui.GuiContext
import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigScreenComponent
import me.shedaniel.math.Point
import net.minecraft.client.gui.DrawContext
+import net.minecraft.text.Text
class MoulConfigFragment(
- context: GuiContext,
- val position: Point,
- val dismiss: () -> Unit
-) : GuiComponentWrapper(context) {
- init {
- this.init(MC.instance, MC.screen!!.width, MC.screen!!.height)
- }
-
- override fun createContext(drawContext: DrawContext?): GuiImmediateContext {
- val oldContext = super.createContext(drawContext)
- return oldContext.translated(
- position.x,
- position.y,
- context.root.width,
- context.root.height,
- )
- }
-
-
- override fun render(drawContext: DrawContext?, i: Int, j: Int, f: Float) {
- val ctx = createContext(drawContext)
- val m = drawContext!!.matrices
- m.push()
- m.translate(position.x.toFloat(), position.y.toFloat(), 0F)
- context.root.render(ctx)
- m.pop()
- ctx.renderContext.doDrawTooltip()
- }
-
- override fun close() {
- dismiss()
- }
+ context: GuiContext,
+ val position: Point,
+ val dismiss: () -> Unit
+) : MoulConfigScreenComponent(Text.empty(), context, null) {
+ init {
+ this.init(MC.instance, MC.screen!!.width, MC.screen!!.height)
+ }
+
+ override fun createContext(drawContext: DrawContext?): GuiImmediateContext {
+ val oldContext = super.createContext(drawContext)
+ return oldContext.translated(
+ position.x,
+ position.y,
+ guiContext.root.width,
+ guiContext.root.height,
+ )
+ }
+
+
+ override fun render(drawContext: DrawContext, i: Int, j: Int, f: Float) {
+ val ctx = createContext(drawContext)
+ val m = drawContext.matrices
+ m.pushMatrix()
+ m.translate(position.x.toFloat(), position.y.toFloat())
+ guiContext.root.render(ctx)
+ m.popMatrix()
+ ctx.renderContext.renderExtraLayers()
+ }
+
+ override fun close() {
+ dismiss()
+ }
}
diff --git a/src/main/kotlin/util/MoulConfigUtils.kt b/src/main/kotlin/util/MoulConfigUtils.kt
index 62bf3dd..fb955ae 100644
--- a/src/main/kotlin/util/MoulConfigUtils.kt
+++ b/src/main/kotlin/util/MoulConfigUtils.kt
@@ -4,13 +4,13 @@ import io.github.notenoughupdates.moulconfig.common.IMinecraft
import io.github.notenoughupdates.moulconfig.common.MyResourceLocation
import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
import io.github.notenoughupdates.moulconfig.gui.GuiComponent
-import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
import io.github.notenoughupdates.moulconfig.gui.GuiContext
import io.github.notenoughupdates.moulconfig.gui.GuiImmediateContext
import io.github.notenoughupdates.moulconfig.gui.KeyboardEvent
import io.github.notenoughupdates.moulconfig.gui.MouseEvent
import io.github.notenoughupdates.moulconfig.observer.GetSetter
-import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigRenderContext
+import io.github.notenoughupdates.moulconfig.platform.MoulConfigScreenComponent
import io.github.notenoughupdates.moulconfig.xml.ChildCount
import io.github.notenoughupdates.moulconfig.xml.XMLContext
import io.github.notenoughupdates.moulconfig.xml.XMLGuiLoader
@@ -25,6 +25,8 @@ import kotlin.time.Duration
import kotlin.time.Duration.Companion.seconds
import net.minecraft.client.gui.DrawContext
import net.minecraft.client.gui.screen.Screen
+import net.minecraft.client.util.InputUtil
+import net.minecraft.text.Text
import moe.nea.firmament.gui.BarComponent
import moe.nea.firmament.gui.FirmButtonComponent
import moe.nea.firmament.gui.FirmHoverComponent
@@ -34,6 +36,21 @@ import moe.nea.firmament.gui.TickComponent
import moe.nea.firmament.util.render.isUntranslatedGuiDrawContext
object MoulConfigUtils {
+ @JvmStatic
+ fun main(args: Array<out String>) {
+ generateXSD(File("MoulConfig.xsd"), XMLUniverse.MOULCONFIG_XML_NS)
+ generateXSD(File("MoulConfig.Firmament.xsd"), firmUrl)
+ File("wrapper.xsd").writeText(
+ """
+<?xml version="1.0" encoding="UTF-8" ?>
+<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
+ <xs:import namespace="http://notenoughupdates.org/moulconfig" schemaLocation="MoulConfig.xsd"/>
+ <xs:import namespace="http://firmament.nea.moe/moulconfig" schemaLocation="MoulConfig.Firmament.xsd"/>
+</xs:schema>
+ """.trimIndent()
+ )
+ }
+
val firmUrl = "http://firmament.nea.moe/moulconfig"
val universe = XMLUniverse.getDefaultUniverse().also { uni ->
uni.registerMapper(java.awt.Color::class.java) {
@@ -80,9 +97,11 @@ object MoulConfigUtils {
override fun createInstance(context: XMLContext<*>, element: Element): FirmHoverComponent {
return FirmHoverComponent(
context.getChildFragment(element),
- context.getPropertyFromAttribute(element,
- QName("lines"),
- List::class.java) as Supplier<List<String>>,
+ context.getPropertyFromAttribute(
+ element,
+ QName("lines"),
+ List::class.java
+ ) as Supplier<List<String>>,
context.getPropertyFromAttribute(element, QName("delay"), Duration::class.java, 0.6.seconds),
)
}
@@ -178,10 +197,8 @@ object MoulConfigUtils {
uni.registerLoader(object : XMLGuiLoader.Basic<FixedComponent> {
override fun createInstance(context: XMLContext<*>, element: Element): FixedComponent {
return FixedComponent(
- context.getPropertyFromAttribute(element, QName("width"), Int::class.java)
- ?: error("Requires width specified"),
- context.getPropertyFromAttribute(element, QName("height"), Int::class.java)
- ?: error("Requires height specified"),
+ context.getPropertyFromAttribute(element, QName("width"), Int::class.java),
+ context.getPropertyFromAttribute(element, QName("height"), Int::class.java),
context.getChildFragment(element)
)
}
@@ -195,7 +212,7 @@ object MoulConfigUtils {
}
override fun getAttributeNames(): Map<String, Boolean> {
- return mapOf("width" to true, "height" to true)
+ return mapOf("width" to false, "height" to false)
}
})
}
@@ -209,29 +226,21 @@ object MoulConfigUtils {
generator.dumpToFile(file)
}
- @JvmStatic
- fun main(args: Array<out String>) {
- generateXSD(File("MoulConfig.xsd"), XMLUniverse.MOULCONFIG_XML_NS)
- generateXSD(File("MoulConfig.Firmament.xsd"), firmUrl)
- File("wrapper.xsd").writeText("""
-<?xml version="1.0" encoding="UTF-8" ?>
-<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
- <xs:import namespace="http://notenoughupdates.org/moulconfig" schemaLocation="MoulConfig.xsd"/>
- <xs:import namespace="http://firmament.nea.moe/moulconfig" schemaLocation="MoulConfig.Firmament.xsd"/>
-</xs:schema>
- """.trimIndent())
- }
-
- fun loadScreen(name: String, bindTo: Any, parent: Screen?): Screen {
- return object : GuiComponentWrapper(loadGui(name, bindTo)) {
+ fun wrapScreen(guiContext: GuiContext, parent: Screen?, onClose: () -> Unit = {}): Screen {
+ return object : MoulConfigScreenComponent(Text.empty(), guiContext, null) {
override fun close() {
- if (context.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) {
+ if (guiContext.onBeforeClose() == CloseEventListener.CloseAction.NO_OBJECTIONS_TO_CLOSE) {
client!!.setScreen(parent)
+ onClose()
}
}
}
}
+ fun loadScreen(name: String, bindTo: Any, parent: Screen?): Screen {
+ return wrapScreen(loadGui(name, bindTo), parent)
+ }
+
// TODO: move this utility into moulconfig (also rework guicontext into an interface so i can make this mesh better into vanilla)
fun GuiContext.adopt(element: GuiComponent) = element.foldRecursive(Unit, { comp, unit -> comp.context = this })
@@ -256,8 +265,18 @@ object MoulConfigUtils {
h: Int,
keyboardEvent: KeyboardEvent
): Boolean {
- val immContext = createInPlaceFullContext(null, IMinecraft.instance.mouseX, IMinecraft.instance.mouseY)
- return component.keyboardEvent(keyboardEvent, immContext.translated(x, y, w, h))
+ val immContext = createInPlaceFullContext(null, IMinecraft.INSTANCE.mouseX, IMinecraft.INSTANCE.mouseY)
+ if (component.keyboardEvent(keyboardEvent, immContext.translated(x, y, w, h)))
+ return true
+ if (component.context.getFocusedElement() != null) {
+ if (keyboardEvent is KeyboardEvent.KeyPressed
+ && keyboardEvent.pressed && keyboardEvent.keycode == InputUtil.GLFW_KEY_ESCAPE
+ ) {
+ component.context.setFocusedElement(null)
+ }
+ return true
+ }
+ return false
}
fun clickMCComponentInPlace(
@@ -274,15 +293,20 @@ object MoulConfigUtils {
}
fun createInPlaceFullContext(drawContext: DrawContext?, mouseX: Int, mouseY: Int): GuiImmediateContext {
- assert(drawContext?.isUntranslatedGuiDrawContext() != false)
- val context = drawContext?.let(::ModernRenderContext)
- ?: IMinecraft.instance.provideTopLevelRenderContext()
- val immContext = GuiImmediateContext(context,
- 0, 0, 0, 0,
- mouseX, mouseY,
- mouseX, mouseY,
- mouseX.toFloat(),
- mouseY.toFloat())
+ ErrorUtil.softCheck(
+ "created moulconfig context with pre-existing translations.",
+ drawContext?.isUntranslatedGuiDrawContext() != false
+ )
+ val context = drawContext?.let(::MoulConfigRenderContext)
+ ?: IMinecraft.INSTANCE.provideTopLevelRenderContext()
+ val immContext = GuiImmediateContext(
+ context,
+ 0, 0, 0, 0,
+ mouseX, mouseY,
+ mouseX, mouseY,
+ mouseX.toFloat(),
+ mouseY.toFloat()
+ )
return immContext
}
@@ -296,10 +320,10 @@ object MoulConfigUtils {
mouseY: Int
) {
val immContext = createInPlaceFullContext(this, mouseX, mouseY)
- matrices.push()
- matrices.translate(x.toFloat(), y.toFloat(), 0F)
+ matrices.pushMatrix()
+ matrices.translate(x.toFloat(), y.toFloat())
component.render(immContext.translated(x, y, w, h))
- matrices.pop()
+ matrices.popMatrix()
}
diff --git a/src/main/kotlin/util/SBData.kt b/src/main/kotlin/util/SBData.kt
index 051d070..8675842 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,83 @@ 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]",
+ )
- 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))
- }
- }
- }
- }
+ val NULL_UUID = UUID(0L, 0L)
+ val profileIdOrNil get() = profileId ?: NULL_UUID
+
+ var profileId: UUID? = null
+ get() {
+ // TODO: allow unfiltered access to this somehow
+ if (!isOnSkyblock) return null
+ return field
+ }
+
+ /**
+ * Source: https://hypixel-skyblock.fandom.com/wiki/Time_Systems
+ */
+ val hypixelTimeZone = ZoneId.of("US/Eastern")
+ private var hasReceivedProfile = false
+ var locraw: Locraw? = null
+
+ /**
+ * The current server location the player is in. This will be null outside of SkyBlock.
+ */
+ 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
+ val oldProfileId = profileId
+ locraw = Locraw(it.serverName,
+ it.serverType.getOrNull()?.name?.uppercase(),
+ it.mode.getOrNull(),
+ it.map.getOrNull())
+ SkyblockServerUpdateEvent.publish(SkyblockServerUpdateEvent(lastLocraw, locraw))
+ if(oldProfileId != profileId) {
+ ProfileSwitchEvent.publish(ProfileSwitchEvent(oldProfileId, profileId))
+ }
+ 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/src/main/kotlin/util/ScoreboardUtil.kt b/src/main/kotlin/util/ScoreboardUtil.kt
index 4311971..0970892 100644
--- a/src/main/kotlin/util/ScoreboardUtil.kt
+++ b/src/main/kotlin/util/ScoreboardUtil.kt
@@ -1,8 +1,6 @@
-
-
package moe.nea.firmament.util
-import java.util.*
+import java.util.Optional
import net.minecraft.client.gui.hud.InGameHud
import net.minecraft.scoreboard.ScoreboardDisplaySlot
import net.minecraft.scoreboard.Team
@@ -10,36 +8,48 @@ import net.minecraft.text.StringVisitable
import net.minecraft.text.Style
import net.minecraft.text.Text
import net.minecraft.util.Formatting
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.TickEvent
-fun getScoreboardLines(): List<Text> {
- val scoreboard = MC.player?.scoreboard ?: return listOf()
- val activeObjective = scoreboard.getObjectiveForSlot(ScoreboardDisplaySlot.SIDEBAR) ?: return listOf()
- return scoreboard.getScoreboardEntries(activeObjective)
- .filter { !it.hidden() }
- .sortedWith(InGameHud.SCOREBOARD_ENTRY_COMPARATOR)
- .take(15).map {
- val team = scoreboard.getScoreHolderTeam(it.owner)
- val text = it.name()
- Team.decorateName(team, text)
- }
-}
+object ScoreboardUtil {
+ var scoreboardLines: List<Text> = listOf()
+ var simplifiedScoreboardLines: List<String> = listOf()
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ scoreboardLines = getScoreboardLinesUncached()
+ simplifiedScoreboardLines = scoreboardLines.map { it.unformattedString }
+ }
+
+ private fun getScoreboardLinesUncached(): List<Text> {
+ val scoreboard = MC.player?.scoreboard ?: return listOf()
+ val activeObjective = scoreboard.getObjectiveForSlot(ScoreboardDisplaySlot.SIDEBAR) ?: return listOf()
+ return scoreboard.getScoreboardEntries(activeObjective)
+ .filter { !it.hidden() }
+ .sortedWith(InGameHud.SCOREBOARD_ENTRY_COMPARATOR)
+ .take(15).map {
+ val team = scoreboard.getScoreHolderTeam(it.owner)
+ val text = it.name()
+ Team.decorateName(team, text)
+ }
+ }
+}
fun Text.formattedString(): String {
- val sb = StringBuilder()
- visit(StringVisitable.StyledVisitor<Unit> { style, string ->
- val c = Formatting.byName(style.color?.name)
- if (c != null) {
- sb.append("§${c.code}")
- }
- if (style.isUnderlined) {
- sb.append("§n")
- }
- if (style.isBold) {
- sb.append("§l")
- }
- sb.append(string)
- Optional.empty()
- }, Style.EMPTY)
- return sb.toString().replace("§[^a-f0-9]".toRegex(), "")
+ val sb = StringBuilder()
+ visit(StringVisitable.StyledVisitor<Unit> { style, string ->
+ val c = Formatting.byName(style.color?.name)
+ if (c != null) {
+ sb.append("§${c.code}")
+ }
+ if (style.isUnderlined) {
+ sb.append("§n")
+ }
+ if (style.isBold) {
+ sb.append("§l")
+ }
+ sb.append(string)
+ Optional.empty()
+ }, Style.EMPTY)
+ return sb.toString().replace("§[^a-f0-9]".toRegex(), "")
}
diff --git a/src/main/kotlin/util/SkyBlockIsland.kt b/src/main/kotlin/util/SkyBlockIsland.kt
index c42a55c..0fa6376 100644
--- a/src/main/kotlin/util/SkyBlockIsland.kt
+++ b/src/main/kotlin/util/SkyBlockIsland.kt
@@ -1,4 +1,3 @@
-
package moe.nea.firmament.util
import kotlinx.serialization.KSerializer
@@ -13,31 +12,44 @@ import moe.nea.firmament.repo.RepoManager
@Serializable(with = SkyBlockIsland.Serializer::class)
class SkyBlockIsland
private constructor(
- val locrawMode: String,
+ val locrawMode: String,
) {
- object Serializer : KSerializer<SkyBlockIsland> {
- override val descriptor: SerialDescriptor
- get() = PrimitiveSerialDescriptor("SkyBlockIsland", PrimitiveKind.STRING)
-
- override fun deserialize(decoder: Decoder): SkyBlockIsland {
- return forMode(decoder.decodeString())
- }
-
- override fun serialize(encoder: Encoder, value: SkyBlockIsland) {
- encoder.encodeString(value.locrawMode)
- }
- }
- companion object {
- private val allIslands = mutableMapOf<String, SkyBlockIsland>()
- fun forMode(mode: String): SkyBlockIsland = allIslands.computeIfAbsent(mode, ::SkyBlockIsland)
- val HUB = forMode("hub")
- val PRIVATE_ISLAND = forMode("dynamic")
- val RIFT = forMode("rift")
- val MINESHAFT = forMode("mineshaft")
- }
-
- val userFriendlyName
- get() = RepoManager.neuRepo.constants.islands.areaNames
- .getOrDefault(locrawMode, locrawMode)
+ object Serializer : KSerializer<SkyBlockIsland> {
+ override val descriptor: SerialDescriptor
+ get() = PrimitiveSerialDescriptor("SkyBlockIsland", PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): SkyBlockIsland {
+ return forMode(decoder.decodeString())
+ }
+
+ override fun serialize(encoder: Encoder, value: SkyBlockIsland) {
+ encoder.encodeString(value.locrawMode)
+ }
+ }
+
+ companion object {
+ private val allIslands = mutableMapOf<String, SkyBlockIsland>()
+ fun forMode(mode: String): SkyBlockIsland = allIslands.computeIfAbsent(mode, ::SkyBlockIsland)
+ val HUB = forMode("hub")
+ val DWARVEN_MINES = forMode("dwarven_mines")
+ val CRYSTAL_HOLLOWS = forMode("crystal_hollows")
+ val CRIMSON_ISLE = forMode("crimson_isle")
+ val PRIVATE_ISLAND = forMode("dynamic")
+ val RIFT = forMode("rift")
+ val MINESHAFT = forMode("mineshaft")
+ val GARDEN = forMode("garden")
+ val DUNGEON = forMode("dungeon")
+ val NIL = forMode("_")
+ val GALATEA = forMode("foraging_2")
+ }
+
+ val hasCustomMining
+ get() = RepoManager.miningData.customMiningAreas[this]?.isSpecialMining ?: false
+ val isModernServer
+ get() = this == GALATEA
+
+ val userFriendlyName
+ get() = RepoManager.neuRepo.constants.islands.areaNames
+ .getOrDefault(locrawMode, locrawMode)
}
diff --git a/src/main/kotlin/util/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt
index 1c1aa77..07d4c30 100644
--- a/src/main/kotlin/util/SkyblockId.kt
+++ b/src/main/kotlin/util/SkyblockId.kt
@@ -6,6 +6,11 @@ import com.mojang.serialization.Codec
import io.github.moulberry.repo.data.NEUIngredient
import io.github.moulberry.repo.data.NEUItem
import io.github.moulberry.repo.data.Rarity
+import java.time.Instant
+import java.time.LocalDateTime
+import java.time.format.DateTimeFormatterBuilder
+import java.time.format.SignStyle
+import java.time.temporal.ChronoField
import java.util.Optional
import java.util.UUID
import kotlinx.serialization.Serializable
@@ -21,35 +26,44 @@ import net.minecraft.network.RegistryByteBuf
import net.minecraft.network.codec.PacketCodec
import net.minecraft.network.codec.PacketCodecs
import net.minecraft.util.Identifier
+import moe.nea.firmament.repo.ExpLadders
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.ItemCache.asItemStack
+import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.repo.set
import moe.nea.firmament.util.collections.WeakCache
import moe.nea.firmament.util.json.DashlessUUIDSerializer
+import moe.nea.firmament.util.mc.loreAccordingToNbt
/**
* A SkyBlock item id, as used by the NEU repo.
- * This is not exactly the format used by HyPixel, but is mostly the same.
- * Usually this id splits an id used by HyPixel into more sub items. For example `PET` becomes `$PET_ID;$PET_RARITY`,
+ * This is not exactly the format used by Hypixel, but is mostly the same.
+ * Usually this id splits an id used by Hypixel into more sub items. For example `PET` becomes `$PET_ID;$PET_RARITY`,
* with those values extracted from other metadata.
*/
@JvmInline
@Serializable
-value class SkyblockId(val neuItem: String) {
+value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> {
val identifier
- get() = Identifier.of("skyblockitem",
- neuItem.lowercase().replace(";", "__")
- .replace(":", "___")
- .replace(illlegalPathRegex) {
- it.value.toCharArray()
- .joinToString("") { "__" + it.code.toString(16).padStart(4, '0') }
- })
+ get() = Identifier.of(
+ "skyblockitem",
+ neuItem.lowercase().replace(";", "__")
+ .replace(":", "___")
+ .replace(illlegalPathRegex) {
+ it.value.toCharArray()
+ .joinToString("") { "__" + it.code.toString(16).padStart(4, '0') }
+ })
override fun toString(): String {
return neuItem
}
+ override fun compareTo(other: SkyblockId): Int {
+ return neuItem.compareTo(other.neuItem)
+ }
+
/**
- * A bazaar stock item id, as returned by the HyPixel bazaar api endpoint.
+ * A bazaar stock item id, as returned by the Hypixel bazaar api endpoint.
* These are not equivalent to the in-game ids, or the NEU repo ids, and in fact, do not refer to items, but instead
* to bazaar stocks. The main difference from [SkyblockId]s is concerning enchanted books. There are probably more,
* but for now this holds.
@@ -57,11 +71,10 @@ value class SkyblockId(val neuItem: String) {
@JvmInline
@Serializable
value class BazaarStock(val bazaarId: String) {
- fun toRepoId(): SkyblockId {
- bazaarEnchantmentRegex.matchEntire(bazaarId)?.let {
- return SkyblockId("${it.groupValues[1]};${it.groupValues[2]}")
+ companion object {
+ fun fromSkyBlockId(skyblockId: SkyblockId): BazaarStock {
+ return BazaarStock(RepoManager.neuRepo.constants.bazaarStocks.getBazaarStockOrDefault(skyblockId.neuItem))
}
- return SkyblockId(bazaarId.replace(":", "-"))
}
}
@@ -80,7 +93,9 @@ value class SkyblockId(val neuItem: String) {
val NEUItem.skyblockId get() = SkyblockId(skyblockItemId)
val NEUIngredient.skyblockId get() = SkyblockId(itemId)
+val SkyblockId.asBazaarStock get() = SkyblockId.BazaarStock.fromSkyBlockId(this)
+@ExpensiveItemCacheApi
fun NEUItem.guessRecipeId(): String? {
if (!skyblockItemId.contains(";")) return skyblockItemId
val item = this.asItemStack()
@@ -99,14 +114,19 @@ data class HypixelPetInfo(
val exp: Double = 0.0,
val candyUsed: Int = 0,
val uuid: UUID? = null,
- val active: Boolean = false,
+ val active: Boolean? = false,
+ val heldItem: String? = null,
) {
val skyblockId get() = SkyblockId("${type.uppercase()};${tier.ordinal}") // TODO: is this ordinal set up correctly?
+ val level get() = ExpLadders.getExpLadder(type, tier).getPetLevel(exp)
}
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())
@@ -116,19 +136,66 @@ val ItemStack.extraAttributes: NbtCompound
return customData.nbt
}
+fun ItemStack.modifyExtraAttributes(block: (NbtCompound) -> Unit) {
+ val baseNbt = get(DataComponentTypes.CUSTOM_DATA)?.copyNbt() ?: NbtCompound()
+ block(baseNbt)
+ set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(baseNbt))
+}
+
val ItemStack.skyblockUUIDString: String?
- get() = extraAttributes.getString("uuid")?.takeIf { it.isNotBlank() }
+ get() = extraAttributes.getString("uuid").getOrNull()?.takeIf { it.isNotBlank() }
+
+private val timestampFormat = //"10/11/21 3:39 PM"
+ DateTimeFormatterBuilder().apply {
+ appendValue(ChronoField.MONTH_OF_YEAR, 1, 2, SignStyle.NOT_NEGATIVE)
+ appendLiteral("/")
+ appendValue(ChronoField.DAY_OF_MONTH, 1, 2, SignStyle.NOT_NEGATIVE)
+ appendLiteral("/")
+ appendValueReduced(ChronoField.YEAR, 2, 2, 1950)
+ appendLiteral(" ")
+ appendValue(ChronoField.HOUR_OF_AMPM, 1, 2, SignStyle.NEVER)
+ appendLiteral(":")
+ appendValue(ChronoField.MINUTE_OF_HOUR, 2)
+ appendLiteral(" ")
+ appendText(ChronoField.AMPM_OF_DAY)
+ }.toFormatter()
+val ItemStack.timestamp
+ get() =
+ extraAttributes.getLong("timestamp").getOrNull()?.let { Instant.ofEpochMilli(it) }
+ ?: extraAttributes.getString("timestamp").getOrNull()?.let {
+ ErrorUtil.catch("Could not parse timestamp $it") {
+ LocalDateTime.from(timestampFormat.parse(it)).atZone(SBData.hypixelTimeZone)
+ .toInstant()
+ }.orNull()
+ }
val ItemStack.skyblockUUID: UUID?
get() = skyblockUUIDString?.let { UUID.fromString(it) }
private val petDataCache = WeakCache.memoize<ItemStack, Optional<HypixelPetInfo>>("PetInfo") {
val jsonString = it.extraAttributes.getString("petInfo")
+ .getOrNull()
if (jsonString.isNullOrBlank()) return@memoize Optional.empty()
- ErrorUtil.catch<HypixelPetInfo?>("Could not decode hypixel pet info") { jsonparser.decodeFromString<HypixelPetInfo>(jsonString) }
+ ErrorUtil.catch<HypixelPetInfo?>("Could not decode hypixel pet info") {
+ jsonparser.decodeFromString<HypixelPetInfo>(jsonString)
+ }
.or { null }.intoOptional()
}
+fun ItemStack.getUpgradeStars(): Int {
+ return extraAttributes.getInt("upgrade_level").getOrNull()?.takeIf { it > 0 }
+ ?: extraAttributes.getInt("dungeon_item_level").getOrNull()?.takeIf { it > 0 }
+ ?: 0
+}
+
+@Serializable
+@JvmInline
+value class ReforgeId(val id: String)
+
+fun ItemStack.getReforgeId(): ReforgeId? {
+ return extraAttributes.getString("modifier").getOrNull()?.takeIf { it.isNotBlank() }?.let(::ReforgeId)
+}
+
val ItemStack.petData: HypixelPetInfo?
get() = petDataCache(this).getOrNull()
@@ -138,10 +205,29 @@ fun ItemStack.setSkyBlockId(skyblockId: SkyblockId): ItemStack {
return this
}
+private val STORED_REGEX = "Stored: ($SHORT_NUMBER_FORMAT)/.+".toPattern()
+private val COMPOST_REGEX = "Compost Available: ($SHORT_NUMBER_FORMAT)".toPattern()
+private val GEMSTONE_SACK_REGEX = " Amount: ($SHORT_NUMBER_FORMAT)".toPattern()
+private val AMOUNT_REGEX = ".*(?:Offer amount|Amount|Order amount): ($SHORT_NUMBER_FORMAT)x".toPattern()
+fun ItemStack.getLogicalStackSize(): Long {
+ return loreAccordingToNbt.firstNotNullOfOrNull {
+ val string = it.unformattedString
+ GEMSTONE_SACK_REGEX.useMatch(string) {
+ parseShortNumber(group(1)).toLong()
+ } ?: STORED_REGEX.useMatch(string) {
+ parseShortNumber(group(1)).toLong()
+ } ?: AMOUNT_REGEX.useMatch(string) {
+ parseShortNumber(group(1)).toLong()
+ } ?: COMPOST_REGEX.useMatch(string) {
+ parseShortNumber(group(1)).toLong()
+ }
+ } ?: count.toLong()
+}
+
val ItemStack.skyBlockId: SkyblockId?
get() {
- return when (val id = extraAttributes.getString("id")) {
- "" -> {
+ return when (val id = extraAttributes.getString("id").getOrNull()) {
+ "", null -> {
null
}
@@ -151,25 +237,68 @@ val ItemStack.skyBlockId: SkyblockId?
"RUNE", "UNIQUE_RUNE" -> {
val runeData = extraAttributes.getCompound("runes")
- val runeKind = runeData.keys.singleOrNull()
+ .getOrNull()
+ val runeKind = runeData?.keys?.singleOrNull()
if (runeKind == null) SkyblockId("RUNE")
- else SkyblockId("${runeKind.uppercase()}_RUNE;${runeData.getInt(runeKind)}")
+ else SkyblockId("${runeKind.uppercase()}_RUNE;${runeData.getInt(runeKind).getOrNull()}")
}
"ABICASE" -> {
- SkyblockId("ABICASE_${extraAttributes.getString("model").uppercase()}")
+ SkyblockId("ABICASE_${extraAttributes.getString("model").getOrNull()?.uppercase()}")
}
"ENCHANTED_BOOK" -> {
val enchantmentData = extraAttributes.getCompound("enchantments")
- val enchantName = enchantmentData.keys.singleOrNull()
+ .getOrNull()
+ val enchantName = enchantmentData?.keys?.singleOrNull()
if (enchantName == null) SkyblockId("ENCHANTED_BOOK")
- else SkyblockId("${enchantName.uppercase()};${enchantmentData.getInt(enchantName)}")
+ else SkyblockId("${enchantName.uppercase()};${enchantmentData.getInt(enchantName).getOrNull()}")
+ }
+
+ "ATTRIBUTE_SHARD" -> {
+ val attributeData = extraAttributes.getCompound("attributes").getOrNull()
+ val attributeName = attributeData?.keys?.singleOrNull()
+ if (attributeName == null) SkyblockId("ATTRIBUTE_SHARD")
+ else SkyblockId(
+ "ATTRIBUTE_SHARD_${attributeName.uppercase()};${
+ attributeData.getInt(attributeName).getOrNull()
+ }"
+ )
+ }
+
+ "POTION" -> {
+ val potionData = extraAttributes.getString("potion").getOrNull()
+ val potionName = extraAttributes.getString("potion_name").getOrNull()
+ val potionLevel = extraAttributes.getInt("potion_level").getOrNull()
+ val potionType = extraAttributes.getString("potion_type").getOrNull()
+ fun String.potionNormalize() = uppercase().replace(" ", "_")
+ when {
+ potionName != null -> SkyblockId("POTION_${potionName.potionNormalize()};$potionLevel")
+ potionData != null -> SkyblockId("POTION_${potionData.potionNormalize()};$potionLevel")
+ potionType != null -> SkyblockId("POTION_${potionType.potionNormalize()}")
+ else -> SkyblockId("WATER_BOTTLE")
+ }
+ }
+
+ "PARTY_HAT_SLOTH", "PARTY_HAT_CRAB", "PARTY_HAT_CRAB_ANIMATED" -> {
+ val partyHatEmoji = extraAttributes.getString("party_hat_emoji").getOrNull()
+ val partyHatYear = extraAttributes.getInt("party_hat_year").getOrNull()
+ val partyHatColor = extraAttributes.getString("party_hat_color").getOrNull()
+ when {
+ partyHatEmoji != null -> SkyblockId("PARTY_HAT_SLOTH_${partyHatEmoji.uppercase()}")
+ partyHatYear == 2022 -> SkyblockId("PARTY_HAT_CRAB_${partyHatColor?.uppercase()}_ANIMATED")
+ else -> SkyblockId("PARTY_HAT_CRAB_${partyHatColor?.uppercase()}")
+ }
+ }
+
+ "BALLOON_HAT_2024", "BALLOON_HAT_2025" -> {
+ val partyHatYear = extraAttributes.getInt("party_hat_year").getOrNull()
+ val partyHatColor = extraAttributes.getString("party_hat_color").getOrNull()
+ SkyblockId("BALLOON_HAT_${partyHatYear}_${partyHatColor?.uppercase()}")
}
- // TODO: PARTY_HAT_CRAB{,_ANIMATED,_SLOTH},POTION
else -> {
- SkyblockId(id)
+ SkyblockId(id.replace(":", "-"))
}
}
}
diff --git a/src/main/kotlin/util/StringUtil.kt b/src/main/kotlin/util/StringUtil.kt
index 68e161a..50c5367 100644
--- a/src/main/kotlin/util/StringUtil.kt
+++ b/src/main/kotlin/util/StringUtil.kt
@@ -5,10 +5,18 @@ object StringUtil {
return splitToSequence(" ") // TODO: better boundaries
}
+ fun String.camelWords(): Sequence<String> {
+ return splitToSequence(camelWordStart)
+ }
+
+ private val camelWordStart = Regex("((?<=[a-z])(?=[A-Z]))| ")
+
fun parseIntWithComma(string: String): Int {
return string.replace(",", "").toInt()
}
+ fun String.title() = replaceFirstChar { it.titlecase() }
+
fun Iterable<String>.unwords() = joinToString(" ")
fun nextLexicographicStringOfSameLength(string: String): String {
val next = StringBuilder(string)
diff --git a/src/main/kotlin/util/TemplateUtil.kt b/src/main/kotlin/util/TemplateUtil.kt
index f4ff37c..44d9ccd 100644
--- a/src/main/kotlin/util/TemplateUtil.kt
+++ b/src/main/kotlin/util/TemplateUtil.kt
@@ -2,10 +2,9 @@
package moe.nea.firmament.util
-import java.util.*
+import java.util.Base64
import kotlinx.serialization.DeserializationStrategy
import kotlinx.serialization.SerializationStrategy
-import kotlinx.serialization.json.Json
import kotlinx.serialization.serializer
import moe.nea.firmament.Firmament
diff --git a/src/main/kotlin/util/TestUtil.kt b/src/main/kotlin/util/TestUtil.kt
index 2d38f35..da8ba38 100644
--- a/src/main/kotlin/util/TestUtil.kt
+++ b/src/main/kotlin/util/TestUtil.kt
@@ -1,6 +1,8 @@
package moe.nea.firmament.util
object TestUtil {
+ inline fun <T> unlessTesting(block: () -> T): T? = if (isInTest) null else block()
+ @JvmField
val isInTest =
Thread.currentThread().stackTrace.any {
it.className.startsWith("org.junit.") || it.className.startsWith("io.kotest.")
diff --git a/src/main/kotlin/util/WarpUtil.kt b/src/main/kotlin/util/WarpUtil.kt
index f733af7..1943edb 100644
--- a/src/main/kotlin/util/WarpUtil.kt
+++ b/src/main/kotlin/util/WarpUtil.kt
@@ -13,84 +13,87 @@ import moe.nea.firmament.commands.thenExecute
import moe.nea.firmament.events.CommandEvent
import moe.nea.firmament.events.ProcessChatEvent
import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.data.Config
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
object WarpUtil {
- val warps: Sequence<Islands.Warp> get() = RepoManager.neuRepo.constants.islands.warps
- .asSequence()
- .filter { it.warp !in ignoredWarps }
+ val warps: Sequence<Islands.Warp>
+ get() = RepoManager.neuRepo.constants.islands.warps
+ .asSequence()
+ .filter { it.warp !in ignoredWarps }
- val ignoredWarps = setOf("carnival", "")
+ val ignoredWarps = setOf("carnival", "")
- @Serializable
- data class Data(
- val excludedWarps: MutableSet<String> = mutableSetOf(),
- )
+ @Serializable
+ data class Data(
+ val excludedWarps: MutableSet<String> = mutableSetOf(),
+ )
- object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "warp-util", ::Data)
+ @Config
+ object DConfig : ProfileSpecificDataHolder<Data>(serializer(), "warp-util", ::Data)
- private var lastAttemptedWarp = ""
- private var lastWarpAttempt = TimeMark.farPast()
- fun findNearestWarp(island: SkyBlockIsland, pos: Position): Islands.Warp? {
- return warps.asSequence().filter { it.mode == island.locrawMode }.minByOrNull {
- if (DConfig.data?.excludedWarps?.contains(it.warp) == true) {
- return@minByOrNull Double.MAX_VALUE
- } else {
- return@minByOrNull squaredDist(pos, it)
- }
- }
- }
+ private var lastAttemptedWarp = ""
+ private var lastWarpAttempt = TimeMark.farPast()
+ fun findNearestWarp(island: SkyBlockIsland, pos: Position): Islands.Warp? {
+ return warps.asSequence().filter { it.mode == island.locrawMode }.minByOrNull {
+ if (DConfig.data?.excludedWarps?.contains(it.warp) == true) {
+ return@minByOrNull Double.MAX_VALUE
+ } else {
+ return@minByOrNull squaredDist(pos, it)
+ }
+ }
+ }
- private fun squaredDist(pos: Position, warp: Warp): Double {
- val dx = pos.x - warp.x
- val dy = pos.y - warp.y
- val dz = pos.z - warp.z
- return dx * dx + dy * dy + dz * dz
- }
+ private fun squaredDist(pos: Position, warp: Warp): Double {
+ val dx = pos.x - warp.x
+ val dy = pos.y - warp.y
+ val dz = pos.z - warp.z
+ return dx * dx + dy * dy + dz * dz
+ }
- fun teleportToNearestWarp(island: SkyBlockIsland, pos: Position) {
- val nearestWarp = findNearestWarp(island, pos)
- if (nearestWarp == null) {
- MC.sendChat(Text.translatable("firmament.warp-util.no-warp-found", island.userFriendlyName))
- return
- }
- if (island == SBData.skyblockLocation
- && sqrt(squaredDist(pos, nearestWarp)) > 1.1 * sqrt(squaredDist((MC.player ?: return).pos, nearestWarp))
- ) {
- MC.sendChat(Text.translatable("firmament.warp-util.already-close", nearestWarp.warp))
- return
- }
- MC.sendChat(Text.translatable("firmament.warp-util.attempting-to-warp", nearestWarp.warp))
- lastWarpAttempt = TimeMark.now()
- lastAttemptedWarp = nearestWarp.warp
- MC.sendServerCommand("warp ${nearestWarp.warp}")
- }
+ fun teleportToNearestWarp(island: SkyBlockIsland, pos: Position) {
+ val nearestWarp = findNearestWarp(island, pos)
+ if (nearestWarp == null) {
+ MC.sendChat(Text.translatable("firmament.warp-util.no-warp-found", island.userFriendlyName))
+ return
+ }
+ if (island == SBData.skyblockLocation
+ && sqrt(squaredDist(pos, nearestWarp)) > 1.1 * sqrt(squaredDist((MC.player ?: return).pos, nearestWarp))
+ ) {
+ MC.sendChat(Text.translatable("firmament.warp-util.already-close", nearestWarp.warp))
+ return
+ }
+ MC.sendChat(Text.translatable("firmament.warp-util.attempting-to-warp", nearestWarp.warp))
+ lastWarpAttempt = TimeMark.now()
+ lastAttemptedWarp = nearestWarp.warp
+ MC.sendServerCommand("warp ${nearestWarp.warp}")
+ }
- @Subscribe
- fun clearUnlockedWarpsCommand(event: CommandEvent.SubCommand) {
- event.subcommand("clearwarps") {
- thenExecute {
- DConfig.data?.excludedWarps?.clear()
- DConfig.markDirty()
- source.sendFeedback(Text.translatable("firmament.warp-util.clear-excluded"))
- }
- }
- }
+ @Subscribe
+ fun clearUnlockedWarpsCommand(event: CommandEvent.SubCommand) {
+ event.subcommand("clearwarps") {
+ thenExecute {
+ DConfig.data?.excludedWarps?.clear()
+ DConfig.markDirty()
+ source.sendFeedback(Text.translatable("firmament.warp-util.clear-excluded"))
+ }
+ }
+ }
- init {
- ProcessChatEvent.subscribe("WarpUtil:processChat") {
- if (it.unformattedString == "You haven't unlocked this fast travel destination!"
- && lastWarpAttempt.passedTime() < 2.seconds
- ) {
- DConfig.data?.excludedWarps?.add(lastAttemptedWarp)
- DConfig.markDirty()
- MC.sendChat(Text.stringifiedTranslatable("firmament.warp-util.mark-excluded", lastAttemptedWarp))
- lastWarpAttempt = TimeMark.farPast()
- }
- if (it.unformattedString.startsWith("You may now fast travel to")) {
- DConfig.data?.excludedWarps?.clear()
- DConfig.markDirty()
- }
- }
- }
+ init {
+ ProcessChatEvent.subscribe("WarpUtil:processChat") {
+ if (it.unformattedString == "You haven't unlocked this fast travel destination!"
+ && lastWarpAttempt.passedTime() < 2.seconds
+ ) {
+ DConfig.data?.excludedWarps?.add(lastAttemptedWarp)
+ DConfig.markDirty()
+ MC.sendChat(Text.stringifiedTranslatable("firmament.warp-util.mark-excluded", lastAttemptedWarp))
+ lastWarpAttempt = TimeMark.farPast()
+ }
+ if (it.unformattedString.startsWith("You may now fast travel to")) {
+ DConfig.data?.excludedWarps?.clear()
+ DConfig.markDirty()
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/util/accessors/GetRectangle.kt b/src/main/kotlin/util/accessors/GetRectangle.kt
index 37acfd9..56f420c 100644
--- a/src/main/kotlin/util/accessors/GetRectangle.kt
+++ b/src/main/kotlin/util/accessors/GetRectangle.kt
@@ -3,8 +3,8 @@
package moe.nea.firmament.util.accessors
import me.shedaniel.math.Rectangle
-import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import net.minecraft.client.gui.screen.ingame.HandledScreen
+import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
fun HandledScreen<*>.getRectangle(): Rectangle {
this as AccessorHandledScreen
diff --git a/src/main/kotlin/util/asm/AsmAnnotationUtil.kt b/src/main/kotlin/util/asm/AsmAnnotationUtil.kt
new file mode 100644
index 0000000..fb0e92c
--- /dev/null
+++ b/src/main/kotlin/util/asm/AsmAnnotationUtil.kt
@@ -0,0 +1,89 @@
+package moe.nea.firmament.util.asm
+
+import com.google.common.base.Defaults
+import java.lang.reflect.InvocationHandler
+import java.lang.reflect.Method
+import java.lang.reflect.Proxy
+import org.objectweb.asm.Type
+import org.objectweb.asm.tree.AnnotationNode
+
+object AsmAnnotationUtil {
+ class AnnotationProxy(
+ val originalType: Class<out Annotation>,
+ val annotationNode: AnnotationNode,
+ ) : InvocationHandler {
+ val offsets = annotationNode.values.withIndex()
+ .chunked(2)
+ .map { it.first() }
+ .associate { (idx, value) -> value as String to idx + 1 }
+
+ fun nestArrayType(depth: Int, comp: Class<*>): Class<*> =
+ if (depth == 0) comp
+ else java.lang.reflect.Array.newInstance(nestArrayType(depth - 1, comp), 0).javaClass
+
+ fun unmap(
+ value: Any?,
+ comp: Class<*>,
+ depth: Int,
+ ): Any? {
+ value ?: return null
+ if (depth > 0)
+ return ((value as List<Any>)
+ .map { unmap(it, comp, depth - 1) } as java.util.List<Any>)
+ .toArray(java.lang.reflect.Array.newInstance(nestArrayType(depth - 1, comp), 0) as Array<*>)
+ if (comp.isEnum) {
+ comp as Class<out Enum<*>>
+ when (value) {
+ is String -> return java.lang.Enum.valueOf(comp, value)
+ is List<*> -> return java.lang.Enum.valueOf(comp, value[1] as String)
+ else -> error("Unknown enum variant $value for $comp")
+ }
+ }
+ when (value) {
+ is Type -> return Class.forName(value.className)
+ is AnnotationNode -> return createProxy(comp as Class<out Annotation>, value)
+ is String, is Boolean, is Byte, is Double, is Int, is Float, is Long, is Short, is Char -> return value
+ }
+ error("Unknown enum variant $value for $comp")
+ }
+
+ fun defaultFor(fullType: Class<*>): Any? {
+ if (fullType.isArray) return java.lang.reflect.Array.newInstance(fullType.componentType, 0)
+ if (fullType.isPrimitive) {
+ return Defaults.defaultValue(fullType)
+ }
+ if (fullType == String::class.java)
+ return ""
+ return null
+ }
+
+ override fun invoke(
+ proxy: Any,
+ method: Method,
+ args: Array<out Any?>?
+ ): Any? {
+ val name = method.name
+ val ret = method.returnType
+ val retU = generateSequence(ret) { if (it.isArray) it.componentType else null }
+ .toList()
+ val arrayDepth = retU.size - 1
+ val componentType = retU.last()
+
+ val off = offsets[name]
+ if (off == null) {
+ return defaultFor(ret)
+ }
+ return unmap(annotationNode.values[off], componentType, arrayDepth)
+ }
+ }
+
+ fun <T : Annotation> createProxy(
+ annotationClass: Class<T>,
+ annotationNode: AnnotationNode
+ ): T {
+ require(Type.getType(annotationClass) == Type.getType(annotationNode.desc))
+ return Proxy.newProxyInstance(javaClass.classLoader,
+ arrayOf(annotationClass),
+ AnnotationProxy(annotationClass, annotationNode)) as T
+ }
+}
diff --git a/src/main/kotlin/util/async/input.kt b/src/main/kotlin/util/async/input.kt
index f22c595..35265f5 100644
--- a/src/main/kotlin/util/async/input.kt
+++ b/src/main/kotlin/util/async/input.kt
@@ -1,47 +1,89 @@
-
-
package moe.nea.firmament.util.async
+import io.github.notenoughupdates.moulconfig.gui.GuiContext
+import io.github.notenoughupdates.moulconfig.gui.component.CenterComponent
+import io.github.notenoughupdates.moulconfig.gui.component.ColumnComponent
+import io.github.notenoughupdates.moulconfig.gui.component.PanelComponent
+import io.github.notenoughupdates.moulconfig.gui.component.TextComponent
+import io.github.notenoughupdates.moulconfig.gui.component.TextFieldComponent
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
import kotlinx.coroutines.suspendCancellableCoroutine
import kotlin.coroutines.resume
+import net.minecraft.client.gui.screen.Screen
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
-import moe.nea.firmament.keybindings.IKeyBinding
+import moe.nea.firmament.gui.FirmButtonComponent
+import moe.nea.firmament.keybindings.SavedKeyBinding
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.MoulConfigUtils
+import moe.nea.firmament.util.ScreenUtil
private object InputHandler {
- data class KeyInputContinuation(val keybind: IKeyBinding, val onContinue: () -> Unit)
-
- private val activeContinuations = mutableListOf<KeyInputContinuation>()
-
- fun registerContinuation(keyInputContinuation: KeyInputContinuation): () -> Unit {
- synchronized(InputHandler) {
- activeContinuations.add(keyInputContinuation)
- }
- return {
- synchronized(this) {
- activeContinuations.remove(keyInputContinuation)
- }
- }
- }
-
- init {
- HandledScreenKeyPressedEvent.subscribe("Input:resumeAfterInput") { event ->
- synchronized(InputHandler) {
- val toRemove = activeContinuations.filter {
- event.matches(it.keybind)
- }
- toRemove.forEach { it.onContinue() }
- activeContinuations.removeAll(toRemove)
- }
- }
- }
+ data class KeyInputContinuation(val keybind: SavedKeyBinding, val onContinue: () -> Unit)
+
+ private val activeContinuations = mutableListOf<KeyInputContinuation>()
+
+ fun registerContinuation(keyInputContinuation: KeyInputContinuation): () -> Unit {
+ synchronized(InputHandler) {
+ activeContinuations.add(keyInputContinuation)
+ }
+ return {
+ synchronized(this) {
+ activeContinuations.remove(keyInputContinuation)
+ }
+ }
+ }
+
+ init {
+ HandledScreenKeyPressedEvent.subscribe("Input:resumeAfterInput") { event ->
+ synchronized(InputHandler) {
+ val toRemove = activeContinuations.filter {
+ event.matches(it.keybind)
+ }
+ toRemove.forEach { it.onContinue() }
+ activeContinuations.removeAll(toRemove)
+ }
+ }
+ }
}
-suspend fun waitForInput(keybind: IKeyBinding): Unit = suspendCancellableCoroutine { cont ->
- val unregister =
- InputHandler.registerContinuation(InputHandler.KeyInputContinuation(keybind) { cont.resume(Unit) })
- cont.invokeOnCancellation {
- unregister()
- }
+suspend fun waitForInput(keybind: SavedKeyBinding): Unit = suspendCancellableCoroutine { cont ->
+ val unregister =
+ InputHandler.registerContinuation(InputHandler.KeyInputContinuation(keybind) { cont.resume(Unit) })
+ cont.invokeOnCancellation {
+ unregister()
+ }
}
+fun createPromptScreenGuiComponent(suggestion: String, prompt: String, action: Runnable) = (run {
+ val text = GetSetter.floating(suggestion)
+ GuiContext(
+ CenterComponent(
+ PanelComponent(
+ ColumnComponent(
+ TextFieldComponent(text, 120),
+ FirmButtonComponent(TextComponent(prompt), action = action)
+ )
+ )
+ )
+ ) to text
+})
+
+suspend fun waitForTextInput(suggestion: String, prompt: String) =
+ suspendCancellableCoroutine<String> { cont ->
+ lateinit var screen: Screen
+ lateinit var text: GetSetter<String>
+ val action = {
+ if (MC.screen === screen)
+ MC.screen = null
+ // TODO: should this exit
+ cont.resume(text.get())
+ }
+ val (gui, text_) = createPromptScreenGuiComponent(suggestion, prompt, action)
+ text = text_
+ screen = MoulConfigUtils.wrapScreen(gui, null, onClose = action)
+ ScreenUtil.setScreenLater(screen)
+ cont.invokeOnCancellation {
+ action()
+ }
+ }
diff --git a/src/main/kotlin/util/collections/RangeUtil.kt b/src/main/kotlin/util/collections/RangeUtil.kt
new file mode 100644
index 0000000..a7029ac
--- /dev/null
+++ b/src/main/kotlin/util/collections/RangeUtil.kt
@@ -0,0 +1,40 @@
+package moe.nea.firmament.util.collections
+
+import kotlin.math.floor
+
+val ClosedFloatingPointRange<Float>.centre get() = (endInclusive + start) / 2
+
+fun ClosedFloatingPointRange<Float>.nonNegligibleSubSectionsAlignedWith(
+ interval: Float
+): Iterable<Float> {
+ require(interval.isFinite())
+ val range = this
+ return object : Iterable<Float> {
+ override fun iterator(): Iterator<Float> {
+ return object : FloatIterator() {
+ var polledValue: Float = range.start
+ var lastValue: Float = polledValue
+
+ override fun nextFloat(): Float {
+ if (!hasNext()) throw NoSuchElementException()
+ lastValue = polledValue
+ polledValue = Float.NaN
+ return lastValue
+ }
+
+ override fun hasNext(): Boolean {
+ if (!polledValue.isNaN()) {
+ return true
+ }
+ if (lastValue == range.endInclusive)
+ return false
+ polledValue = (floor(lastValue / interval) + 1) * interval
+ if (polledValue > range.endInclusive) {
+ polledValue = range.endInclusive
+ }
+ return true
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/util/collections/WeakCache.kt b/src/main/kotlin/util/collections/WeakCache.kt
index 38f9886..4a48c63 100644
--- a/src/main/kotlin/util/collections/WeakCache.kt
+++ b/src/main/kotlin/util/collections/WeakCache.kt
@@ -9,102 +9,108 @@ import moe.nea.firmament.features.debug.DebugLogger
* the key. Each key can have additional extra data that is used to look up values. That extra data is not required to
* be a life reference. The main Key is compared using strict reference equality. This map is not synchronized.
*/
-class WeakCache<Key : Any, ExtraKey : Any, Value : Any>(val name: String) {
- private val queue = object : ReferenceQueue<Key>() {}
- private val map = mutableMapOf<Ref, Value>()
-
- val size: Int
- get() {
- clearOldReferences()
- return map.size
- }
-
- fun clearOldReferences() {
- var successCount = 0
- var totalCount = 0
- while (true) {
- val reference = queue.poll() ?: break
- totalCount++
- if (map.remove(reference) != null)
- successCount++
- }
- if (totalCount > 0)
- logger.log { "Cleared $successCount/$totalCount references from queue" }
- }
-
- fun get(key: Key, extraData: ExtraKey): Value? {
- clearOldReferences()
- return map[Ref(key, extraData)]
- }
-
- fun put(key: Key, extraData: ExtraKey, value: Value) {
- clearOldReferences()
- map[Ref(key, extraData)] = value
- }
-
- fun getOrPut(key: Key, extraData: ExtraKey, value: (Key, ExtraKey) -> Value): Value {
- clearOldReferences()
- return map.getOrPut(Ref(key, extraData)) { value(key, extraData) }
- }
-
- fun clear() {
- map.clear()
- }
-
- init {
- allInstances.add(this)
- }
-
- companion object {
- val allInstances = InstanceList<WeakCache<*, *, *>>("WeakCaches")
- private val logger = DebugLogger("WeakCache")
- fun <Key : Any, Value : Any> memoize(name: String, function: (Key) -> Value):
- CacheFunction.NoExtraData<Key, Value> {
- return CacheFunction.NoExtraData(WeakCache(name), function)
- }
-
- fun <Key : Any, ExtraKey : Any, Value : Any> memoize(name: String, function: (Key, ExtraKey) -> Value):
- CacheFunction.WithExtraData<Key, ExtraKey, Value> {
- return CacheFunction.WithExtraData(WeakCache(name), function)
- }
- }
-
- inner class Ref(
- weakInstance: Key,
- val extraData: ExtraKey,
- ) : WeakReference<Key>(weakInstance, queue) {
- val hashCode = System.identityHashCode(weakInstance) * 31 + extraData.hashCode()
- override fun equals(other: Any?): Boolean {
- if (other !is WeakCache<*, *, *>.Ref) return false
- return other.hashCode == this.hashCode
- && other.get() === this.get()
- && other.extraData == this.extraData
- }
-
- override fun hashCode(): Int {
- return hashCode
- }
- }
-
- interface CacheFunction {
- val cache: WeakCache<*, *, *>
-
- data class NoExtraData<Key : Any, Value : Any>(
- override val cache: WeakCache<Key, Unit, Value>,
- val wrapped: (Key) -> Value,
- ) : CacheFunction, (Key) -> Value {
- override fun invoke(p1: Key): Value {
- return cache.getOrPut(p1, Unit, { a, _ -> wrapped(a) })
- }
- }
-
- data class WithExtraData<Key : Any, ExtraKey : Any, Value : Any>(
- override val cache: WeakCache<Key, ExtraKey, Value>,
- val wrapped: (Key, ExtraKey) -> Value,
- ) : CacheFunction, (Key, ExtraKey) -> Value {
- override fun invoke(p1: Key, p2: ExtraKey): Value {
- return cache.getOrPut(p1, p2, wrapped)
- }
- }
- }
+open class WeakCache<Key : Any, ExtraKey : Any, Value : Any>(val name: String) {
+ private val queue = object : ReferenceQueue<Key>() {}
+ private val map = mutableMapOf<Ref, Value>()
+
+ val size: Int
+ get() {
+ clearOldReferences()
+ return map.size
+ }
+
+ fun clearOldReferences() {
+ var successCount = 0
+ var totalCount = 0
+ while (true) {
+ val reference = queue.poll() as WeakCache<*, *, *>.Ref? ?: break
+ totalCount++
+ if (reference.shouldBeEvicted() && map.remove(reference) != null)
+ successCount++
+ }
+ if (totalCount > 0)
+ logger.log("Cleared $successCount/$totalCount references from queue")
+ }
+
+ open fun mkRef(key: Key, extraData: ExtraKey): Ref {
+ return Ref(key, extraData)
+ }
+
+ fun get(key: Key, extraData: ExtraKey): Value? {
+ clearOldReferences()
+ return map[mkRef(key, extraData)]
+ }
+
+ fun put(key: Key, extraData: ExtraKey, value: Value) {
+ clearOldReferences()
+ map[mkRef(key, extraData)] = value
+ }
+
+ fun getOrPut(key: Key, extraData: ExtraKey, value: (Key, ExtraKey) -> Value): Value {
+ clearOldReferences()
+ return map.getOrPut(mkRef(key, extraData)) { value(key, extraData) }
+ }
+
+ fun clear() {
+ map.clear()
+ }
+
+ init {
+ allInstances.add(this)
+ }
+
+ companion object {
+ val allInstances = InstanceList<WeakCache<*, *, *>>("WeakCaches")
+ private val logger = DebugLogger("WeakCache")
+ fun <Key : Any, Value : Any> memoize(name: String, function: (Key) -> Value):
+ CacheFunction.NoExtraData<Key, Value> {
+ return CacheFunction.NoExtraData(WeakCache(name), function)
+ }
+
+ fun <Key : Any, ExtraKey : Any, Value : Any> dontMemoize(name: String, function: (Key, ExtraKey) -> Value) = function
+ fun <Key : Any, ExtraKey : Any, Value : Any> memoize(name: String, function: (Key, ExtraKey) -> Value):
+ CacheFunction.WithExtraData<Key, ExtraKey, Value> {
+ return CacheFunction.WithExtraData(WeakCache(name), function)
+ }
+ }
+
+ open inner class Ref(
+ weakInstance: Key,
+ val extraData: ExtraKey,
+ ) : WeakReference<Key>(weakInstance, queue) {
+ open fun shouldBeEvicted() = true
+ val hashCode = System.identityHashCode(weakInstance) * 31 + extraData.hashCode()
+ override fun equals(other: Any?): Boolean {
+ if (other !is WeakCache<*, *, *>.Ref) return false
+ return other.hashCode == this.hashCode
+ && other.get() === this.get()
+ && other.extraData == this.extraData
+ }
+
+ override fun hashCode(): Int {
+ return hashCode
+ }
+ }
+
+ interface CacheFunction {
+ val cache: WeakCache<*, *, *>
+
+ data class NoExtraData<Key : Any, Value : Any>(
+ override val cache: WeakCache<Key, Unit, Value>,
+ val wrapped: (Key) -> Value,
+ ) : CacheFunction, (Key) -> Value {
+ override fun invoke(p1: Key): Value {
+ return cache.getOrPut(p1, Unit, { a, _ -> wrapped(a) })
+ }
+ }
+
+ data class WithExtraData<Key : Any, ExtraKey : Any, Value : Any>(
+ override val cache: WeakCache<Key, ExtraKey, Value>,
+ val wrapped: (Key, ExtraKey) -> Value,
+ ) : CacheFunction, (Key, ExtraKey) -> Value {
+ override fun invoke(p1: Key, p2: ExtraKey): Value {
+ return cache.getOrPut(p1, p2, wrapped)
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/util/compatloader/CompatLoader.kt b/src/main/kotlin/util/compatloader/CompatLoader.kt
index 6b60e87..d1073af 100644
--- a/src/main/kotlin/util/compatloader/CompatLoader.kt
+++ b/src/main/kotlin/util/compatloader/CompatLoader.kt
@@ -6,7 +6,7 @@ import kotlin.reflect.KClass
import kotlin.streams.asSequence
import moe.nea.firmament.Firmament
-abstract class CompatLoader<T : Any>(val kClass: Class<T>) {
+open class CompatLoader<T : Any>(val kClass: Class<T>) {
constructor(kClass: KClass<T>) : this(kClass.java)
val loader: ServiceLoader<T> = ServiceLoader.load(kClass)
diff --git a/src/main/kotlin/util/compatloader/CompatMeta.kt b/src/main/kotlin/util/compatloader/CompatMeta.kt
new file mode 100644
index 0000000..cf63645
--- /dev/null
+++ b/src/main/kotlin/util/compatloader/CompatMeta.kt
@@ -0,0 +1,48 @@
+package moe.nea.firmament.util.compatloader
+
+import java.util.ServiceLoader
+import moe.nea.firmament.events.subscription.SubscriptionList
+import moe.nea.firmament.init.AutoDiscoveryPlugin
+import moe.nea.firmament.util.ErrorUtil
+
+/**
+ * Declares the compat meta interface for the current source set.
+ * This is used by [CompatLoader], [SubscriptionList], and [AutoDiscoveryPlugin]. Annotate a [ICompatMeta] object with
+ * this.
+ */
+annotation class CompatMeta
+
+interface ICompatMetaGen {
+ fun owns(className: String): Boolean
+ val meta: ICompatMeta
+}
+
+interface ICompatMeta {
+ fun shouldLoad(): Boolean
+
+ companion object {
+ val allMetas = ServiceLoader
+ .load(ICompatMetaGen::class.java)
+ .toList()
+
+ fun shouldLoad(className: String): Boolean {
+ // TODO: replace this with a more performant package lookup
+ val meta = if (ErrorUtil.aggressiveErrors) {
+ val fittingMetas = allMetas.filter { it.owns(className) }
+ require(fittingMetas.size == 1) { "Orphaned or duplicate owned class $className (${fittingMetas.map { it.meta }}). Consider adding a @CompatMeta object." }
+ fittingMetas.single()
+ } else {
+ allMetas.firstOrNull { it.owns(className) }
+ }
+ return meta?.meta?.shouldLoad() ?: true
+ }
+ }
+}
+
+object CompatHelper {
+ fun isOwnedByPackage(className: String, vararg packages: String): Boolean {
+ // TODO: create package lookup structure once
+ val packageName = className.substringBeforeLast('.')
+ return packageName in packages
+ }
+}
diff --git a/src/main/kotlin/util/data/Config.kt b/src/main/kotlin/util/data/Config.kt
new file mode 100644
index 0000000..41de039
--- /dev/null
+++ b/src/main/kotlin/util/data/Config.kt
@@ -0,0 +1,15 @@
+package moe.nea.firmament.util.data
+
+import moe.nea.firmament.util.compatloader.CompatLoader
+
+@Retention(AnnotationRetention.RUNTIME)
+@Target(AnnotationTarget.CLASS)
+annotation class Config(val prefix: String = "")
+
+
+interface IConfigProvider {
+ val configs: List<IDataHolder<*>>
+ companion object {
+ val providers = CompatLoader(IConfigProvider::class)
+ }
+}
diff --git a/src/main/kotlin/util/data/DataHolder.kt b/src/main/kotlin/util/data/DataHolder.kt
index 21a6014..c138d78 100644
--- a/src/main/kotlin/util/data/DataHolder.kt
+++ b/src/main/kotlin/util/data/DataHolder.kt
@@ -1,62 +1,13 @@
-
-
package moe.nea.firmament.util.data
-import java.nio.file.Path
import kotlinx.serialization.KSerializer
-import kotlin.io.path.exists
-import kotlin.io.path.readText
-import kotlin.io.path.writeText
-import moe.nea.firmament.Firmament
+import moe.nea.firmament.gui.config.storage.ConfigStorageClass
abstract class DataHolder<T>(
- val serializer: KSerializer<T>,
- val name: String,
- val default: () -> T
-) : IDataHolder<T> {
-
-
- final override var data: T
- private set
-
- init {
- data = readValueOrDefault()
- IDataHolder.putDataHolder(this::class, this)
- }
-
- private val file: Path get() = Firmament.CONFIG_DIR.resolve("$name.json")
-
- protected fun readValueOrDefault(): T {
- if (file.exists())
- try {
- return Firmament.json.decodeFromString(
- serializer,
- file.readText()
- )
- } catch (e: Exception) {/* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/
- IDataHolder.badLoads.add(name)
- Firmament.logger.error(
- "Exception during loading of config file $name. This will reset this config.",
- e
- )
- }
- return default()
- }
-
- private fun writeValue(t: T) {
- file.writeText(Firmament.json.encodeToString(serializer, t))
- }
-
- override fun save() {
- writeValue(data)
- }
-
- override fun load() {
- data = readValueOrDefault()
- }
-
- override fun markDirty() {
- IDataHolder.markDirty(this::class)
- }
-
+ serializer: KSerializer<T>,
+ name: String,
+ default: () -> T
+) : GenericConfig<T>(name, serializer, default) {
+ override val storageClass: ConfigStorageClass
+ get() = ConfigStorageClass.STORAGE
}
diff --git a/src/main/kotlin/util/data/IDataHolder.kt b/src/main/kotlin/util/data/IDataHolder.kt
index 1e9ba98..541fc1b 100644
--- a/src/main/kotlin/util/data/IDataHolder.kt
+++ b/src/main/kotlin/util/data/IDataHolder.kt
@@ -1,71 +1,99 @@
package moe.nea.firmament.util.data
-import java.util.concurrent.CopyOnWriteArrayList
-import net.fabricmc.fabric.api.client.event.lifecycle.v1.ClientLifecycleEvents
-import kotlin.reflect.KClass
-import net.minecraft.text.Text
+import java.util.UUID
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.buildJsonObject
import moe.nea.firmament.Firmament
-import moe.nea.firmament.events.ScreenChangeEvent
-import moe.nea.firmament.util.MC
+import moe.nea.firmament.gui.config.storage.ConfigStorageClass
+import moe.nea.firmament.gui.config.storage.FirmamentConfigLoader
+import moe.nea.firmament.util.SBData
-interface IDataHolder<T> {
- companion object {
- internal var badLoads: MutableList<String> = CopyOnWriteArrayList()
- private val allConfigs: MutableMap<KClass<out IDataHolder<*>>, IDataHolder<*>> = mutableMapOf()
- private val dirty: MutableSet<KClass<out IDataHolder<*>>> = mutableSetOf()
+sealed class IDataHolder<T> {
+ fun markDirty() {
+ FirmamentConfigLoader.markDirty(this)
+ }
- internal fun <T : IDataHolder<K>, K> putDataHolder(kClass: KClass<T>, inst: IDataHolder<K>) {
- allConfigs[kClass] = inst
- }
+ init {
+ require(this.javaClass.getAnnotation(Config::class.java) != null)
+ }
- fun <T : IDataHolder<K>, K> markDirty(kClass: KClass<T>) {
- if (kClass !in allConfigs) {
- Firmament.logger.error("Tried to markDirty '${kClass.qualifiedName}', which isn't registered as 'IConfigHolder'")
- return
- }
- dirty.add(kClass)
- }
+ abstract fun keys(): Collection<T>
+ abstract fun saveTo(key: T): JsonObject
+ abstract fun loadFrom(key: T, jsonObject: JsonObject)
+ abstract fun clear()
+ abstract val storageClass: ConfigStorageClass
+}
- private fun performSaves() {
- val toSave = dirty.toList().also {
- dirty.clear()
- }
- for (it in toSave) {
- val obj = allConfigs[it]
- if (obj == null) {
- Firmament.logger.error("Tried to save '${it}', which isn't registered as 'ConfigHolder'")
- continue
- }
- obj.save()
- }
- }
+open class ProfileKeyedConfig<T>(
+ val prefix: String,
+ val serializer: KSerializer<T>,
+ val default: () -> T,
+) : IDataHolder<UUID>() {
+
+ override val storageClass: ConfigStorageClass
+ get() = ConfigStorageClass.PROFILE
+ private var _data: MutableMap<UUID, T>? = null
- private fun warnForResetConfigs() {
- if (badLoads.isNotEmpty()) {
- MC.sendChat(
- Text.literal(
- "The following configs have been reset: ${badLoads.joinToString(", ")}. " +
- "This can be intentional, but probably isn't."
- )
- )
- badLoads.clear()
- }
+ val data
+ get() = _data!!.let { map ->
+ map[SBData.profileIdOrNil]
+ ?: default().also { map[SBData.profileIdOrNil] = it }
+ } ?: error("Config $this not loaded — forgot to register?")
+
+ override fun keys(): Collection<UUID> {
+ return _data!!.keys
+ }
+
+ override fun saveTo(key: UUID): JsonObject {
+ val d = _data!!
+ return buildJsonObject {
+ put(prefix, Firmament.json.encodeToJsonElement(serializer, d[key] ?: return@buildJsonObject))
}
+ }
+
+ override fun loadFrom(key: UUID, jsonObject: JsonObject) {
+ (_data ?: mutableMapOf<UUID, T>().also { _data = it })[key] =
+ jsonObject[prefix]
+ ?.let {
+ Firmament.json.decodeFromJsonElement(serializer, it)
+ } ?: default()
+ }
- fun registerEvents() {
- ScreenChangeEvent.subscribe("IDataHolder:saveOnScreenChange") { event ->
- performSaves()
- warnForResetConfigs()
- }
- ClientLifecycleEvents.CLIENT_STOPPING.register(ClientLifecycleEvents.ClientStopping {
- performSaves()
- })
+ override fun clear() {
+ _data = null
+ }
+}
+
+abstract class GenericConfig<T>(
+ val prefix: String,
+ val serializer: KSerializer<T>,
+ val default: () -> T,
+) : IDataHolder<Unit>() {
+
+ private var _data: T? = null
+
+ val data get() = _data ?: error("Config $this not loaded — forgot to register?")
+
+ override fun keys(): Collection<Unit> {
+ return listOf(Unit)
+ }
+
+ open fun onLoad() {
+ }
+
+ override fun saveTo(key: Unit): JsonObject {
+ return buildJsonObject {
+ put(prefix, Firmament.json.encodeToJsonElement(serializer, data))
}
+ }
+ override fun loadFrom(key: Unit, jsonObject: JsonObject) {
+ _data = jsonObject[prefix]?.let { Firmament.json.decodeFromJsonElement(serializer, it) } ?: default()
+ onLoad()
}
- val data: T
- fun save()
- fun markDirty()
- fun load()
+ override fun clear() {
+ _data = null
+ }
}
diff --git a/src/main/kotlin/util/data/MultiFileDataHolder.kt b/src/main/kotlin/util/data/MultiFileDataHolder.kt
new file mode 100644
index 0000000..209f780
--- /dev/null
+++ b/src/main/kotlin/util/data/MultiFileDataHolder.kt
@@ -0,0 +1,62 @@
+package moe.nea.firmament.util.data
+
+import kotlinx.serialization.KSerializer
+import kotlin.io.path.createDirectories
+import kotlin.io.path.deleteExisting
+import kotlin.io.path.exists
+import kotlin.io.path.extension
+import kotlin.io.path.listDirectoryEntries
+import kotlin.io.path.nameWithoutExtension
+import kotlin.io.path.readText
+import kotlin.io.path.writeText
+import moe.nea.firmament.Firmament
+
+abstract class MultiFileDataHolder<T>(
+ val dataSerializer: KSerializer<T>,
+ val configName: String
+) { // TODO: abstract this + ProfileSpecificDataHolder
+ val configDirectory = Firmament.CONFIG_DIR.resolve(configName)
+ private var allData = readValues()
+ protected fun readValues(): MutableMap<String, T> {
+ if (!configDirectory.exists()) {
+ configDirectory.createDirectories()
+ }
+ val profileFiles = configDirectory.listDirectoryEntries()
+ return profileFiles
+ .filter { it.extension == "json" }
+ .mapNotNull {
+ try {
+ it.nameWithoutExtension to Firmament.json.decodeFromString(dataSerializer, it.readText())
+ } catch (e: Exception) { /* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/
+ Firmament.logger.error(
+ "Exception during loading of multi file data holder $it ($configName). This will reset that profiles config.",
+ e
+ )
+ null
+ }
+ }.toMap().toMutableMap()
+ }
+
+ fun save() {
+ if (!configDirectory.exists()) {
+ configDirectory.createDirectories()
+ }
+ val c = allData
+ configDirectory.listDirectoryEntries().forEach {
+ if (it.nameWithoutExtension !in c.mapKeys { it.toString() }) {
+ it.deleteExisting()
+ }
+ }
+ c.forEach { (name, value) ->
+ val f = configDirectory.resolve("$name.json")
+ f.writeText(Firmament.json.encodeToString(dataSerializer, value))
+ }
+ }
+
+ fun list(): Map<String, T> = allData
+ val validPathRegex = "[a-zA-Z0-9_][a-zA-Z0-9\\-_.]*".toPattern()
+ fun insert(name: String, value: T) {
+ require(validPathRegex.matcher(name).matches()) { "Not a valid name: $name" }
+ allData[name] = value
+ }
+}
diff --git a/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt b/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt
index 1cd4f22..3922c34 100644
--- a/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt
+++ b/src/main/kotlin/util/data/ProfileSpecificDataHolder.kt
@@ -1,84 +1,9 @@
-
-
package moe.nea.firmament.util.data
-import java.nio.file.Path
-import java.util.UUID
import kotlinx.serialization.KSerializer
-import kotlin.io.path.createDirectories
-import kotlin.io.path.deleteExisting
-import kotlin.io.path.exists
-import kotlin.io.path.extension
-import kotlin.io.path.listDirectoryEntries
-import kotlin.io.path.nameWithoutExtension
-import kotlin.io.path.readText
-import kotlin.io.path.writeText
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.util.SBData
abstract class ProfileSpecificDataHolder<S>(
- private val dataSerializer: KSerializer<S>,
- val configName: String,
- private val configDefault: () -> S
-) : IDataHolder<S?> {
-
- var allConfigs: MutableMap<UUID, S>
-
- override val data: S?
- get() = SBData.profileId?.let {
- allConfigs.computeIfAbsent(it) { configDefault() }
- }
-
- init {
- allConfigs = readValues()
- IDataHolder.putDataHolder(this::class, this)
- }
-
- private val configDirectory: Path get() = Firmament.CONFIG_DIR.resolve("profiles").resolve(configName)
-
- private fun readValues(): MutableMap<UUID, S> {
- if (!configDirectory.exists()) {
- configDirectory.createDirectories()
- }
- val profileFiles = configDirectory.listDirectoryEntries()
- return profileFiles
- .filter { it.extension == "json" }
- .mapNotNull {
- try {
- UUID.fromString(it.nameWithoutExtension) to Firmament.json.decodeFromString(dataSerializer, it.readText())
- } catch (e: Exception) { /* Expecting IOException and SerializationException, but Kotlin doesn't allow multi catches*/
- IDataHolder.badLoads.add(configName)
- Firmament.logger.error(
- "Exception during loading of profile specific config file $it ($configName). This will reset that profiles config.",
- e
- )
- null
- }
- }.toMap().toMutableMap()
- }
-
- override fun save() {
- if (!configDirectory.exists()) {
- configDirectory.createDirectories()
- }
- val c = allConfigs
- configDirectory.listDirectoryEntries().forEach {
- if (it.nameWithoutExtension !in c.mapKeys { it.toString() }) {
- it.deleteExisting()
- }
- }
- c.forEach { (name, value) ->
- val f = configDirectory.resolve("$name.json")
- f.writeText(Firmament.json.encodeToString(dataSerializer, value))
- }
- }
-
- override fun markDirty() {
- IDataHolder.markDirty(this::class)
- }
-
- override fun load() {
- allConfigs = readValues()
- }
-
-}
+ dataSerializer: KSerializer<S>,
+ configName: String,
+ configDefault: () -> S
+) : ProfileKeyedConfig<S>(configName, dataSerializer, configDefault)
diff --git a/src/main/kotlin/util/json/CodecSerializer.kt b/src/main/kotlin/util/json/CodecSerializer.kt
new file mode 100644
index 0000000..9ea08ad
--- /dev/null
+++ b/src/main/kotlin/util/json/CodecSerializer.kt
@@ -0,0 +1,26 @@
+package util.json
+
+import com.mojang.serialization.Codec
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
+import kotlinx.serialization.json.JsonElement
+import moe.nea.firmament.util.json.KJsonOps
+
+abstract class CodecSerializer<T>(val codec: Codec<T>) : KSerializer<T> {
+ override val descriptor: SerialDescriptor
+ get() = JsonElement.serializer().descriptor
+
+ override fun serialize(encoder: Encoder, value: T) {
+ encoder.encodeSerializableValue(
+ JsonElement.serializer(),
+ codec.encodeStart(KJsonOps.INSTANCE, value).orThrow
+ )
+ }
+
+ override fun deserialize(decoder: Decoder): T {
+ return codec.decode(KJsonOps.INSTANCE, decoder.decodeSerializableValue(JsonElement.serializer()))
+ .orThrow.first
+ }
+}
diff --git a/src/main/kotlin/util/json/DashlessUUIDSerializer.kt b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt
index acb1dc8..f4b073a 100644
--- a/src/main/kotlin/util/json/DashlessUUIDSerializer.kt
+++ b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt
@@ -9,7 +9,7 @@ import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
-import moe.nea.firmament.util.parseDashlessUUID
+import moe.nea.firmament.util.parsePotentiallyDashlessUUID
object DashlessUUIDSerializer : KSerializer<UUID> {
override val descriptor: SerialDescriptor =
@@ -17,10 +17,7 @@ object DashlessUUIDSerializer : KSerializer<UUID> {
override fun deserialize(decoder: Decoder): UUID {
val str = decoder.decodeString()
- if ("-" in str) {
- return UUID.fromString(str)
- }
- return parseDashlessUUID(str)
+ return parsePotentiallyDashlessUUID(str)
}
override fun serialize(encoder: Encoder, value: UUID) {
diff --git a/src/main/kotlin/util/json/InstantAsLongSerializer.kt b/src/main/kotlin/util/json/InstantAsLongSerializer.kt
index ad738dc..51b5f0a 100644
--- a/src/main/kotlin/util/json/InstantAsLongSerializer.kt
+++ b/src/main/kotlin/util/json/InstantAsLongSerializer.kt
@@ -2,7 +2,7 @@
package moe.nea.firmament.util.json
-import kotlinx.datetime.Instant
+import java.time.Instant
import kotlinx.serialization.KSerializer
import kotlinx.serialization.descriptors.PrimitiveKind
import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
@@ -13,10 +13,10 @@ import kotlinx.serialization.encoding.Encoder
object InstantAsLongSerializer : KSerializer<Instant> {
override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("InstantAsLongSerializer", PrimitiveKind.LONG)
override fun deserialize(decoder: Decoder): Instant {
- return Instant.fromEpochMilliseconds(decoder.decodeLong())
+ return Instant.ofEpochMilli(decoder.decodeLong())
}
override fun serialize(encoder: Encoder, value: Instant) {
- encoder.encodeLong(value.toEpochMilliseconds())
+ encoder.encodeLong(value.toEpochMilli())
}
}
diff --git a/src/main/kotlin/util/json/KJsonOps.kt b/src/main/kotlin/util/json/KJsonOps.kt
new file mode 100644
index 0000000..404ea5e
--- /dev/null
+++ b/src/main/kotlin/util/json/KJsonOps.kt
@@ -0,0 +1,131 @@
+package moe.nea.firmament.util.json
+
+import com.google.gson.internal.LazilyParsedNumber
+import com.mojang.datafixers.util.Pair
+import com.mojang.serialization.DataResult
+import com.mojang.serialization.DynamicOps
+import java.util.stream.Stream
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonNull
+import kotlinx.serialization.json.JsonObject
+import kotlinx.serialization.json.JsonPrimitive
+import kotlinx.serialization.json.boolean
+import kotlinx.serialization.json.booleanOrNull
+import kotlin.streams.asSequence
+
+class KJsonOps : DynamicOps<JsonElement> {
+ companion object {
+ val INSTANCE = KJsonOps()
+ }
+
+ override fun empty(): JsonElement {
+ return JsonNull
+ }
+
+ override fun createNumeric(num: Number): JsonElement {
+ return JsonPrimitive(num)
+ }
+
+ override fun createString(str: String): JsonElement {
+ return JsonPrimitive(str)
+ }
+
+ override fun remove(input: JsonElement, key: String): JsonElement {
+ if (input is JsonObject) {
+ return JsonObject(input.filter { it.key != key })
+ } else {
+ return input
+ }
+ }
+
+ override fun createList(stream: Stream<JsonElement>): JsonElement {
+ return JsonArray(stream.toList())
+ }
+
+ override fun getStream(input: JsonElement): DataResult<Stream<JsonElement>> {
+ if (input is JsonArray)
+ return DataResult.success(input.stream())
+ return DataResult.error { "Not a json array: $input" }
+ }
+
+ override fun createMap(map: Stream<Pair<JsonElement, JsonElement>>): JsonElement {
+ return JsonObject(map.asSequence()
+ .map { ((it.first as JsonPrimitive).content) to it.second }
+ .toMap())
+ }
+
+ override fun getMapValues(input: JsonElement): DataResult<Stream<Pair<JsonElement, JsonElement>>> {
+ if (input is JsonObject) {
+ return DataResult.success(input.entries.stream().map { Pair.of(createString(it.key), it.value) })
+ }
+ return DataResult.error { "Not a JSON object: $input" }
+ }
+
+ override fun mergeToMap(map: JsonElement, key: JsonElement, value: JsonElement): DataResult<JsonElement> {
+ if (key !is JsonPrimitive || key.isString) {
+ return DataResult.error { "key is not a string: $key" }
+ }
+ val jKey = key.content
+ val extra = mapOf(jKey to value)
+ if (map == empty()) {
+ return DataResult.success(JsonObject(extra))
+ }
+ if (map is JsonObject) {
+ return DataResult.success(JsonObject(map + extra))
+ }
+ return DataResult.error { "mergeToMap called with not a map: $map" }
+ }
+
+ override fun mergeToList(list: JsonElement, value: JsonElement): DataResult<JsonElement> {
+ if (list == empty())
+ return DataResult.success(JsonArray(listOf(value)))
+ if (list is JsonArray) {
+ return DataResult.success(JsonArray(list + value))
+ }
+ return DataResult.error { "mergeToList called with not a list: $list" }
+ }
+
+ override fun getStringValue(input: JsonElement): DataResult<String> {
+ if (input is JsonPrimitive && input.isString) {
+ return DataResult.success(input.content)
+ }
+ return DataResult.error { "Not a string: $input" }
+ }
+
+ override fun getNumberValue(input: JsonElement): DataResult<Number> {
+ if (input is JsonPrimitive && !input.isString && input.booleanOrNull == null)
+ return DataResult.success(LazilyParsedNumber(input.content))
+ return DataResult.error { "not a number: $input" }
+ }
+
+ override fun createBoolean(value: Boolean): JsonElement {
+ return JsonPrimitive(value)
+ }
+
+ override fun getBooleanValue(input: JsonElement): DataResult<Boolean> {
+ if (input is JsonPrimitive) {
+ if (input.booleanOrNull != null)
+ return DataResult.success(input.boolean)
+ return super.getBooleanValue(input)
+ }
+ return DataResult.error { "Not a boolean: $input" }
+ }
+
+ override fun <U : Any?> convertTo(output: DynamicOps<U>, input: JsonElement): U {
+ if (input is JsonObject)
+ return output.createMap(
+ input.entries.stream().map { Pair.of(output.createString(it.key), convertTo(output, it.value)) })
+ if (input is JsonArray)
+ return output.createList(input.stream().map { convertTo(output, it) })
+ if (input is JsonNull)
+ return output.empty()
+ if (input is JsonPrimitive) {
+ if (input.isString)
+ return output.createString(input.content)
+ if (input.booleanOrNull != null)
+ return output.createBoolean(input.boolean)
+ }
+ error("Unknown json value: $input")
+ }
+}
diff --git a/src/main/kotlin/util/json/KJsonUtils.kt b/src/main/kotlin/util/json/KJsonUtils.kt
new file mode 100644
index 0000000..b15119b
--- /dev/null
+++ b/src/main/kotlin/util/json/KJsonUtils.kt
@@ -0,0 +1,11 @@
+package moe.nea.firmament.util.json
+
+import kotlinx.serialization.json.JsonArray
+import kotlinx.serialization.json.JsonElement
+import kotlinx.serialization.json.JsonPrimitive
+
+fun <T : JsonElement> List<T>.asJsonArray(): JsonArray {
+ return JsonArray(this)
+}
+
+fun Iterable<String>.toJsonArray(): JsonArray = map { JsonPrimitive(it) }.asJsonArray()
diff --git a/src/main/kotlin/util/json/jsonConversion.kt b/src/main/kotlin/util/json/jsonConversion.kt
new file mode 100644
index 0000000..f921f7b
--- /dev/null
+++ b/src/main/kotlin/util/json/jsonConversion.kt
@@ -0,0 +1,65 @@
+package moe.nea.firmament.util.json
+
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonNull
+import com.google.gson.JsonObject
+import com.google.gson.JsonPrimitive
+import com.google.gson.internal.LazilyParsedNumber
+
+
+fun JsonElement.intoKotlinJson(): kotlinx.serialization.json.JsonElement {
+ when (this) {
+ is JsonNull -> return kotlinx.serialization.json.JsonNull
+ is JsonObject -> {
+ return kotlinx.serialization.json.JsonObject(
+ this.entrySet()
+ .associate { it.key to it.value.intoKotlinJson() })
+ }
+
+ is JsonArray -> {
+ return kotlinx.serialization.json.JsonArray(this.map { it.intoKotlinJson() })
+ }
+
+ is JsonPrimitive -> {
+ if (this.isString)
+ return kotlinx.serialization.json.JsonPrimitive(this.asString)
+ if (this.isBoolean)
+ return kotlinx.serialization.json.JsonPrimitive(this.asBoolean)
+ return kotlinx.serialization.json.JsonPrimitive(this.asNumber)
+ }
+
+ else -> error("Unknown json variant $this")
+ }
+}
+
+fun kotlinx.serialization.json.JsonElement.intoGson(): JsonElement {
+ when (this) {
+ is kotlinx.serialization.json.JsonNull -> return JsonNull.INSTANCE
+ is kotlinx.serialization.json.JsonPrimitive -> {
+ if (this.isString)
+ return JsonPrimitive(this.content)
+ if (this.content == "true")
+ return JsonPrimitive(true)
+ if (this.content == "false")
+ return JsonPrimitive(false)
+ return JsonPrimitive(LazilyParsedNumber(this.content))
+ }
+
+ is kotlinx.serialization.json.JsonObject -> {
+ val obj = JsonObject()
+ for ((k, v) in this) {
+ obj.add(k, v.intoGson())
+ }
+ return obj
+ }
+
+ is kotlinx.serialization.json.JsonArray -> {
+ val arr = JsonArray()
+ for (v in this) {
+ arr.add(v.intoGson())
+ }
+ return arr
+ }
+ }
+}
diff --git a/src/main/kotlin/util/math/GChainReconciliation.kt b/src/main/kotlin/util/math/GChainReconciliation.kt
new file mode 100644
index 0000000..37998d5
--- /dev/null
+++ b/src/main/kotlin/util/math/GChainReconciliation.kt
@@ -0,0 +1,102 @@
+package moe.nea.firmament.util.math
+
+import kotlin.math.min
+
+/**
+ * Algorithm for (sort of) cheap reconciliation of two cycles with missing frames.
+ */
+object GChainReconciliation {
+ // Step one: Find the most common element and shift the arrays until it is at the start in both (this could be just rotating until minimal levenshtein distance or smth. that would be way better for cycles with duplicates, but i do not want to implement levenshtein as well)
+ // Step two: Find the first different element.
+ // Step three: Find the next index of both of the elements.
+ // Step four: Insert the element that is further away.
+
+ fun <T> Iterable<T>.frequencies(): Map<T, Int> {
+ val acc = mutableMapOf<T, Int>()
+ for (t in this) {
+ acc.compute(t, { _, old -> (old ?: 0) + 1 })
+ }
+ return acc
+ }
+
+ fun <T> findMostCommonlySharedElement(
+ leftChain: List<T>,
+ rightChain: List<T>,
+ ): T {
+ val lf = leftChain.frequencies()
+ val rf = rightChain.frequencies()
+ val mostCommonlySharedElement = lf.maxByOrNull { min(it.value, rf[it.key] ?: 0) }?.key
+ if (mostCommonlySharedElement == null || mostCommonlySharedElement !in rf)
+ error("Could not find a shared element")
+ return mostCommonlySharedElement
+ }
+
+ fun <T> List<T>.getMod(index: Int): T {
+ return this[index.mod(size)]
+ }
+
+ fun <T> List<T>.rotated(offset: Int): List<T> {
+ val newList = mutableListOf<T>()
+ for (index in indices) {
+ newList.add(getMod(index - offset))
+ }
+ return newList
+ }
+
+ fun <T> shiftToFront(list: List<T>, element: T): List<T> {
+ val shiftDistance = list.indexOf(element)
+ require(shiftDistance >= 0)
+ return list.rotated(-shiftDistance)
+ }
+
+ fun <T> List<T>.indexOfOrMaxInt(element: T): Int = indexOf(element).takeUnless { it < 0 } ?: Int.MAX_VALUE
+
+ fun <T> reconcileCycles(
+ leftChain: List<T>,
+ rightChain: List<T>,
+ ): List<T> {
+ val mostCommonElement = findMostCommonlySharedElement(leftChain, rightChain)
+ val left = shiftToFront(leftChain, mostCommonElement).toMutableList()
+ val right = shiftToFront(rightChain, mostCommonElement).toMutableList()
+
+ var index = 0
+ while (index < left.size && index < right.size) {
+ val leftEl = left[index]
+ val rightEl = right[index]
+ if (leftEl == rightEl) {
+ index++
+ continue
+ }
+ val nextLeftInRight = right.subList(index, right.size)
+ .indexOfOrMaxInt(leftEl)
+
+ val nextRightInLeft = left.subList(index, left.size)
+ .indexOfOrMaxInt(rightEl)
+ if (nextLeftInRight < nextRightInLeft) {
+ left.add(index, rightEl)
+ } else if (nextRightInLeft < nextLeftInRight) {
+ right.add(index, leftEl)
+ } else {
+ index++
+ }
+ }
+ return if (left.size < right.size) right else left
+ }
+
+ fun <T> isValidCycle(longList: List<T>, cycle: List<T>): Boolean {
+ for ((i, value) in longList.withIndex()) {
+ if (cycle.getMod(i) != value)
+ return false
+ }
+ return true
+ }
+
+ fun <T> List<T>.shortenCycle(): List<T> {
+ for (i in (1..<size)) {
+ if (isValidCycle(this, subList(0, i)))
+ return subList(0, i)
+ }
+ return this
+ }
+
+}
diff --git a/src/main/kotlin/util/math/Projections.kt b/src/main/kotlin/util/math/Projections.kt
new file mode 100644
index 0000000..359b21b
--- /dev/null
+++ b/src/main/kotlin/util/math/Projections.kt
@@ -0,0 +1,46 @@
+package moe.nea.firmament.util.math
+
+import kotlin.math.absoluteValue
+import kotlin.math.cos
+import kotlin.math.sin
+import net.minecraft.util.math.Vec2f
+import moe.nea.firmament.util.render.wrapAngle
+
+object Projections {
+ object Two {
+ val ε = 1e-6
+ val π = moe.nea.firmament.util.render.π
+ val τ = 2 * π
+
+ fun isNullish(float: Float) = float.absoluteValue < ε
+
+ fun xInterceptOfLine(origin: Vec2f, direction: Vec2f): Vec2f? {
+ if (isNullish(direction.x))
+ return Vec2f(origin.x, 0F)
+ if (isNullish(direction.y))
+ return null
+
+ val slope = direction.y / direction.x
+ return Vec2f(origin.x - origin.y / slope, 0F)
+ }
+
+ fun interceptAlongCardinal(distanceFromAxis: Float, slope: Float): Float? {
+ if (isNullish(slope))
+ return null
+ return -distanceFromAxis / slope
+ }
+
+ fun projectAngleOntoUnitBox(angleRadians: Double): Vec2f {
+ val angleRadians = wrapAngle(angleRadians)
+ val cx = cos(angleRadians)
+ val cy = sin(angleRadians)
+
+ val ex = 1 / cx.absoluteValue
+ val ey = 1 / cy.absoluteValue
+
+ val e = minOf(ex, ey)
+
+ return Vec2f((cx * e).toFloat(), (cy * e).toFloat())
+ }
+ }
+}
diff --git a/src/main/kotlin/util/mc/ArmorUtil.kt b/src/main/kotlin/util/mc/ArmorUtil.kt
new file mode 100644
index 0000000..fd1867c
--- /dev/null
+++ b/src/main/kotlin/util/mc/ArmorUtil.kt
@@ -0,0 +1,8 @@
+package moe.nea.firmament.util.mc
+
+import net.minecraft.entity.EquipmentSlot
+import net.minecraft.entity.LivingEntity
+
+val LivingEntity.iterableArmorItems
+ get() = EquipmentSlot.entries.asSequence()
+ .map { it to getEquippedStack(it) }
diff --git a/src/main/kotlin/util/mc/CustomRenderPassHelper.kt b/src/main/kotlin/util/mc/CustomRenderPassHelper.kt
new file mode 100644
index 0000000..295f727
--- /dev/null
+++ b/src/main/kotlin/util/mc/CustomRenderPassHelper.kt
@@ -0,0 +1,160 @@
+package moe.nea.firmament.util.mc
+
+import com.mojang.blaze3d.buffers.GpuBuffer
+import com.mojang.blaze3d.buffers.GpuBufferSlice
+import com.mojang.blaze3d.buffers.Std140Builder
+import com.mojang.blaze3d.pipeline.RenderPipeline
+import com.mojang.blaze3d.systems.RenderPass
+import com.mojang.blaze3d.systems.RenderSystem
+import com.mojang.blaze3d.vertex.VertexFormat
+import java.nio.ByteBuffer
+import java.nio.ByteOrder
+import java.util.OptionalDouble
+import java.util.OptionalInt
+import org.joml.Vector4f
+import net.minecraft.client.gl.Framebuffer
+import net.minecraft.client.render.BufferBuilder
+import net.minecraft.client.render.BuiltBuffer
+import net.minecraft.client.texture.AbstractTexture
+import net.minecraft.client.util.BufferAllocator
+import net.minecraft.util.Identifier
+import net.minecraft.util.math.MathHelper
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.MC
+
+
+class CustomRenderPassHelper(
+ val labelSupplier: () -> String,
+ val drawMode: VertexFormat.DrawMode,
+ val vertexFormat: VertexFormat,
+ val frameBuffer: Framebuffer,
+ val hasDepth: Boolean,
+) : AutoCloseable {
+ private val scope = mutableListOf<AutoCloseable>()
+ private val preparations = mutableListOf<(RenderPass) -> Unit>()
+ val device = RenderSystem.getDevice()
+ private var hasPipelineAction = false
+ private var hasSetDefaultUniforms = false
+ val commandEncoder = device.createCommandEncoder()
+ fun setPipeline(pipeline: RenderPipeline) {
+ ErrorUtil.softCheck("Already has a pipeline", !hasPipelineAction)
+ hasPipelineAction = true
+ queueAction {
+ it.setPipeline(pipeline)
+ }
+ }
+
+ fun bindSampler(name: String, texture: Identifier) {
+ bindSampler(name, MC.textureManager.getTexture(texture))
+ }
+
+ fun bindSampler(name: String, texture: AbstractTexture) {
+ queueAction { it.bindSampler(name, texture.glTextureView) }
+ }
+
+
+ fun dontSetDefaultUniforms() {
+ hasSetDefaultUniforms = true
+ }
+
+ fun setAllDefaultUniforms() {
+ hasSetDefaultUniforms = true
+ queueAction {
+ RenderSystem.bindDefaultUniforms(it)
+ }
+ setUniform(
+ "DynamicTransforms", RenderSystem.getDynamicUniforms()
+ .write(
+ RenderSystem.getModelViewMatrix(),
+ Vector4f(1.0F, 1.0F, 1.0F, 1.0F),
+ RenderSystem.getModelOffset(),
+ RenderSystem.getTextureMatrix(),
+ RenderSystem.getShaderLineWidth()
+ )
+ )
+ }
+
+ fun setUniform(name: String, slice: GpuBufferSlice) = queueAction { it.setUniform(name, slice) }
+ fun setUniform(name: String, slice: GpuBuffer) = queueAction { it.setUniform(name, slice) }
+
+ fun setUniform(name: String, size: Int, labelSupplier: () -> String = { name }, init: (Std140Builder) -> Unit) {
+ val buffer = createUniformBuffer(labelSupplier, allocateByteBuf(size, init))
+ setUniform(name, buffer)
+ }
+
+ var vertices: BuiltBuffer? = null
+
+ fun uploadVertices(size: Int, init: (BufferBuilder) -> Unit) {
+ uploadVertices(
+ BufferBuilder(queueClose(BufferAllocator(size)), drawMode, vertexFormat)
+ .also(init)
+ .end()
+ )
+ }
+
+ fun uploadVertices(buffer: BuiltBuffer) {
+ queueClose(buffer)
+ ErrorUtil.softCheck("Vertices have already been uploaded", vertices == null)
+ vertices = buffer
+ val vertexBuffer = vertexFormat.uploadImmediateVertexBuffer(buffer.buffer)
+ val indexBufferConstructor = RenderSystem.getSequentialBuffer(drawMode)
+ val indexBuffer = indexBufferConstructor.getIndexBuffer(buffer.drawParameters.indexCount)
+ queueAction {
+ it.setIndexBuffer(indexBuffer, indexBufferConstructor.indexType)
+ it.setVertexBuffer(0, vertexBuffer)
+ }
+ }
+
+ fun createUniformBuffer(labelSupplier: () -> String, buffer: ByteBuffer): GpuBuffer {
+ return queueClose(
+ device.createBuffer(
+ labelSupplier::invoke,
+ GpuBuffer.USAGE_UNIFORM or GpuBuffer.USAGE_MAP_READ,
+ buffer
+ )
+ )
+ }
+
+ fun allocateByteBuf(size: Int, init: (Std140Builder) -> Unit): ByteBuffer {
+ return Std140Builder.intoBuffer( // TODO: i really dont know about this 16 align? but it seems to be generally correct.
+ ByteBuffer
+ .allocateDirect(MathHelper.roundUpToMultiple(size, 16))
+ .order(ByteOrder.nativeOrder())
+ ).also(init).get()
+ }
+
+ fun queueAction(action: (RenderPass) -> Unit) {
+ preparations.add(action)
+ }
+
+ fun <T : AutoCloseable> queueClose(t: T): T = t.also { scope.add(it) }
+ override fun close() {
+ scope.reversed().forEach { it.close() }
+ }
+
+ object DrawToken
+
+ fun draw(): DrawToken {
+ val vertexData = (ErrorUtil.notNullOr(vertices, "No vertex data uploaded") { return DrawToken })
+ ErrorUtil.softCheck("Missing default uniforms", hasSetDefaultUniforms)
+ ErrorUtil.softCheck("Missing a pipeline", hasPipelineAction)
+ val renderPass = queueClose(
+ commandEncoder.createRenderPass(
+ labelSupplier::invoke,
+ RenderSystem.outputColorTextureOverride ?: frameBuffer.getColorAttachmentView(),
+ OptionalInt.empty(),
+ (RenderSystem.outputDepthTextureOverride
+ ?: frameBuffer.getDepthAttachmentView()).takeIf { frameBuffer.useDepthAttachment && hasDepth },
+ OptionalDouble.empty()
+ )
+ )
+ preparations.forEach { it(renderPass) }
+ renderPass.drawIndexed(
+ 0,
+ 0,
+ vertexData.drawParameters.indexCount,
+ 1
+ )
+ return DrawToken
+ }
+}
diff --git a/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt b/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt
index 012f52e..0866665 100644
--- a/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt
+++ b/src/main/kotlin/util/mc/FirmamentDataComponentTypes.kt
@@ -1,12 +1,15 @@
package moe.nea.firmament.util.mc
import com.mojang.serialization.Codec
+import io.netty.buffer.ByteBuf
import net.minecraft.component.ComponentType
+import net.minecraft.network.codec.PacketCodec
import net.minecraft.registry.Registries
import net.minecraft.registry.Registry
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ClientInitEvent
+import moe.nea.firmament.repo.MiningRepoData
object FirmamentDataComponentTypes {
@@ -26,11 +29,32 @@ object FirmamentDataComponentTypes {
)
}
+ fun <T> errorCodec(message: String): PacketCodec<in ByteBuf, T> =
+ object : PacketCodec<ByteBuf, T> {
+ override fun decode(buf: ByteBuf?): T? {
+ error(message)
+ }
+
+ override fun encode(buf: ByteBuf?, value: T?) {
+ error(message)
+ }
+ }
+
+ fun <T, B : ComponentType.Builder<T>> B.neverEncode(message: String = "This element should never be encoded or decoded"): B {
+ packetCodec(errorCodec(message))
+ codec(null)
+ return this
+ }
+
val IS_BROKEN = register<Boolean>(
"is_broken"
) {
it.codec(Codec.BOOL.fieldOf("is_broken").codec())
}
+ val CUSTOM_MINING_BLOCK_DATA = register<MiningRepoData.CustomMiningBlock>("custom_mining_block") {
+ it.neverEncode()
+ }
+
}
diff --git a/src/main/kotlin/util/mc/InitLevel.kt b/src/main/kotlin/util/mc/InitLevel.kt
new file mode 100644
index 0000000..2c3eedb
--- /dev/null
+++ b/src/main/kotlin/util/mc/InitLevel.kt
@@ -0,0 +1,25 @@
+package moe.nea.firmament.util.mc
+
+enum class InitLevel {
+ STARTING,
+ MC_INIT,
+ RENDER_INIT,
+ RENDER,
+ MAIN_MENU,
+ ;
+
+ companion object {
+ var initLevel = InitLevel.STARTING
+ private set
+
+ @JvmStatic
+ fun isAtLeast(wantedLevel: InitLevel): Boolean = initLevel >= wantedLevel
+
+ @JvmStatic
+ fun bump(nextLevel: InitLevel) {
+ if (nextLevel.ordinal != initLevel.ordinal + 1)
+ error("Cannot bump initLevel $nextLevel from $initLevel")
+ initLevel = nextLevel
+ }
+ }
+}
diff --git a/src/main/kotlin/util/mc/IntrospectableItemModelManager.kt b/src/main/kotlin/util/mc/IntrospectableItemModelManager.kt
new file mode 100644
index 0000000..e546fd3
--- /dev/null
+++ b/src/main/kotlin/util/mc/IntrospectableItemModelManager.kt
@@ -0,0 +1,7 @@
+package moe.nea.firmament.util.mc
+
+import net.minecraft.util.Identifier
+
+interface IntrospectableItemModelManager {
+ fun hasModel_firmament(identifier: Identifier): Boolean
+}
diff --git a/src/main/kotlin/util/mc/ItemUtil.kt b/src/main/kotlin/util/mc/ItemUtil.kt
index 13519cf..3cabb8e 100644
--- a/src/main/kotlin/util/mc/ItemUtil.kt
+++ b/src/main/kotlin/util/mc/ItemUtil.kt
@@ -1,20 +1,30 @@
package moe.nea.firmament.util.mc
+import kotlin.jvm.optionals.getOrNull
import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtOps
+import net.minecraft.registry.RegistryOps
+import net.minecraft.registry.RegistryWrapper
import net.minecraft.text.Text
+import moe.nea.firmament.util.MC
fun ItemStack.appendLore(args: List<Text>) {
- if (args.isEmpty()) return
- modifyLore {
- val loreList = loreAccordingToNbt.toMutableList()
- for (arg in args) {
- loreList.add(arg)
- }
- loreList
- }
+ if (args.isEmpty()) return
+ modifyLore {
+ val loreList = loreAccordingToNbt.toMutableList()
+ for (arg in args) {
+ loreList.add(arg)
+ }
+ loreList
+ }
}
fun ItemStack.modifyLore(update: (List<Text>) -> List<Text>) {
- val loreList = loreAccordingToNbt
- loreAccordingToNbt = update(loreList)
+ val loreList = loreAccordingToNbt
+ loreAccordingToNbt = update(loreList)
+}
+
+fun loadItemFromNbt(nbt: NbtCompound, registries: RegistryWrapper.WrapperLookup = MC.defaultRegistries): ItemStack? {
+ return ItemStack.CODEC.decode(RegistryOps.of(NbtOps.INSTANCE, registries), nbt).result().getOrNull()?.first
}
diff --git a/src/main/kotlin/util/mc/MCTabListAPI.kt b/src/main/kotlin/util/mc/MCTabListAPI.kt
new file mode 100644
index 0000000..66bdd55
--- /dev/null
+++ b/src/main/kotlin/util/mc/MCTabListAPI.kt
@@ -0,0 +1,96 @@
+package moe.nea.firmament.util.mc
+
+import com.mojang.serialization.Codec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import java.util.Optional
+import org.jetbrains.annotations.TestOnly
+import net.minecraft.client.gui.hud.PlayerListHud
+import net.minecraft.nbt.NbtOps
+import net.minecraft.scoreboard.Team
+import net.minecraft.text.Text
+import net.minecraft.text.TextCodecs
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.features.debug.DeveloperFeatures
+import moe.nea.firmament.features.debug.ExportedTestConstantMeta
+import moe.nea.firmament.mixins.accessor.AccessorPlayerListHud
+import moe.nea.firmament.util.ClipboardUtils
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.intoOptional
+import moe.nea.firmament.util.mc.SNbtFormatter.Companion.toPrettyString
+
+object MCTabListAPI {
+
+ fun PlayerListHud.cast() = this as AccessorPlayerListHud
+
+ @Subscribe
+ fun onTick(event: TickEvent) {
+ _currentTabList = null
+ }
+
+ @Subscribe
+ fun devCommand(event: CommandEvent.SubCommand) {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
+ thenLiteral("copytablist") {
+ thenExecute {
+ currentTabList.body.forEach {
+ MC.sendChat(Text.literal(TextCodecs.CODEC.encodeStart(NbtOps.INSTANCE, it).orThrow.toString()))
+ }
+ var compound = CurrentTabList.CODEC.encodeStart(NbtOps.INSTANCE, currentTabList).orThrow
+ compound = ExportedTestConstantMeta.SOURCE_CODEC.encode(
+ ExportedTestConstantMeta.current,
+ NbtOps.INSTANCE,
+ compound
+ ).orThrow
+ ClipboardUtils.setTextContent(
+ compound.toPrettyString()
+ )
+ }
+ }
+ }
+ }
+
+ @get:TestOnly
+ @set:TestOnly
+ var _currentTabList: CurrentTabList? = null
+
+ val currentTabList get() = _currentTabList ?: getTabListNow().also { _currentTabList = it }
+
+ data class CurrentTabList(
+ val header: Optional<Text>,
+ val footer: Optional<Text>,
+ val body: List<Text>,
+ ) {
+ companion object {
+ val CODEC: Codec<CurrentTabList> = RecordCodecBuilder.create {
+ it.group(
+ TextCodecs.CODEC.optionalFieldOf("header").forGetter(CurrentTabList::header),
+ TextCodecs.CODEC.optionalFieldOf("footer").forGetter(CurrentTabList::footer),
+ TextCodecs.CODEC.listOf().fieldOf("body").forGetter(CurrentTabList::body),
+ ).apply(it, ::CurrentTabList)
+ }
+ }
+ }
+
+ private fun getTabListNow(): CurrentTabList {
+ // This is a precondition for PlayerListHud.collectEntries to be valid
+ MC.networkHandler ?: return CurrentTabList(Optional.empty(), Optional.empty(), emptyList())
+ val hud = MC.inGameHud.playerListHud.cast()
+ val entries = hud.collectPlayerEntries_firmament()
+ .map {
+ it.displayName ?: run {
+ val team = it.scoreboardTeam
+ val name = it.profile.name
+ Team.decorateName(team, Text.literal(name))
+ }
+ }
+ return CurrentTabList(
+ header = hud.header_firmament.intoOptional(),
+ footer = hud.footer_firmament.intoOptional(),
+ body = entries,
+ )
+ }
+}
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<Text>
+ get() = get(DataComponentTypes.LORE)?.lines ?: listOf()
set(value) {
set(DataComponentTypes.LORE, LoreComponent(value))
}
diff --git a/src/main/kotlin/util/mc/NbtPrism.kt b/src/main/kotlin/util/mc/NbtPrism.kt
new file mode 100644
index 0000000..f13fad5
--- /dev/null
+++ b/src/main/kotlin/util/mc/NbtPrism.kt
@@ -0,0 +1,85 @@
+package moe.nea.firmament.util.mc
+
+import com.google.gson.Gson
+import com.google.gson.JsonArray
+import com.google.gson.JsonElement
+import com.google.gson.JsonPrimitive
+import com.mojang.brigadier.StringReader
+import com.mojang.brigadier.arguments.ArgumentType
+import com.mojang.brigadier.arguments.StringArgumentType
+import com.mojang.serialization.JsonOps
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtList
+import net.minecraft.nbt.NbtOps
+import net.minecraft.nbt.NbtString
+import moe.nea.firmament.util.Base64Util
+
+class NbtPrism(val path: List<String>) {
+ companion object {
+ fun fromElement(path: JsonElement): NbtPrism? {
+ if (path is JsonArray) {
+ return NbtPrism(path.map { (it as JsonPrimitive).asString })
+ } else if (path is JsonPrimitive && path.isString) {
+ return NbtPrism(path.asString.split("."))
+ }
+ return null
+ }
+ }
+
+ object Argument : ArgumentType<NbtPrism> {
+ override fun parse(reader: StringReader): NbtPrism? {
+ return fromElement(JsonPrimitive(StringArgumentType.string().parse(reader)))
+ }
+
+ override fun getExamples(): Collection<String?>? {
+ return listOf("some.nbt.path", "some.other.*", "some.path.*json.in.a.json.string")
+ }
+ }
+
+ override fun toString(): String {
+ return "Prism($path)"
+ }
+
+ fun access(root: NbtElement): Collection<NbtElement> {
+ var rootSet = mutableListOf(root)
+ var switch = mutableListOf<NbtElement>()
+ for (pathSegment in path) {
+ if (pathSegment == ".") continue
+ if (pathSegment != "*" && pathSegment.startsWith("*")) {
+ if (pathSegment == "*json") {
+ for (element in rootSet) {
+ val eString = element.asString().getOrNull() ?: continue
+ val element = Gson().fromJson(eString, JsonElement::class.java)
+ switch.add(JsonOps.INSTANCE.convertTo(NbtOps.INSTANCE, element))
+ }
+ } else if (pathSegment == "*base64") {
+ for (element in rootSet) {
+ val string = element.asString().getOrNull() ?: continue
+ switch.add(NbtString.of(Base64Util.decodeString(string)))
+ }
+ }
+ }
+ for (element in rootSet) {
+ if (element is NbtList) {
+ if (pathSegment == "*")
+ switch.addAll(element)
+ val index = pathSegment.toIntOrNull() ?: continue
+ if (index !in element.indices) continue
+ switch.add(element[index])
+ }
+ if (element is NbtCompound) {
+ if (pathSegment == "*")
+ element.keys.mapTo(switch) { element.get(it)!! }
+ switch.add(element.get(pathSegment) ?: continue)
+ }
+ }
+ val temp = switch
+ switch = rootSet
+ rootSet = temp
+ switch.clear()
+ }
+ return rootSet
+ }
+}
diff --git a/src/main/kotlin/util/mc/NbtUtil.kt b/src/main/kotlin/util/mc/NbtUtil.kt
new file mode 100644
index 0000000..2cab1c7
--- /dev/null
+++ b/src/main/kotlin/util/mc/NbtUtil.kt
@@ -0,0 +1,10 @@
+package moe.nea.firmament.util.mc
+
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtList
+
+fun Iterable<NbtElement>.toNbtList() = NbtList().also {
+ for (element in this) {
+ it.add(element)
+ }
+}
diff --git a/src/main/kotlin/util/mc/PlayerUtil.kt b/src/main/kotlin/util/mc/PlayerUtil.kt
new file mode 100644
index 0000000..53ef1f4
--- /dev/null
+++ b/src/main/kotlin/util/mc/PlayerUtil.kt
@@ -0,0 +1,7 @@
+package moe.nea.firmament.util.mc
+
+import net.minecraft.entity.EquipmentSlot
+import net.minecraft.entity.player.PlayerEntity
+
+
+val PlayerEntity.mainHandStack get() = this.getEquippedStack(EquipmentSlot.MAINHAND)
diff --git a/src/main/kotlin/util/mc/SNbtFormatter.kt b/src/main/kotlin/util/mc/SNbtFormatter.kt
index e773927..7617d17 100644
--- a/src/main/kotlin/util/mc/SNbtFormatter.kt
+++ b/src/main/kotlin/util/mc/SNbtFormatter.kt
@@ -1,5 +1,6 @@
package moe.nea.firmament.util.mc
+import net.minecraft.nbt.AbstractNbtList
import net.minecraft.nbt.NbtByte
import net.minecraft.nbt.NbtByteArray
import net.minecraft.nbt.NbtCompound
@@ -38,7 +39,7 @@ class SNbtFormatter private constructor() : NbtElementVisitor {
override fun visitString(element: NbtString) {
- result.append(NbtString.escape(element.asString()))
+ result.append(NbtString.escape(element.value))
}
override fun visitByte(element: NbtByte) {
@@ -65,18 +66,18 @@ class SNbtFormatter private constructor() : NbtElementVisitor {
result.append(element.doubleValue()).append("d")
}
- private fun visitArrayContents(array: List<NbtElement>) {
+ private fun visitArrayContents(array: AbstractNbtList) {
array.forEachIndexed { index, element ->
writeIndent()
element.accept(this)
- if (array.size != index + 1) {
+ if (array.size() != index + 1) {
result.append(",")
}
result.append("\n")
}
}
- private fun writeArray(arrayTypeTag: String, array: List<NbtElement>) {
+ private fun writeArray(arrayTypeTag: String, array: AbstractNbtList) {
result.append("[").append(arrayTypeTag).append("\n")
pushIndent()
visitArrayContents(array)
@@ -109,7 +110,7 @@ class SNbtFormatter private constructor() : NbtElementVisitor {
keys.forEachIndexed { index, key ->
writeIndent()
val element = compound[key] ?: error("Key '$key' found but not present in compound: $compound")
- val escapedName = if (key.matches(SIMPLE_NAME)) key else NbtString.escape(key)
+ val escapedName = escapeName(key)
result.append(escapedName).append(": ")
element.accept(this)
if (keys.size != index + 1) {
@@ -133,6 +134,9 @@ class SNbtFormatter private constructor() : NbtElementVisitor {
fun NbtElement.toPrettyString() = prettify(this)
- private val SIMPLE_NAME = "[A-Za-z0-9._+-]+".toRegex()
+ fun escapeName(key: String): String =
+ if (key.matches(SIMPLE_NAME)) key else NbtString.escape(key)
+
+ val SIMPLE_NAME = "[A-Za-z0-9._+-]+".toRegex()
}
}
diff --git a/src/main/kotlin/util/mc/SkullItemData.kt b/src/main/kotlin/util/mc/SkullItemData.kt
index 0405b65..3a4c508 100644
--- a/src/main/kotlin/util/mc/SkullItemData.kt
+++ b/src/main/kotlin/util/mc/SkullItemData.kt
@@ -5,12 +5,10 @@ package moe.nea.firmament.util.mc
import com.mojang.authlib.GameProfile
import com.mojang.authlib.minecraft.MinecraftProfileTexture
import com.mojang.authlib.properties.Property
+import java.time.Instant
import java.util.UUID
-import kotlinx.datetime.Clock
-import kotlinx.datetime.Instant
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
-import kotlinx.serialization.encodeToString
import net.minecraft.component.DataComponentTypes
import net.minecraft.component.type.ProfileComponent
import net.minecraft.item.ItemStack
@@ -33,7 +31,7 @@ data class MinecraftTexturesPayloadKt(
val profileId: UUID? = null,
val profileName: String? = null,
val isPublic: Boolean = true,
- val timestamp: Instant = Clock.System.now(),
+ val timestamp: Instant = Instant.now(),
)
fun GameProfile.setTextures(textures: MinecraftTexturesPayloadKt) {
@@ -51,7 +49,7 @@ fun ItemStack.setEncodedSkullOwner(uuid: UUID, encodedData: String) {
this.set(DataComponentTypes.PROFILE, ProfileComponent(gameProfile))
}
-val zeroUUID = UUID.fromString("d3cb85e2-3075-48a1-b213-a9bfb62360c1")
+val arbitraryUUID = UUID.fromString("d3cb85e2-3075-48a1-b213-a9bfb62360c1")
fun createSkullItem(uuid: UUID, url: String) = ItemStack(Items.PLAYER_HEAD)
.also { it.setSkullOwner(uuid, url) }
diff --git a/src/main/kotlin/util/mc/SlotUtils.kt b/src/main/kotlin/util/mc/SlotUtils.kt
index 4709dcf..9eb4918 100644
--- a/src/main/kotlin/util/mc/SlotUtils.kt
+++ b/src/main/kotlin/util/mc/SlotUtils.kt
@@ -1,5 +1,6 @@
package moe.nea.firmament.util.mc
+import org.lwjgl.glfw.GLFW
import net.minecraft.screen.ScreenHandler
import net.minecraft.screen.slot.Slot
import net.minecraft.screen.slot.SlotActionType
@@ -10,7 +11,7 @@ object SlotUtils {
MC.interactionManager?.clickSlot(
handler.syncId,
this.id,
- 2,
+ GLFW.GLFW_MOUSE_BUTTON_MIDDLE,
SlotActionType.CLONE,
MC.player
)
@@ -20,14 +21,25 @@ object SlotUtils {
MC.interactionManager?.clickSlot(
handler.syncId, this.id,
hotbarIndex, SlotActionType.SWAP,
- MC.player)
+ MC.player
+ )
}
fun Slot.clickRightMouseButton(handler: ScreenHandler) {
MC.interactionManager?.clickSlot(
handler.syncId,
this.id,
- 1,
+ GLFW.GLFW_MOUSE_BUTTON_RIGHT,
+ SlotActionType.PICKUP,
+ MC.player
+ )
+ }
+
+ fun Slot.clickLeftMouseButton(handler: ScreenHandler) {
+ MC.interactionManager?.clickSlot(
+ handler.syncId,
+ this.id,
+ GLFW.GLFW_MOUSE_BUTTON_LEFT,
SlotActionType.PICKUP,
MC.player
)
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<T>(
+ delegate: DynamicOps<T>,
+ registryInfoGetter: RegistryInfoGetter
+) : RegistryOps<T>(delegate, registryInfoGetter) {
+ constructor(delegate: DynamicOps<T>, registry: RegistryWrapper.WrapperLookup) :
+ this(delegate, CachedRegistryInfoGetter(registry))
+
+ class TolerantOwner<E> : RegistryEntryOwner<E> {
+ override fun ownerEquals(other: RegistryEntryOwner<E>?): Boolean {
+ return true
+ }
+ }
+
+ override fun <E : Any?> getOwner(registryRef: RegistryKey<out Registry<out E>>?): Optional<RegistryEntryOwner<E>> {
+ return super.getOwner(registryRef).map {
+ TolerantOwner()
+ }
+ }
+}
diff --git a/src/main/kotlin/util/mc/asFakeServer.kt b/src/main/kotlin/util/mc/asFakeServer.kt
new file mode 100644
index 0000000..d3811bd
--- /dev/null
+++ b/src/main/kotlin/util/mc/asFakeServer.kt
@@ -0,0 +1,37 @@
+package moe.nea.firmament.util.mc
+
+import net.fabricmc.fabric.api.client.command.v2.FabricClientCommandSource
+import net.minecraft.server.command.CommandOutput
+import net.minecraft.server.command.ServerCommandSource
+import net.minecraft.text.Text
+
+fun FabricClientCommandSource.asFakeServer(): ServerCommandSource {
+ val source = this
+ return ServerCommandSource(
+ object : CommandOutput {
+ override fun sendMessage(message: Text?) {
+ source.player.sendMessage(message, false)
+ }
+
+ override fun shouldReceiveFeedback(): Boolean {
+ return true
+ }
+
+ override fun shouldTrackOutput(): Boolean {
+ return true
+ }
+
+ override fun shouldBroadcastConsoleToOps(): Boolean {
+ return true
+ }
+ },
+ source.position,
+ source.rotation,
+ null,
+ 0,
+ "FakeServerCommandSource",
+ Text.literal("FakeServerCommandSource"),
+ null,
+ source.player
+ )
+}
diff --git a/src/main/kotlin/util/regex.kt b/src/main/kotlin/util/regex.kt
index a44435c..be6bcfb 100644
--- a/src/main/kotlin/util/regex.kt
+++ b/src/main/kotlin/util/regex.kt
@@ -16,15 +16,23 @@ import kotlin.time.Duration.Companion.seconds
inline fun <T> String.ifMatches(regex: Regex, block: (MatchResult) -> T): T? =
regex.matchEntire(this)?.let(block)
-inline fun <T> Pattern.useMatch(string: String, block: Matcher.() -> T): T? {
+inline fun <T> Pattern.useMatch(string: String?, block: Matcher.() -> T): T? {
contract {
callsInPlace(block, InvocationKind.AT_MOST_ONCE)
}
- return matcher(string)
- .takeIf(Matcher::matches)
+ return string
+ ?.let(this::matcher)
+ ?.takeIf(Matcher::matches)
?.let(block)
}
+fun <T> String.ifDropLast(suffix: String, block: (String) -> T): T? {
+ if (endsWith(suffix)) {
+ return block(dropLast(suffix.length))
+ }
+ return null
+}
+
@Language("RegExp")
val TIME_PATTERN = "[0-9]+[ms]"
diff --git a/src/main/kotlin/util/render/CustomRenderLayers.kt b/src/main/kotlin/util/render/CustomRenderLayers.kt
new file mode 100644
index 0000000..d88a1e4
--- /dev/null
+++ b/src/main/kotlin/util/render/CustomRenderLayers.kt
@@ -0,0 +1,105 @@
+package util.render
+
+import com.mojang.blaze3d.pipeline.BlendFunction
+import com.mojang.blaze3d.pipeline.RenderPipeline
+import com.mojang.blaze3d.platform.DepthTestFunction
+import com.mojang.blaze3d.vertex.VertexFormat.DrawMode
+import java.util.function.Function
+import net.minecraft.client.gl.RenderPipelines
+import net.minecraft.client.gl.UniformType
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.client.render.RenderPhase
+import net.minecraft.client.render.VertexFormats
+import net.minecraft.util.Identifier
+import net.minecraft.util.Util
+import moe.nea.firmament.Firmament
+
+object CustomRenderPipelines {
+ val GUI_TEXTURED_NO_DEPTH_TRIS =
+ RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET)
+ .withVertexFormat(VertexFormats.POSITION_TEXTURE_COLOR, DrawMode.TRIANGLES)
+ .withLocation(Firmament.identifier("gui_textured_overlay_tris"))
+ .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
+ .withCull(false)
+ .withDepthWrite(false)
+ .build()
+ val OMNIPRESENT_LINES = RenderPipeline
+ .builder(RenderPipelines.RENDERTYPE_LINES_SNIPPET)
+ .withLocation(Firmament.identifier("lines"))
+ .withDepthWrite(false)
+ .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
+ .build()
+ val COLORED_OMNIPRESENT_QUADS =
+ RenderPipeline.builder(RenderPipelines.TRANSFORMS_AND_PROJECTION_SNIPPET)// TODO: split this up to support better transparent ordering.
+ .withLocation(Firmament.identifier("colored_omnipresent_quads"))
+ .withVertexShader("core/position_color")
+ .withFragmentShader("core/position_color")
+ .withVertexFormat(VertexFormats.POSITION_COLOR, DrawMode.QUADS)
+ .withDepthTestFunction(DepthTestFunction.NO_DEPTH_TEST)
+ .withCull(false)
+ .withDepthWrite(false)
+ .withBlend(BlendFunction.TRANSLUCENT)
+ .build()
+
+ val CIRCLE_FILTER_TRANSLUCENT_GUI_TRIS =
+ RenderPipeline.builder(RenderPipelines.POSITION_TEX_COLOR_SNIPPET)
+ .withVertexFormat(VertexFormats.POSITION_TEXTURE_COLOR, DrawMode.TRIANGLES)
+ .withLocation(Firmament.identifier("gui_textured_overlay_tris_circle"))
+ .withUniform("CutoutRadius", UniformType.UNIFORM_BUFFER)
+ .withFragmentShader(Firmament.identifier("circle_discard_color"))
+// .withBlend(BlendFunction.TRANSLUCENT)
+ .build()
+ val PARALLAX_CAPE_SHADER =
+ RenderPipeline.builder(RenderPipelines.ENTITY_SNIPPET)
+ .withLocation(Firmament.identifier("parallax_cape"))
+ .withFragmentShader(Firmament.identifier("cape/parallax"))
+ .withSampler("Sampler0")
+ .withSampler("Sampler1")
+ .withSampler("Sampler3")
+ .withUniform("Animation", UniformType.UNIFORM_BUFFER)
+ .build()
+}
+
+object CustomRenderLayers {
+ inline fun memoizeTextured(crossinline func: (Identifier) -> RenderLayer.MultiPhase) = memoize(func)
+ inline fun <T, R> memoize(crossinline func: (T) -> R): Function<T, R> {
+ return Util.memoize { it: T -> func(it) }
+ }
+
+ val GUI_TEXTURED_NO_DEPTH_TRIS = memoizeTextured { texture ->
+ RenderLayer.of(
+ "firmament_gui_textured_overlay_tris",
+ RenderLayer.DEFAULT_BUFFER_SIZE,
+ CustomRenderPipelines.GUI_TEXTURED_NO_DEPTH_TRIS,
+ RenderLayer.MultiPhaseParameters.builder().texture(
+ RenderPhase.Texture(texture, false)
+ )
+ .build(false)
+ )
+ }
+ val LINES = RenderLayer.of(
+ "firmament_lines",
+ RenderLayer.DEFAULT_BUFFER_SIZE,
+ CustomRenderPipelines.OMNIPRESENT_LINES,
+ RenderLayer.MultiPhaseParameters.builder() // TODO: accept linewidth here
+ .build(false)
+ )
+ val COLORED_QUADS = RenderLayer.of(
+ "firmament_quads",
+ RenderLayer.DEFAULT_BUFFER_SIZE,
+ false, true,
+ CustomRenderPipelines.COLORED_OMNIPRESENT_QUADS,
+ RenderLayer.MultiPhaseParameters.builder()
+ .lightmap(RenderPhase.DISABLE_LIGHTMAP)
+ .build(false)
+ )
+
+ val TRANSLUCENT_CIRCLE_GUI =
+ RenderLayer.of(
+ "firmament_circle_gui",
+ RenderLayer.DEFAULT_BUFFER_SIZE,
+ CustomRenderPipelines.CIRCLE_FILTER_TRANSLUCENT_GUI_TRIS,
+ RenderLayer.MultiPhaseParameters.builder()
+ .build(false)
+ )
+}
diff --git a/src/main/kotlin/util/render/DrawContextExt.kt b/src/main/kotlin/util/render/DrawContextExt.kt
index a143d4d..e96fab9 100644
--- a/src/main/kotlin/util/render/DrawContextExt.kt
+++ b/src/main/kotlin/util/render/DrawContextExt.kt
@@ -2,60 +2,30 @@ package moe.nea.firmament.util.render
import com.mojang.blaze3d.systems.RenderSystem
import me.shedaniel.math.Color
-import org.joml.Matrix4f
+import org.joml.Vector3f
+import util.render.CustomRenderLayers
+import kotlin.math.abs
+import net.minecraft.client.gl.RenderPipelines
import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.render.RenderLayer
-import net.minecraft.client.render.RenderLayer.MultiPhaseParameters
-import net.minecraft.client.render.RenderPhase
-import net.minecraft.client.render.VertexFormat
-import net.minecraft.client.render.VertexFormat.DrawMode
-import net.minecraft.client.render.VertexFormats
+import net.minecraft.client.gui.ScreenRect
+import net.minecraft.client.render.VertexConsumerProvider
+import net.minecraft.client.util.math.MatrixStack
import net.minecraft.util.Identifier
-import net.minecraft.util.TriState
-import net.minecraft.util.Util
import moe.nea.firmament.util.MC
fun DrawContext.isUntranslatedGuiDrawContext(): Boolean {
- return (matrices.peek().positionMatrix.properties() and Matrix4f.PROPERTY_IDENTITY.toInt()) != 0
-}
-
-object GuiRenderLayers {
- val GUI_TEXTURED_NO_DEPTH = Util.memoize<Identifier, RenderLayer> { texture: Identifier ->
- RenderLayer.of("firmament_gui_textured_no_depth",
- VertexFormats.POSITION_TEXTURE_COLOR,
- DrawMode.QUADS,
- DEFAULT_BUFFER_SIZE,
- MultiPhaseParameters.builder()
- .texture(RenderPhase.Texture(texture, TriState.FALSE, false))
- .program(RenderPhase.POSITION_TEXTURE_COLOR_PROGRAM)
- .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
- .depthTest(RenderPhase.ALWAYS_DEPTH_TEST)
- .build(false))
- }
- val GUI_TEXTURED_TRIS = Util.memoize { texture: Identifier ->
- RenderLayer.of("firmament_gui_textured_overlay_tris",
- VertexFormats.POSITION_TEXTURE_COLOR,
- DrawMode.TRIANGLES,
- DEFAULT_BUFFER_SIZE,
- MultiPhaseParameters.builder()
- .texture(RenderPhase.Texture(texture, TriState.DEFAULT, false))
- .program(RenderPhase.POSITION_TEXTURE_COLOR_PROGRAM)
- .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
- .depthTest(RenderPhase.ALWAYS_DEPTH_TEST)
- .writeMaskState(RenderPhase.COLOR_MASK)
- .build(false))
- }
+ return matrices.m00 == 1F && matrices.m11 == 1f && matrices.m01 == 0F && matrices.m10 == 0F && matrices.m20 == 0F && matrices.m21 == 0F
}
@Deprecated("Use the other drawGuiTexture")
fun DrawContext.drawGuiTexture(
x: Int, y: Int, z: Int, width: Int, height: Int, sprite: Identifier
-) = this.drawGuiTexture(RenderLayer::getGuiTextured, sprite, x, y, width, height)
+) = this.drawGuiTexture(RenderPipelines.GUI_TEXTURED, sprite, x, y, width, height)
fun DrawContext.drawGuiTexture(
sprite: Identifier,
x: Int, y: Int, width: Int, height: Int
-) = this.drawGuiTexture(RenderLayer::getGuiTextured, sprite, x, y, width, height)
+) = this.drawGuiTexture(RenderPipelines.GUI_TEXTURED, sprite, x, y, width, height)
fun DrawContext.drawTexture(
sprite: Identifier,
@@ -68,34 +38,130 @@ fun DrawContext.drawTexture(
textureWidth: Int,
textureHeight: Int
) {
- this.drawTexture(RenderLayer::getGuiTextured,
- sprite,
- x,
- y,
- u,
- v,
- width,
- height,
- width,
- height,
- textureWidth,
- textureHeight)
+ this.drawTexture(
+ RenderPipelines.GUI_TEXTURED,
+ sprite,
+ x,
+ y,
+ u,
+ v,
+ width,
+ height,
+ width,
+ height,
+ textureWidth,
+ textureHeight
+ )
+}
+
+data class LineRenderState(
+ override val x1: Int,
+ override val x2: Int,
+ override val y1: Int,
+ override val y2: Int,
+ override val scale: Float,
+ override val bounds: ScreenRect,
+ val lineWidth: Float,
+ val w: Int,
+ val h: Int,
+ val color: Int,
+ val direction: LineDirection,
+) : MultiSpecialGuiRenderState() {
+ enum class LineDirection {
+ TOP_LEFT_TO_BOTTOM_RIGHT,
+ BOTTOM_LEFT_TO_TOP_RIGHT,
+ }
+
+ override fun createRenderer(vertexConsumers: VertexConsumerProvider.Immediate): MultiSpecialGuiRenderer<out MultiSpecialGuiRenderState> {
+ return LineRenderer(vertexConsumers)
+ }
+
+ override val scissorArea = null
}
-fun DrawContext.drawLine(fromX: Int, fromY: Int, toX: Int, toY: Int, color: Color) {
- // TODO: push scissors
- // TODO: use matrix translations and a different render layer
+class LineRenderer(vertexConsumers: VertexConsumerProvider.Immediate) :
+ MultiSpecialGuiRenderer<LineRenderState>(vertexConsumers) {
+ override fun getElementClass(): Class<LineRenderState> {
+ return LineRenderState::class.java
+ }
+
+ override fun getYOffset(height: Int, windowScaleFactor: Int): Float {
+ return height / 2F
+ }
+
+ override fun render(
+ state: LineRenderState,
+ matrices: MatrixStack
+ ) {
+ val gr = MC.instance.gameRenderer
+ val client = MC.instance
+ gr.globalSettings
+ .set(
+ state.bounds.width,
+ state.bounds.height,
+ client.options.glintStrength.getValue(),
+ client.world?.time ?: 0L,
+ client.renderTickCounter,
+ client.options.menuBackgroundBlurrinessValue
+ )
+
+ RenderSystem.lineWidth(state.lineWidth)
+ val buf = vertexConsumers.getBuffer(CustomRenderLayers.LINES)
+ val matrix = matrices.peek()
+ val wh = state.w / 2F
+ val hh = state.h / 2F
+ val lowX = -wh
+ val lowY = if (state.direction == LineRenderState.LineDirection.BOTTOM_LEFT_TO_TOP_RIGHT) hh else -hh
+ val highX = wh
+ val highY = -lowY
+ val norm = Vector3f(highX - lowX, highY - lowY, 0F).normalize()
+ buf.vertex(matrix, lowX, lowY, 0F).color(state.color)
+ .normal(matrix, norm)
+ buf.vertex(matrix, highX, highY, 0F).color(state.color)
+ .normal(matrix, norm)
+ vertexConsumers.draw()
+ gr.globalSettings
+ .set(
+ client.window.framebufferWidth,
+ client.window.framebufferHeight,
+ client.options.glintStrength.getValue(),
+ client.world?.getTime() ?: 0L,
+ client.renderTickCounter,
+ client.options.menuBackgroundBlurrinessValue
+ )
+
+ }
+
+ override fun getName(): String? {
+ return "Firmament Line Renderer"
+ }
+}
+
+
+fun DrawContext.drawLine(fromX: Int, fromY: Int, toX: Int, toY: Int, color: Color, lineWidth: Float = 1F) {
if (toY < fromY) {
drawLine(toX, toY, fromX, fromY, color)
return
}
- RenderSystem.lineWidth(MC.window.scaleFactor.toFloat())
- draw { vertexConsumers ->
- val buf = vertexConsumers.getBuffer(RenderInWorldContext.RenderLayers.LINES)
- buf.vertex(fromX.toFloat(), fromY.toFloat(), 0F).color(color.color)
- .normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F)
- buf.vertex(toX.toFloat(), toY.toFloat(), 0F).color(color.color)
- .normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F)
- }
+ val originalRect = ScreenRect(
+ minOf(fromX, toX), minOf(toY, fromY),
+ abs(toX - fromX), abs(toY - fromY)
+ ).transform(matrices)
+ val expansionFactor = 3
+ val rect = ScreenRect(
+ originalRect.left - expansionFactor,
+ originalRect.top - expansionFactor,
+ originalRect.width + expansionFactor * 2,
+ originalRect.height + expansionFactor * 2
+ )
+ // TODO: expand the bounds so that the thickness of the line can be used
+ // TODO: fix this up to work with scissorarea
+ state.addSpecialElement(
+ LineRenderState(
+ rect.left, rect.right, rect.top, rect.bottom, 1F, rect, lineWidth,
+ originalRect.width, originalRect.height, color.color,
+ if (fromX < toX) LineRenderState.LineDirection.TOP_LEFT_TO_BOTTOM_RIGHT else LineRenderState.LineDirection.BOTTOM_LEFT_TO_TOP_RIGHT
+ )
+ )
}
diff --git a/src/main/kotlin/util/render/DumpTexture.kt b/src/main/kotlin/util/render/DumpTexture.kt
new file mode 100644
index 0000000..a7b4e78
--- /dev/null
+++ b/src/main/kotlin/util/render/DumpTexture.kt
@@ -0,0 +1,34 @@
+package moe.nea.firmament.util.render
+
+import com.mojang.blaze3d.buffers.GpuBuffer
+import com.mojang.blaze3d.systems.RenderSystem
+import com.mojang.blaze3d.textures.GpuTexture
+import java.io.File
+import net.minecraft.client.texture.NativeImage
+
+fun dumpTexture(gpuTexture: GpuTexture, name: String) {
+ val w = gpuTexture.getWidth(0)
+ val h = gpuTexture.getHeight(0)
+ val buffer = RenderSystem.getDevice()
+ .createBuffer(
+ { "Dump Buffer" },
+ GpuBuffer.USAGE_COPY_DST or GpuBuffer.USAGE_MAP_READ,
+ w * h * gpuTexture.getFormat().pixelSize()
+ )
+ val commandEncoder = RenderSystem.getDevice().createCommandEncoder()
+ commandEncoder.copyTextureToBuffer(
+ gpuTexture, buffer, 0, {
+ val nativeImage = NativeImage(w, h, false)
+ commandEncoder.mapBuffer(buffer, true, false).use { mappedView ->
+ for (i in 0..<w) {
+ for (j in 0..<h) {
+ val color = mappedView.data().getInt((j + i * w) * gpuTexture.format.pixelSize())
+ nativeImage.setColor(j, h - i - 1, color)
+ }
+ }
+ }
+ buffer.close()
+ nativeImage.writeTo(File("$name.png"))
+ }, 0
+ )
+}
diff --git a/src/main/kotlin/util/render/FacingThePlayerContext.kt b/src/main/kotlin/util/render/FacingThePlayerContext.kt
index daa8da9..e5cb78a 100644
--- a/src/main/kotlin/util/render/FacingThePlayerContext.kt
+++ b/src/main/kotlin/util/render/FacingThePlayerContext.kt
@@ -1,18 +1,12 @@
package moe.nea.firmament.util.render
-import com.mojang.blaze3d.systems.RenderSystem
-import io.github.notenoughupdates.moulconfig.platform.next
import org.joml.Matrix4f
+import util.render.CustomRenderLayers
import net.minecraft.client.font.TextRenderer
-import net.minecraft.client.render.BufferRenderer
-import net.minecraft.client.render.GameRenderer
import net.minecraft.client.render.LightmapTextureManager
import net.minecraft.client.render.RenderLayer
-import net.minecraft.client.render.Tessellator
import net.minecraft.client.render.VertexConsumer
-import net.minecraft.client.render.VertexFormat
-import net.minecraft.client.render.VertexFormats
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
@@ -44,14 +38,14 @@ class FacingThePlayerContext(val worldContext: RenderInWorldContext) {
worldContext.vertexConsumers.getBuffer(RenderLayer.getTextBackgroundSeeThrough())
val matrix4f = worldContext.matrixStack.peek().positionMatrix
vertexConsumer.vertex(matrix4f, -1.0f, -1.0f, 0.0f).color(background)
- .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE).next()
+ .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE)
vertexConsumer.vertex(matrix4f, -1.0f, MC.font.fontHeight.toFloat(), 0.0f).color(background)
- .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE).next()
+ .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE)
vertexConsumer.vertex(matrix4f, width.toFloat(), MC.font.fontHeight.toFloat(), 0.0f)
.color(background)
- .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE).next()
+ .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE)
vertexConsumer.vertex(matrix4f, width.toFloat(), -1.0f, 0.0f).color(background)
- .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE).next()
+ .light(LightmapTextureManager.MAX_BLOCK_LIGHT_COORDINATE)
worldContext.matrixStack.translate(0F, 0F, 0.01F)
MC.font.draw(
@@ -76,22 +70,22 @@ class FacingThePlayerContext(val worldContext: RenderInWorldContext) {
u1: Float, v1: Float,
u2: Float, v2: Float,
) {
- val buf = worldContext.vertexConsumers.getBuffer(RenderLayer.getGuiTexturedOverlay(texture))
+ val buf = worldContext.vertexConsumers.getBuffer(CustomRenderLayers.GUI_TEXTURED_NO_DEPTH_TRIS.apply(texture)) // TODO: this is strictly an incorrect render layer
val hw = width / 2F
val hh = height / 2F
val matrix4f: Matrix4f = worldContext.matrixStack.peek().positionMatrix
buf.vertex(matrix4f, -hw, -hh, 0F)
.color(-1)
- .texture(u1, v1).next()
+ .texture(u1, v1)
buf.vertex(matrix4f, -hw, +hh, 0F)
.color(-1)
- .texture(u1, v2).next()
+ .texture(u1, v2)
buf.vertex(matrix4f, +hw, +hh, 0F)
.color(-1)
- .texture(u2, v2).next()
+ .texture(u2, v2)
buf.vertex(matrix4f, +hw, -hh, 0F)
.color(-1)
- .texture(u2, v1).next()
+ .texture(u2, v1)
worldContext.vertexConsumers.draw()
}
diff --git a/src/main/kotlin/util/render/FirmamentShaders.kt b/src/main/kotlin/util/render/FirmamentShaders.kt
index ba67dbb..53afdf5 100644
--- a/src/main/kotlin/util/render/FirmamentShaders.kt
+++ b/src/main/kotlin/util/render/FirmamentShaders.kt
@@ -1,30 +1,12 @@
package moe.nea.firmament.util.render
-import net.minecraft.client.gl.Defines
-import net.minecraft.client.gl.ShaderProgramKey
-import net.minecraft.client.render.RenderPhase
-import net.minecraft.client.render.VertexFormat
-import net.minecraft.client.render.VertexFormats
-import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.DebugInstantiateEvent
-import moe.nea.firmament.util.MC
object FirmamentShaders {
- val shaders = mutableListOf<ShaderProgramKey>()
-
- private fun shader(name: String, format: VertexFormat, defines: Defines): ShaderProgramKey {
- val key = ShaderProgramKey(Firmament.identifier(name), format, defines)
- shaders.add(key)
- return key
- }
-
- val LINES = RenderPhase.ShaderProgram(shader("core/rendertype_lines", VertexFormats.LINES, Defines.EMPTY))
@Subscribe
fun debugLoad(event: DebugInstantiateEvent) {
- shaders.forEach {
- MC.instance.shaderLoader.getOrCreateProgram(it)
- }
+ // TODO: do i still need to work with shaders like this?
}
}
diff --git a/src/main/kotlin/util/render/LerpUtils.kt b/src/main/kotlin/util/render/LerpUtils.kt
index f2c2f25..e7f226c 100644
--- a/src/main/kotlin/util/render/LerpUtils.kt
+++ b/src/main/kotlin/util/render/LerpUtils.kt
@@ -1,33 +1,40 @@
-
package moe.nea.firmament.util.render
import me.shedaniel.math.Color
+import kotlin.math.absoluteValue
-val pi = Math.PI
-val tau = Math.PI * 2
+val π = Math.PI
+val τ = Math.PI * 2
fun lerpAngle(a: Float, b: Float, progress: Float): Float {
- // TODO: there is at least 10 mods to many in here lol
- val shortestAngle = ((((b.mod(tau) - a.mod(tau)).mod(tau)) + tau + pi).mod(tau)) - pi
- return ((a + (shortestAngle) * progress).mod(tau)).toFloat()
+ // TODO: there is at least 10 mods to many in here lol
+ if (((b - a).absoluteValue - π).absoluteValue < 0.0001) {
+ return lerp(a, b, progress)
+ }
+ val shortestAngle = ((((b.mod(τ) - a.mod(τ)).mod(τ)) + τ + π).mod(τ)) - π
+ return ((a + (shortestAngle) * progress).mod(τ)).toFloat()
}
+fun wrapAngle(angle: Float): Float = (angle.mod(τ) + τ).mod(τ).toFloat()
+fun wrapAngle(angle: Double): Double = (angle.mod(τ) + τ).mod(τ)
+
fun lerp(a: Float, b: Float, progress: Float): Float {
- return a + (b - a) * progress
+ return a + (b - a) * progress
}
+
fun lerp(a: Int, b: Int, progress: Float): Int {
- return (a + (b - a) * progress).toInt()
+ return (a + (b - a) * progress).toInt()
}
fun ilerp(a: Float, b: Float, value: Float): Float {
- return (value - a) / (b - a)
+ return (value - a) / (b - a)
}
fun lerp(a: Color, b: Color, progress: Float): Color {
- return Color.ofRGBA(
- lerp(a.red, b.red, progress),
- lerp(a.green, b.green, progress),
- lerp(a.blue, b.blue, progress),
- lerp(a.alpha, b.alpha, progress),
- )
+ return Color.ofRGBA(
+ lerp(a.red, b.red, progress),
+ lerp(a.green, b.green, progress),
+ lerp(a.blue, b.blue, progress),
+ lerp(a.alpha, b.alpha, progress),
+ )
}
diff --git a/src/main/kotlin/util/render/MultiSpecialGuiRenderState.kt b/src/main/kotlin/util/render/MultiSpecialGuiRenderState.kt
new file mode 100644
index 0000000..d05e71e
--- /dev/null
+++ b/src/main/kotlin/util/render/MultiSpecialGuiRenderState.kt
@@ -0,0 +1,47 @@
+package moe.nea.firmament.util.render
+
+import net.minecraft.client.gui.ScreenRect
+import net.minecraft.client.gui.render.SpecialGuiElementRenderer
+import net.minecraft.client.gui.render.state.GuiRenderState
+import net.minecraft.client.gui.render.state.special.SpecialGuiElementRenderState
+import net.minecraft.client.render.VertexConsumerProvider
+
+abstract class MultiSpecialGuiRenderState : SpecialGuiElementRenderState {
+ // I wish i had manifolds @Self type here... Maybe i should switch to java after all :(
+ abstract fun createRenderer(vertexConsumers: VertexConsumerProvider.Immediate): MultiSpecialGuiRenderer<out MultiSpecialGuiRenderState>
+ abstract val x1: Int
+ abstract val x2: Int
+ abstract val y1: Int
+ abstract val y2: Int
+ abstract val scale: Float
+ abstract val bounds: ScreenRect?
+ abstract val scissorArea: ScreenRect?
+ override fun x1(): Int = x1
+
+ override fun x2(): Int = x2
+
+ override fun y1(): Int = y1
+
+ override fun y2(): Int = y2
+
+ override fun scale(): Float = scale
+
+ override fun scissorArea(): ScreenRect? = scissorArea
+
+ override fun bounds(): ScreenRect? = bounds
+
+}
+
+abstract class MultiSpecialGuiRenderer<T : MultiSpecialGuiRenderState>(
+ vertexConsumers: VertexConsumerProvider.Immediate
+) : SpecialGuiElementRenderer<T>(vertexConsumers) {
+ var wasUsedThisFrame = false
+ fun consumeRender(): Boolean {
+ return wasUsedThisFrame.also { wasUsedThisFrame = false }
+ }
+
+ override fun renderElement(element: T, state: GuiRenderState) {
+ wasUsedThisFrame = true
+ super.renderElement(element, state)
+ }
+}
diff --git a/src/main/kotlin/util/render/RenderCircleProgress.kt b/src/main/kotlin/util/render/RenderCircleProgress.kt
index 805633c..7ea4ca8 100644
--- a/src/main/kotlin/util/render/RenderCircleProgress.kt
+++ b/src/main/kotlin/util/render/RenderCircleProgress.kt
@@ -1,22 +1,150 @@
package moe.nea.firmament.util.render
-import com.mojang.blaze3d.systems.RenderSystem
-import io.github.notenoughupdates.moulconfig.platform.next
-import org.joml.Matrix4f
-import org.joml.Vector2f
-import kotlin.math.atan2
-import kotlin.math.tan
+import com.mojang.blaze3d.vertex.VertexFormat
+import util.render.CustomRenderLayers
import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.render.BufferRenderer
+import net.minecraft.client.gui.ScreenRect
+import net.minecraft.client.render.BufferBuilder
import net.minecraft.client.render.RenderLayer
-import net.minecraft.client.render.RenderPhase
-import net.minecraft.client.render.Tessellator
-import net.minecraft.client.render.VertexFormat.DrawMode
-import net.minecraft.client.render.VertexFormats
+import net.minecraft.client.render.VertexConsumerProvider
+import net.minecraft.client.util.BufferAllocator
+import net.minecraft.client.util.math.MatrixStack
import net.minecraft.util.Identifier
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.collections.nonNegligibleSubSectionsAlignedWith
+import moe.nea.firmament.util.math.Projections
+import moe.nea.firmament.util.mc.CustomRenderPassHelper
object RenderCircleProgress {
+
+ data class State(
+ override val x1: Int,
+ override val x2: Int,
+ override val y1: Int,
+ override val y2: Int,
+ val layer: RenderLayer.MultiPhase,
+ val u1: Float,
+ val u2: Float,
+ val v1: Float,
+ val v2: Float,
+ val angleRadians: ClosedFloatingPointRange<Float>,
+ val color: Int,
+ val innerCutoutRadius: Float,
+ override val scale: Float,
+ override val bounds: ScreenRect?,
+ override val scissorArea: ScreenRect?,
+ ) : MultiSpecialGuiRenderState() {
+ override fun createRenderer(vertexConsumers: VertexConsumerProvider.Immediate): MultiSpecialGuiRenderer<out MultiSpecialGuiRenderState> {
+ return Renderer(vertexConsumers)
+ }
+ }
+
+ class Renderer(vertexConsumers: VertexConsumerProvider.Immediate) :
+ MultiSpecialGuiRenderer<State>(vertexConsumers) {
+ override fun render(
+ state: State,
+ matrices: MatrixStack
+ ) {
+ matrices.push()
+ matrices.translate(0F, -1F, 0F)
+ val sections = state.angleRadians.nonNegligibleSubSectionsAlignedWith((τ / 8f).toFloat())
+ .zipWithNext().toList()
+ val u1 = state.u1
+ val u2 = state.u2
+ val v1 = state.v1
+ val v2 = state.v2
+ val color = state.color
+ val matrix = matrices.peek().positionMatrix
+ BufferAllocator(state.layer.vertexFormat.vertexSize * sections.size * 3).use { allocator ->
+
+ val bufferBuilder = BufferBuilder(allocator, VertexFormat.DrawMode.TRIANGLES, state.layer.vertexFormat)
+
+ for ((sectionStart, sectionEnd) in sections) {
+ val firstPoint = Projections.Two.projectAngleOntoUnitBox(sectionStart.toDouble())
+ val secondPoint = Projections.Two.projectAngleOntoUnitBox(sectionEnd.toDouble())
+ fun ilerp(f: Float): Float =
+ ilerp(-1f, 1f, f)
+
+ bufferBuilder
+ .vertex(matrix, secondPoint.x, secondPoint.y, 0F)
+ .texture(lerp(u1, u2, ilerp(secondPoint.x)), lerp(v1, v2, ilerp(secondPoint.y)))
+ .color(color)
+
+ bufferBuilder
+ .vertex(matrix, firstPoint.x, firstPoint.y, 0F)
+ .texture(lerp(u1, u2, ilerp(firstPoint.x)), lerp(v1, v2, ilerp(firstPoint.y)))
+ .color(color)
+
+ bufferBuilder
+ .vertex(matrix, 0F, 0F, 0F)
+ .texture(lerp(u1, u2, ilerp(0F)), lerp(v1, v2, ilerp(0F)))
+ .color(color)
+
+ }
+
+ bufferBuilder.end().use { buffer ->
+ if (state.innerCutoutRadius <= 0) {
+ state.layer.draw(buffer)
+ return
+ }
+ CustomRenderPassHelper(
+ { "RenderCircleProgress" },
+ VertexFormat.DrawMode.TRIANGLES,
+ state.layer.vertexFormat,
+ MC.instance.framebuffer,
+ false,
+ ).use { renderPass ->
+ renderPass.uploadVertices(buffer)
+ renderPass.setAllDefaultUniforms()
+ renderPass.setPipeline(state.layer.pipeline)
+ renderPass.setUniform("CutoutRadius", 4) {
+ it.putFloat(state.innerCutoutRadius)
+ }
+ renderPass.draw()
+ }
+ }
+ }
+ matrices.pop()
+ }
+
+ override fun getElementClass(): Class<State> {
+ return State::class.java
+ }
+
+ override fun getName(): String {
+ return "Firmament Circle"
+ }
+ }
+
+ fun renderCircularSlice(
+ drawContext: DrawContext,
+ layer: RenderLayer.MultiPhase,
+ u1: Float,
+ u2: Float,
+ v1: Float,
+ v2: Float,
+ angleRadians: ClosedFloatingPointRange<Float>,
+ color: Int = -1,
+ innerCutoutRadius: Float = 0F
+ ) {
+ val screenRect = ScreenRect(-1, -1, 2, 2).transform(drawContext.matrices)
+ drawContext.state.addSpecialElement(
+ State(
+ screenRect.left, screenRect.right,
+ screenRect.top, screenRect.bottom,
+ layer,
+ u1, u2, v1, v2,
+ angleRadians,
+ color,
+ innerCutoutRadius,
+ screenRect.width / 2F,
+ screenRect,
+ null
+ )
+ )
+ }
+
fun renderCircle(
drawContext: DrawContext,
texture: Identifier,
@@ -26,68 +154,11 @@ object RenderCircleProgress {
v1: Float,
v2: Float,
) {
- RenderSystem.enableBlend()
- drawContext.draw {
- val bufferBuilder = it.getBuffer(GuiRenderLayers.GUI_TEXTURED_TRIS.apply(texture))
- val matrix: Matrix4f = drawContext.matrices.peek().positionMatrix
-
- val corners = listOf(
- Vector2f(0F, -1F),
- Vector2f(1F, -1F),
- Vector2f(1F, 0F),
- Vector2f(1F, 1F),
- Vector2f(0F, 1F),
- Vector2f(-1F, 1F),
- Vector2f(-1F, 0F),
- Vector2f(-1F, -1F),
- )
-
- for (i in (0 until 8)) {
- if (progress < i / 8F) {
- break
- }
- val second = corners[(i + 1) % 8]
- val first = corners[i]
- if (progress <= (i + 1) / 8F) {
- val internalProgress = 1 - (progress - i / 8F) * 8F
- val angle = lerpAngle(
- atan2(second.y, second.x),
- atan2(first.y, first.x),
- internalProgress
- )
- if (angle < tau / 8 || angle >= tau * 7 / 8) {
- second.set(1F, tan(angle))
- } else if (angle < tau * 3 / 8) {
- second.set(1 / tan(angle), 1F)
- } else if (angle < tau * 5 / 8) {
- second.set(-1F, -tan(angle))
- } else {
- second.set(-1 / tan(angle), -1F)
- }
- }
-
- fun ilerp(f: Float): Float =
- ilerp(-1f, 1f, f)
-
- bufferBuilder
- .vertex(matrix, second.x, second.y, 0F)
- .texture(lerp(u1, u2, ilerp(second.x)), lerp(v1, v2, ilerp(second.y)))
- .color(-1)
- .next()
- bufferBuilder
- .vertex(matrix, first.x, first.y, 0F)
- .texture(lerp(u1, u2, ilerp(first.x)), lerp(v1, v2, ilerp(first.y)))
- .color(-1)
- .next()
- bufferBuilder
- .vertex(matrix, 0F, 0F, 0F)
- .texture(lerp(u1, u2, ilerp(0F)), lerp(v1, v2, ilerp(0F)))
- .color(-1)
- .next()
- }
- }
- RenderSystem.disableBlend()
+ renderCircularSlice(
+ drawContext,
+ CustomRenderLayers.GUI_TEXTURED_NO_DEPTH_TRIS.apply(texture),
+ u1, u2, v1, v2,
+ (-τ / 4).toFloat()..(progress * τ - τ / 4).toFloat()
+ )
}
-
-
}
diff --git a/src/main/kotlin/util/render/RenderInWorldContext.kt b/src/main/kotlin/util/render/RenderInWorldContext.kt
index bb58200..12a061d 100644
--- a/src/main/kotlin/util/render/RenderInWorldContext.kt
+++ b/src/main/kotlin/util/render/RenderInWorldContext.kt
@@ -1,20 +1,15 @@
package moe.nea.firmament.util.render
import com.mojang.blaze3d.systems.RenderSystem
-import io.github.notenoughupdates.moulconfig.platform.next
-import java.lang.Math.pow
import org.joml.Matrix4f
import org.joml.Vector3f
-import net.minecraft.client.gl.VertexBuffer
+import util.render.CustomRenderLayers
+import kotlin.math.pow
import net.minecraft.client.render.Camera
import net.minecraft.client.render.RenderLayer
-import net.minecraft.client.render.RenderPhase
import net.minecraft.client.render.RenderTickCounter
-import net.minecraft.client.render.Tessellator
import net.minecraft.client.render.VertexConsumer
import net.minecraft.client.render.VertexConsumerProvider
-import net.minecraft.client.render.VertexFormat
-import net.minecraft.client.render.VertexFormats
import net.minecraft.client.texture.Sprite
import net.minecraft.client.util.math.MatrixStack
import net.minecraft.text.Text
@@ -27,65 +22,50 @@ import moe.nea.firmament.util.MC
@RenderContextDSL
class RenderInWorldContext private constructor(
- private val tesselator: Tessellator,
val matrixStack: MatrixStack,
private val camera: Camera,
private val tickCounter: RenderTickCounter,
val vertexConsumers: VertexConsumerProvider.Immediate,
) {
-
- object RenderLayers {
- val TRANSLUCENT_TRIS = RenderLayer.of("firmament_translucent_tris",
- VertexFormats.POSITION_COLOR,
- VertexFormat.DrawMode.TRIANGLES,
- RenderLayer.CUTOUT_BUFFER_SIZE,
- false, true,
- RenderLayer.MultiPhaseParameters.builder()
- .depthTest(RenderPhase.ALWAYS_DEPTH_TEST)
- .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
- .program(RenderPhase.POSITION_COLOR_PROGRAM)
- .build(false))
- val LINES = RenderLayer.of("firmament_rendertype_lines",
- VertexFormats.LINES,
- VertexFormat.DrawMode.LINES,
- RenderLayer.CUTOUT_BUFFER_SIZE,
- false, false, // do we need translucent? i dont think so
- RenderLayer.MultiPhaseParameters.builder()
- .depthTest(RenderPhase.ALWAYS_DEPTH_TEST)
- .program(FirmamentShaders.LINES)
- .build(false)
- )
- val COLORED_QUADS = RenderLayer.of(
- "firmament_quads",
- VertexFormats.POSITION_COLOR,
- VertexFormat.DrawMode.QUADS,
- RenderLayer.CUTOUT_BUFFER_SIZE,
- false, true,
- RenderLayer.MultiPhaseParameters.builder()
- .depthTest(RenderPhase.ALWAYS_DEPTH_TEST)
- .program(RenderPhase.POSITION_COLOR_PROGRAM)
- .transparency(RenderPhase.TRANSLUCENT_TRANSPARENCY)
- .build(false)
- )
- }
-
- @Deprecated("stateful color management is no longer a thing")
- fun color(color: me.shedaniel.math.Color) {
- color(color.red / 255F, color.green / 255f, color.blue / 255f, color.alpha / 255f)
- }
-
- @Deprecated("stateful color management is no longer a thing")
- fun color(red: Float, green: Float, blue: Float, alpha: Float) {
- RenderSystem.setShaderColor(red, green, blue, alpha)
- }
-
fun block(blockPos: BlockPos, color: Int) {
matrixStack.push()
matrixStack.translate(blockPos.x.toFloat(), blockPos.y.toFloat(), blockPos.z.toFloat())
- buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(RenderLayers.COLORED_QUADS), color)
+ buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS), color)
matrixStack.pop()
}
+ fun sharedVoxelSurface(blocks: Set<BlockPos>, color: Int) {
+ val m = BlockPos.Mutable()
+ val l = vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS)
+ blocks.forEach {
+ matrixStack.push()
+ matrixStack.translate(it.x.toFloat(), it.y.toFloat(), it.z.toFloat())
+ val p = matrixStack.peek().positionMatrix
+ m.set(it)
+ if (m.setX(it.x + 1) !in blocks) {
+ buildFaceXP(p, l, color)
+ }
+ if (m.setX(it.x - 1) !in blocks) {
+ buildFaceXN(p, l, color)
+ }
+ m.set(it)
+ if (m.setY(it.y + 1) !in blocks) {
+ buildFaceYP(p, l, color)
+ }
+ if (m.setY(it.y - 1) !in blocks) {
+ buildFaceYN(p, l, color)
+ }
+ m.set(it)
+ if (m.setZ(it.z + 1) !in blocks) {
+ buildFaceZP(p, l, color)
+ }
+ if (m.setZ(it.z - 1) !in blocks) {
+ buildFaceZN(p, l, color)
+ }
+ matrixStack.pop()
+ }
+ }
+
enum class VerticalAlign {
TOP, BOTTOM, CENTER;
@@ -155,7 +135,7 @@ class RenderInWorldContext private constructor(
matrixStack.translate(vec3d.x, vec3d.y, vec3d.z)
matrixStack.scale(size, size, size)
matrixStack.translate(-.5, -.5, -.5)
- buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(RenderLayers.COLORED_QUADS), color)
+ buildCube(matrixStack.peek().positionMatrix, vertexConsumers.getBuffer(CustomRenderLayers.COLORED_QUADS), color)
matrixStack.pop()
vertexConsumers.draw()
}
@@ -163,27 +143,35 @@ class RenderInWorldContext private constructor(
fun wireframeCube(blockPos: BlockPos, lineWidth: Float = 10F) {
val buf = vertexConsumers.getBuffer(RenderLayer.LINES)
matrixStack.push()
+ // TODO: add color arg to this
// TODO: this does not render through blocks (or water layers) anymore
- RenderSystem.lineWidth(lineWidth / pow(camera.pos.squaredDistanceTo(blockPos.toCenterPos()), 0.25).toFloat())
- matrixStack.translate(blockPos.x.toFloat(), blockPos.y.toFloat(), blockPos.z.toFloat())
+ RenderSystem.lineWidth(lineWidth / camera.pos.squaredDistanceTo(blockPos.toCenterPos()).pow(0.25).toFloat())
+ val offset = 1 / 512F
+ matrixStack.translate(
+ blockPos.x.toFloat() - offset,
+ blockPos.y.toFloat() - offset,
+ blockPos.z.toFloat() - offset
+ )
+ val scale = 1 + 2 * offset
+ matrixStack.scale(scale, scale, scale)
+
buildWireFrameCube(matrixStack.peek(), buf)
matrixStack.pop()
vertexConsumers.draw()
}
- fun line(vararg points: Vec3d, lineWidth: Float = 10F) {
- line(points.toList(), lineWidth)
+ fun line(vararg points: Vec3d, color: Int, lineWidth: Float = 10F) {
+ line(points.toList(), color, lineWidth)
}
- fun tracer(toWhere: Vec3d, lineWidth: Float = 3f) {
+ fun tracer(toWhere: Vec3d, color: Int, lineWidth: Float = 3f) {
val cameraForward = Vector3f(0f, 0f, -1f).rotate(camera.rotation)
- line(camera.pos.add(Vec3d(cameraForward)), toWhere, lineWidth = lineWidth)
+ line(camera.pos.add(Vec3d(cameraForward)), toWhere, color = color, lineWidth = lineWidth)
}
- fun line(points: List<Vec3d>, lineWidth: Float = 10F) {
+ fun line(points: List<Vec3d>, color: Int, lineWidth: Float = 10F) {
RenderSystem.lineWidth(lineWidth)
- // TODO: replace with renderlayers
- val buffer = tesselator.begin(VertexFormat.DrawMode.LINES, VertexFormats.LINES)
+ val buffer = vertexConsumers.getBuffer(CustomRenderLayers.LINES)
val matrix = matrixStack.peek()
var lastNormal: Vector3f? = null
@@ -194,16 +182,15 @@ class RenderInWorldContext private constructor(
val lastNormal0 = lastNormal ?: normal
lastNormal = normal
buffer.vertex(matrix.positionMatrix, a.x.toFloat(), a.y.toFloat(), a.z.toFloat())
- .color(-1)
+ .color(color)
.normal(matrix, lastNormal0.x, lastNormal0.y, lastNormal0.z)
- .next()
+
buffer.vertex(matrix.positionMatrix, b.x.toFloat(), b.y.toFloat(), b.z.toFloat())
- .color(-1)
+ .color(color)
.normal(matrix, normal.x, normal.y, normal.z)
- .next()
+
}
- RenderLayers.LINES.draw(buffer.end())
}
// TODO: put the favourite icons in front of items again
@@ -224,11 +211,11 @@ class RenderInWorldContext private constructor(
buf.vertex(matrix.positionMatrix, i, j, k)
.normal(matrix, normal.x, normal.y, normal.z)
.color(-1)
- .next()
+
buf.vertex(matrix.positionMatrix, x, y, z)
.normal(matrix, normal.x, normal.y, normal.z)
.color(-1)
- .next()
+
}
@@ -244,53 +231,63 @@ class RenderInWorldContext private constructor(
}
}
- private fun buildCube(matrix: Matrix4f, buf: VertexConsumer, color: Int) {
- // Y-
- buf.vertex(matrix, 0F, 0F, 0F).color(color)
- buf.vertex(matrix, 0F, 0F, 1F).color(color)
- buf.vertex(matrix, 1F, 0F, 1F).color(color)
- buf.vertex(matrix, 1F, 0F, 0F).color(color)
- // Y+
- buf.vertex(matrix, 0F, 1F, 0F).color(color)
- buf.vertex(matrix, 1F, 1F, 0F).color(color)
- buf.vertex(matrix, 1F, 1F, 1F).color(color)
- buf.vertex(matrix, 0F, 1F, 1F).color(color)
- // X-
- buf.vertex(matrix, 0F, 0F, 0F).color(color)
- buf.vertex(matrix, 0F, 0F, 1F).color(color)
- buf.vertex(matrix, 0F, 1F, 1F).color(color)
- buf.vertex(matrix, 0F, 1F, 0F).color(color)
- // X+
- buf.vertex(matrix, 1F, 0F, 0F).color(color)
- buf.vertex(matrix, 1F, 1F, 0F).color(color)
- buf.vertex(matrix, 1F, 1F, 1F).color(color)
- buf.vertex(matrix, 1F, 0F, 1F).color(color)
- // Z-
- buf.vertex(matrix, 0F, 0F, 0F).color(color)
- buf.vertex(matrix, 1F, 0F, 0F).color(color)
- buf.vertex(matrix, 1F, 1F, 0F).color(color)
- buf.vertex(matrix, 0F, 1F, 0F).color(color)
- // Z+
- buf.vertex(matrix, 0F, 0F, 1F).color(color)
- buf.vertex(matrix, 0F, 1F, 1F).color(color)
- buf.vertex(matrix, 1F, 1F, 1F).color(color)
- buf.vertex(matrix, 1F, 0F, 1F).color(color)
+ private fun buildFaceZP(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) {
+ buf.vertex(matrix, 0F, 0F, 1F).color(rgba)
+ buf.vertex(matrix, 0F, 1F, 1F).color(rgba)
+ buf.vertex(matrix, 1F, 1F, 1F).color(rgba)
+ buf.vertex(matrix, 1F, 0F, 1F).color(rgba)
+ }
+
+ private fun buildFaceZN(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) {
+ buf.vertex(matrix, 0F, 0F, 0F).color(rgba)
+ buf.vertex(matrix, 1F, 0F, 0F).color(rgba)
+ buf.vertex(matrix, 1F, 1F, 0F).color(rgba)
+ buf.vertex(matrix, 0F, 1F, 0F).color(rgba)
}
+ private fun buildFaceXP(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) {
+ buf.vertex(matrix, 1F, 0F, 0F).color(rgba)
+ buf.vertex(matrix, 1F, 1F, 0F).color(rgba)
+ buf.vertex(matrix, 1F, 1F, 1F).color(rgba)
+ buf.vertex(matrix, 1F, 0F, 1F).color(rgba)
+ }
+
+ private fun buildFaceXN(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) {
+ buf.vertex(matrix, 0F, 0F, 0F).color(rgba)
+ buf.vertex(matrix, 0F, 0F, 1F).color(rgba)
+ buf.vertex(matrix, 0F, 1F, 1F).color(rgba)
+ buf.vertex(matrix, 0F, 1F, 0F).color(rgba)
+ }
+
+ private fun buildFaceYN(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) {
+ buf.vertex(matrix, 0F, 0F, 0F).color(rgba)
+ buf.vertex(matrix, 0F, 0F, 1F).color(rgba)
+ buf.vertex(matrix, 1F, 0F, 1F).color(rgba)
+ buf.vertex(matrix, 1F, 0F, 0F).color(rgba)
+ }
+
+ private fun buildFaceYP(matrix: Matrix4f, buf: VertexConsumer, rgba: Int) {
+ buf.vertex(matrix, 0F, 1F, 0F).color(rgba)
+ buf.vertex(matrix, 1F, 1F, 0F).color(rgba)
+ buf.vertex(matrix, 1F, 1F, 1F).color(rgba)
+ buf.vertex(matrix, 0F, 1F, 1F).color(rgba)
+ }
+
+ private fun buildCube(matrix4f: Matrix4f, buf: VertexConsumer, rgba: Int) {
+ buildFaceXP(matrix4f, buf, rgba)
+ buildFaceXN(matrix4f, buf, rgba)
+ buildFaceYP(matrix4f, buf, rgba)
+ buildFaceYN(matrix4f, buf, rgba)
+ buildFaceZP(matrix4f, buf, rgba)
+ buildFaceZN(matrix4f, buf, rgba)
+ }
fun renderInWorld(event: WorldRenderLastEvent, block: RenderInWorldContext. () -> Unit) {
- // TODO: there should be *no more global state*. the only thing we should be doing is render layers. that includes settings like culling, blending, shader color, and depth testing
- // For now i will let these functions remain, but this needs to go before i do a full (non-beta) release
- RenderSystem.disableDepthTest()
- RenderSystem.enableBlend()
- RenderSystem.defaultBlendFunc()
- RenderSystem.disableCull()
event.matrices.push()
event.matrices.translate(-event.camera.pos.x, -event.camera.pos.y, -event.camera.pos.z)
val ctx = RenderInWorldContext(
- RenderSystem.renderThreadTesselator(),
event.matrices,
event.camera,
event.tickCounter,
@@ -301,11 +298,6 @@ class RenderInWorldContext private constructor(
event.matrices.pop()
event.vertexConsumers.draw()
- RenderSystem.setShaderColor(1F, 1F, 1F, 1F)
- VertexBuffer.unbind()
- RenderSystem.enableDepthTest()
- RenderSystem.enableCull()
- RenderSystem.disableBlend()
}
}
}
diff --git a/src/main/kotlin/util/render/TintedOverlayTexture.kt b/src/main/kotlin/util/render/TintedOverlayTexture.kt
new file mode 100644
index 0000000..0677846
--- /dev/null
+++ b/src/main/kotlin/util/render/TintedOverlayTexture.kt
@@ -0,0 +1,35 @@
+package moe.nea.firmament.util.render
+
+import me.shedaniel.math.Color
+import net.minecraft.client.render.OverlayTexture
+import net.minecraft.util.math.ColorHelper
+import moe.nea.firmament.util.ErrorUtil
+
+class TintedOverlayTexture : OverlayTexture() {
+ companion object {
+ val size = 16
+ }
+
+ private var lastColor: Color? = null
+ fun setColor(color: Color): TintedOverlayTexture {
+ val image = ErrorUtil.notNullOr(texture.image, "Disposed TintedOverlayTexture written to") { return this }
+ if (color == lastColor) return this
+ lastColor = color
+
+ for (i in 0..<size) {
+ for (j in 0..<size) {
+ if (i < 8) {
+ image.setColorArgb(j, i, 0xB2FF0000.toInt())
+ } else {
+ val k = ((1F - j / 15F * 0.75F) * 255F).toInt()
+ image.setColorArgb(j, i, ColorHelper.withAlpha(k, color.color))
+ }
+ }
+ }
+
+ texture.setFilter(false, false)
+ texture.setClamp(true)
+ texture.upload()
+ return this
+ }
+}
diff --git a/src/main/kotlin/util/render/TranslatedScissors.kt b/src/main/kotlin/util/render/TranslatedScissors.kt
index c1e6544..ee90a2d 100644
--- a/src/main/kotlin/util/render/TranslatedScissors.kt
+++ b/src/main/kotlin/util/render/TranslatedScissors.kt
@@ -1,22 +1,26 @@
-
package moe.nea.firmament.util.render
-import org.joml.Vector4f
+import org.joml.Matrix3x2f
+import org.joml.Vector3f
import net.minecraft.client.gui.DrawContext
fun DrawContext.enableScissorWithTranslation(x1: Float, y1: Float, x2: Float, y2: Float) {
- val pMat = matrices.peek().positionMatrix
- val target = Vector4f()
+ enableScissor(x1.toInt(), y1.toInt(), x2.toInt(), y2.toInt())
+}
+
+fun DrawContext.enableScissorWithoutTranslation(x1: Float, y1: Float, x2: Float, y2: Float) {
+ val pMat = Matrix3x2f(matrices).invert()
+ var target = Vector3f()
- target.set(x1, y1, 0f, 1f)
- target.mul(pMat)
- val scissorX1 = target.x
- val scissorY1 = target.y
+ target.set(x1, y1, 1F)
+ target.mul(pMat)
+ val scissorX1 = target.x
+ val scissorY1 = target.y
- target.set(x2, y2, 0f, 1f)
- target.mul(pMat)
- val scissorX2 = target.x
- val scissorY2 = target.y
+ target.set(x2, y2, 1F)
+ target.mul(pMat)
+ val scissorX2 = target.x
+ val scissorY2 = target.y
- enableScissor(scissorX1.toInt(), scissorY1.toInt(), scissorX2.toInt(), scissorY2.toInt())
+ enableScissor(scissorX1.toInt(), scissorY1.toInt(), scissorX2.toInt(), scissorY2.toInt())
}
diff --git a/src/main/kotlin/util/skyblock/DungeonUtil.kt b/src/main/kotlin/util/skyblock/DungeonUtil.kt
new file mode 100644
index 0000000..488b158
--- /dev/null
+++ b/src/main/kotlin/util/skyblock/DungeonUtil.kt
@@ -0,0 +1,33 @@
+package moe.nea.firmament.util.skyblock
+
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.ScoreboardUtil
+import moe.nea.firmament.util.SkyBlockIsland
+import moe.nea.firmament.util.TIME_PATTERN
+
+object DungeonUtil {
+ val isInDungeonIsland get() = SBData.skyblockLocation == SkyBlockIsland.DUNGEON
+ private val timeElapsedRegex = "Time Elapsed: $TIME_PATTERN".toRegex()
+ val isInActiveDungeon get() = isInDungeonIsland && ScoreboardUtil.simplifiedScoreboardLines.any { it.matches(
+ timeElapsedRegex) }
+
+/*Title:
+
+§f§lSKYBLOCK§B§L CO-OP
+
+' Late Spring 7th'
+' §75:20am'
+' §7⏣ §cThe Catacombs §7(M3)'
+' §7♲ §7Ironman'
+' '
+'Keys: §c■ §c✗ §8■ §a1x'
+'Time Elapsed: §a46s'
+'Cleared: §660% §8(105)'
+' '
+'§e[B] §b151_Dragon §e2,062§c❤'
+'§e[A] §6Lennart0312 §a17,165§c'
+'§e[T] §b187i §a14,581§c❤'
+'§e[H] §bFlameeke §a8,998§c❤'
+' '
+'§ewww.hypixel.net'*/
+}
diff --git a/src/main/kotlin/util/skyblock/ItemType.kt b/src/main/kotlin/util/skyblock/ItemType.kt
index b031b69..9045646 100644
--- a/src/main/kotlin/util/skyblock/ItemType.kt
+++ b/src/main/kotlin/util/skyblock/ItemType.kt
@@ -6,13 +6,19 @@ import moe.nea.firmament.util.mc.loreAccordingToNbt
import moe.nea.firmament.util.petData
-@JvmInline
-value class ItemType private constructor(val name: String) {
+data class ItemType private constructor(val name: String) {
companion object {
fun ofName(name: String): ItemType {
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,16 +32,46 @@ 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 AXE = ofName("AXE")
+ 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 SHOVEL = ofName("SHOVEL")
+
+ val NIL = ofName("__NIL")
/**
* This one is not really official (it never shows up in game).
*/
val PET = ofName("PET")
}
+
+ val dungeonVariant get() = ofName("DUNGEON $name")
+
+ val isDungeon get() = name.startsWith("DUNGEON ")
+
+ override fun toString(): String {
+ return name
+ }
}
diff --git a/src/main/kotlin/util/skyblock/PartyUtil.kt b/src/main/kotlin/util/skyblock/PartyUtil.kt
new file mode 100644
index 0000000..7d28868
--- /dev/null
+++ b/src/main/kotlin/util/skyblock/PartyUtil.kt
@@ -0,0 +1,210 @@
+package moe.nea.firmament.util.skyblock
+
+import java.util.UUID
+import net.hypixel.modapi.HypixelModAPI
+import net.hypixel.modapi.packet.impl.clientbound.ClientboundPartyInfoPacket
+import net.hypixel.modapi.packet.impl.clientbound.ClientboundPartyInfoPacket.PartyRole
+import net.hypixel.modapi.packet.impl.serverbound.ServerboundPartyInfoPacket
+import org.intellij.lang.annotations.Language
+import kotlinx.coroutines.launch
+import net.minecraft.text.Text
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.apis.Routes
+import moe.nea.firmament.commands.thenExecute
+import moe.nea.firmament.commands.thenLiteral
+import moe.nea.firmament.events.CommandEvent
+import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.events.WorldReadyEvent
+import moe.nea.firmament.features.debug.DeveloperFeatures
+import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.bold
+import moe.nea.firmament.util.boolColour
+import moe.nea.firmament.util.grey
+import moe.nea.firmament.util.tr
+import moe.nea.firmament.util.useMatch
+
+object PartyUtil {
+ object Internal {
+ val hma = HypixelModAPI.getInstance()
+
+ val handler = hma.createHandler(ClientboundPartyInfoPacket::class.java) { clientboundPartyInfoPacket ->
+ Firmament.coroutineScope.launch {
+ party = Party(clientboundPartyInfoPacket.memberMap.values.map {
+ PartyMember.fromUuid(it.uuid, it.role)
+ })
+ }
+ }
+
+ fun sendSyncPacket() {
+ hma.sendPacket(ServerboundPartyInfoPacket())
+ }
+
+ @Subscribe
+ fun onDevCommand(event: CommandEvent.SubCommand) {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
+ thenLiteral("party") {
+ thenLiteral("refresh") {
+ thenExecute {
+ sendSyncPacket()
+ source.sendFeedback(tr("firmament.dev.partyinfo.refresh", "Refreshing party info"))
+ }
+ }
+ thenExecute {
+ val p = party
+ val text = Text.empty()
+ text.append(
+ tr("firmament.dev.partyinfo", "Party Info: ")
+ .boolColour(p != null)
+ )
+ if (p == null) {
+ text.append(tr("firmament.dev.partyinfo.empty", "Empty Party").grey())
+ } else {
+ text.append(tr("firmament.dev.partyinfo.count", "${p.members.size} members").grey())
+ p.members.forEach {
+ text.append("\n")
+ .append(Text.literal(" - ${it.name}"))
+ .append(" (")
+ .append(
+ when (it.role) {
+ PartyRole.LEADER -> tr("firmament.dev.partyinfo.leader", "Leader").bold()
+ PartyRole.MOD -> tr("firmament.dev.partyinfo.mod", "Moderator")
+ PartyRole.MEMBER -> tr("firmament.dev.partyinfo.member", "Member")
+ }
+ )
+ .append(")")
+ }
+ }
+ source.sendFeedback(text)
+ }
+ }
+ }
+ }
+
+ object Regexes {
+ @Language("RegExp")
+ val NAME = "(\\[[^\\]]+\\] )?(?<name>[a-zA-Z0-9_]{2,16})"
+ val NAME_SECONDARY = NAME.replace("name", "name2")
+ val joinSelf = "You have joined $NAME's? party!".toPattern()
+ val joinOther = "$NAME joined the party\\.".toPattern()
+ val leaveSelf = "You left the party\\.".toPattern()
+ val disbandedEmpty =
+ "The party was disbanded because all invites expired and the party was empty\\.".toPattern()
+ val leaveOther = "$NAME has left the party\\.".toPattern()
+ val kickedOther = "$NAME has been removed from the party\\.".toPattern()
+ val kickedOtherOffline = "Kicked $NAME because they were offline\\.".toPattern()
+ val disconnectedOther = "$NAME was removed from your party because they disconnected\\.".toPattern()
+ val transferLeave = "The party was transferred to $NAME because $NAME_SECONDARY left\\.?".toPattern()
+ val transferVoluntary = "The party was transferred to $NAME by $NAME_SECONDARY\\.?".toPattern()
+ val disbanded = "$NAME has disbanded the party!".toPattern()
+ val kickedSelf = "You have been kicked from the party by $NAME ?\\.?".toPattern()
+ val partyFinderJoin = "Party Finder > $NAME joined the .* group!.*".toPattern()
+ }
+
+ fun modifyParty(
+ allowEmpty: Boolean = false,
+ modifier: (MutableList<PartyMember>) -> Unit
+ ) {
+ val oldList = party?.members ?: emptyList()
+ if (oldList.isEmpty() && !allowEmpty) return
+ party = Party(oldList.toMutableList().also(modifier))
+ }
+
+ fun MutableList<PartyMember>.modifyMember(name: String, mod: (PartyMember) -> PartyMember) {
+ val idx = indexOfFirst { it.name == name }
+ val member = if (idx < 0) {
+ PartyMember(name, PartyRole.MEMBER)
+ } else {
+ removeAt(idx)
+ }
+ add(mod(member))
+ }
+
+ fun addMemberToParty(name: String) {
+ modifyParty(true) {
+ if (it.isEmpty())
+ it.add(PartyMember(MC.playerName, PartyRole.LEADER))
+ it.add(PartyMember(name, PartyRole.MEMBER))
+ }
+ }
+
+ @Subscribe
+ fun onJoinServer(event: WorldReadyEvent) { // This event isn't perfect... Hypixel isn't ready yet when we join the server. We should probably just listen to the mod api hello packet and go from there, but this works (since you join and leave servers quite often).
+ if (party == null)
+ sendSyncPacket()
+ }
+
+ @Subscribe
+ fun onPartyRelatedMessage(event: ProcessChatEvent) {
+ Regexes.joinSelf.useMatch(event.unformattedString) {
+ sendSyncPacket()
+ }
+ Regexes.joinOther.useMatch(event.unformattedString) {
+ addMemberToParty(group("name"))
+ }
+ Regexes.leaveOther.useMatch(event.unformattedString) {
+ modifyParty { it.removeIf { it.name == group("name") } }
+ }
+ Regexes.leaveSelf.useMatch(event.unformattedString) {
+ modifyParty { it.clear() }
+ }
+ Regexes.disbandedEmpty.useMatch(event.unformattedString) {
+ modifyParty { it.clear() }
+ }
+ Regexes.kickedOther.useMatch(event.unformattedString) {
+ modifyParty { it.removeIf { it.name == group("name") } }
+ }
+ Regexes.kickedOtherOffline.useMatch(event.unformattedString) {
+ modifyParty { it.removeIf { it.name == group("name") } }
+ }
+ Regexes.disconnectedOther.useMatch(event.unformattedString) {
+ modifyParty { it.removeIf { it.name == group("name") } }
+ }
+ Regexes.transferLeave.useMatch(event.unformattedString) {
+ modifyParty {
+ it.modifyMember(group("name")) { it.copy(role = PartyRole.LEADER) }
+ it.removeIf { it.name == group("name2") }
+ }
+ }
+ Regexes.transferVoluntary.useMatch(event.unformattedString) {
+ modifyParty {
+ it.modifyMember(group("name")) { it.copy(role = PartyRole.LEADER) }
+ it.modifyMember(group("name2")) { it.copy(role = PartyRole.MOD) }
+ }
+ }
+ Regexes.disbanded.useMatch(event.unformattedString) {
+ modifyParty { it.clear() }
+ }
+ Regexes.kickedSelf.useMatch(event.unformattedString) {
+ modifyParty { it.clear() }
+ }
+ Regexes.partyFinderJoin.useMatch(event.unformattedString) {
+ addMemberToParty(group("name"))
+ }
+ }
+ }
+
+ data class Party(
+ val members: List<PartyMember>
+ )
+
+ data class PartyMember(
+ val name: String,
+ val role: PartyRole
+ ) {
+ companion object {
+ suspend fun fromUuid(uuid: UUID, role: PartyRole = PartyRole.MEMBER): PartyMember {
+ return PartyMember(
+ ErrorUtil.notNullOr(
+ Routes.getPlayerNameForUUID(uuid),
+ "Could not find username for player $uuid"
+ ) { "Ghost" },
+ role
+ )
+ }
+ }
+ }
+
+ var party: Party? = null
+}
diff --git a/src/main/kotlin/util/skyblock/Rarity.kt b/src/main/kotlin/util/skyblock/Rarity.kt
index f26cefe..2507256 100644
--- a/src/main/kotlin/util/skyblock/Rarity.kt
+++ b/src/main/kotlin/util/skyblock/Rarity.kt
@@ -1,7 +1,16 @@
package moe.nea.firmament.util.skyblock
+import kotlinx.serialization.KSerializer
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.descriptors.PrimitiveKind
+import kotlinx.serialization.descriptors.PrimitiveSerialDescriptor
+import kotlinx.serialization.descriptors.SerialDescriptor
+import kotlinx.serialization.encoding.Decoder
+import kotlinx.serialization.encoding.Encoder
import net.minecraft.item.ItemStack
+import net.minecraft.text.Style
import net.minecraft.text.Text
+import net.minecraft.util.Formatting
import moe.nea.firmament.util.StringUtil.words
import moe.nea.firmament.util.collections.lastNotNullOfOrNull
import moe.nea.firmament.util.mc.loreAccordingToNbt
@@ -10,6 +19,7 @@ import moe.nea.firmament.util.unformattedString
typealias RepoRarity = io.github.moulberry.repo.data.Rarity
+@Serializable(with = Rarity.Serializer::class)
enum class Rarity(vararg altNames: String) {
COMMON,
UNCOMMON,
@@ -21,14 +31,42 @@ enum class Rarity(vararg altNames: String) {
SUPREME,
SPECIAL,
VERY_SPECIAL,
+ ULTIMATE,
UNKNOWN
;
- val names = setOf(name) + altNames
+ object Serializer : KSerializer<Rarity> {
+ override val descriptor: SerialDescriptor
+ get() = PrimitiveSerialDescriptor(Rarity::class.java.name, PrimitiveKind.STRING)
+
+ override fun deserialize(decoder: Decoder): Rarity {
+ return valueOf(decoder.decodeString().replace(" ", "_"))
+ }
+ override fun serialize(encoder: Encoder, value: Rarity) {
+ encoder.encodeString(value.name)
+ }
+ }
+
+ val names = setOf(name) + altNames
+ val text: Text get() = Text.literal(name).setStyle(Style.EMPTY.withColor(colourMap[this]))
val neuRepoRarity: RepoRarity? = RepoRarity.entries.find { it.name == name }
companion object {
+ // TODO: inline those formattings as fields
+ val colourMap = mapOf(
+ Rarity.COMMON to Formatting.WHITE,
+ Rarity.UNCOMMON to Formatting.GREEN,
+ Rarity.RARE to Formatting.BLUE,
+ Rarity.EPIC to Formatting.DARK_PURPLE,
+ Rarity.LEGENDARY to Formatting.GOLD,
+ Rarity.MYTHIC to Formatting.LIGHT_PURPLE,
+ Rarity.DIVINE to Formatting.AQUA,
+ Rarity.SPECIAL to Formatting.RED,
+ Rarity.VERY_SPECIAL to Formatting.RED,
+ Rarity.SUPREME to Formatting.DARK_RED,
+ Rarity.ULTIMATE 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/SackUtil.kt b/src/main/kotlin/util/skyblock/SackUtil.kt
index fd67c44..af03363 100644
--- a/src/main/kotlin/util/skyblock/SackUtil.kt
+++ b/src/main/kotlin/util/skyblock/SackUtil.kt
@@ -8,9 +8,12 @@ import net.minecraft.text.Text
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.ChestInventoryUpdateEvent
import moe.nea.firmament.events.ProcessChatEvent
+import moe.nea.firmament.gui.config.storage.ConfigFixEvent
+import moe.nea.firmament.gui.config.storage.ConfigStorageClass
import moe.nea.firmament.repo.ItemNameLookup
import moe.nea.firmament.util.SHORT_NUMBER_FORMAT
import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.data.Config
import moe.nea.firmament.util.data.ProfileSpecificDataHolder
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.iterableView
@@ -28,7 +31,15 @@ object SackUtil {
// val sackTypes:
)
- object Store : ProfileSpecificDataHolder<SackContents>(serializer(), "Sacks", ::SackContents)
+ @Config
+ object Store : ProfileSpecificDataHolder<SackContents>(serializer(), "sacks", ::SackContents)
+
+ @Subscribe
+ fun onConfigFix(event: ConfigFixEvent) {
+ event.on(996, ConfigStorageClass.PROFILE) {
+ move("Sacks", "sacks")
+ }
+ }
val items get() = Store.data?.contents ?: mutableMapOf()
val storedRegex = "^Stored: (?<stored>$SHORT_NUMBER_FORMAT)/(?<max>$SHORT_NUMBER_FORMAT)$".toPattern()
@@ -93,7 +104,7 @@ object SackUtil {
fun updateFromHoverText(text: Text) {
text.siblings.forEach(::updateFromHoverText)
- val hoverText = text.style.hoverEvent?.getValue(HoverEvent.Action.SHOW_TEXT) ?: return
+ val hoverText = (text.style.hoverEvent as? HoverEvent.ShowText)?.value ?: return
val cleanedText = hoverText.unformattedString
if (cleanedText.startsWith("Added items:\n")) {
if (!foundAdded) {
diff --git a/src/main/kotlin/util/skyblock/SkyBlockItems.kt b/src/main/kotlin/util/skyblock/SkyBlockItems.kt
index c94ebfe..785866e 100644
--- a/src/main/kotlin/util/skyblock/SkyBlockItems.kt
+++ b/src/main/kotlin/util/skyblock/SkyBlockItems.kt
@@ -3,8 +3,23 @@ package moe.nea.firmament.util.skyblock
import moe.nea.firmament.util.SkyblockId
object SkyBlockItems {
+ val COINS = SkyblockId("SKYBLOCK_COIN")
val ROTTEN_FLESH = SkyblockId("ROTTEN_FLESH")
val ENCHANTED_DIAMOND = SkyblockId("ENCHANTED_DIAMOND")
val DIAMOND = SkyblockId("DIAMOND")
val ANCESTRAL_SPADE = SkyblockId("ANCESTRAL_SPADE")
+ val REFORGE_ANVIL = SkyblockId("REFORGE_ANVIL")
+ val SLICE_OF_BLUEBERRY_CAKE = SkyblockId("SLICE_OF_BLUEBERRY_CAKE")
+ val SLICE_OF_CHEESECAKE = SkyblockId("SLICE_OF_CHEESECAKE")
+ val SLICE_OF_GREEN_VELVET_CAKE = SkyblockId("SLICE_OF_GREEN_VELVET_CAKE")
+ val SLICE_OF_RED_VELVET_CAKE = SkyblockId("SLICE_OF_RED_VELVET_CAKE")
+ val SLICE_OF_STRAWBERRY_SHORTCAKE = SkyblockId("SLICE_OF_STRAWBERRY_SHORTCAKE")
+ val ASPECT_OF_THE_VOID = SkyblockId("ASPECT_OF_THE_VOID")
+ val ASPECT_OF_THE_END = SkyblockId("ASPECT_OF_THE_END")
+ val BONE_BOOMERANG = SkyblockId("BONE_BOOMERANG")
+ val STARRED_BONE_BOOMERANG = SkyblockId("STARRED_BONE_BOOMERANG")
+ val TRIBAL_SPEAR = SkyblockId("TRIBAL_SPEAR")
+ val BLOCK_ZAPPER = SkyblockId("BLOCK_ZAPPER")
+ val HUNTING_TOOLKIT = SkyblockId("HUNTING_TOOLKIT")
+ val ETHERWARP_CONDUIT = SkyblockId("ETHERWARP_CONDUIT")
}
diff --git a/src/main/kotlin/util/skyblock/TabListAPI.kt b/src/main/kotlin/util/skyblock/TabListAPI.kt
new file mode 100644
index 0000000..6b937da
--- /dev/null
+++ b/src/main/kotlin/util/skyblock/TabListAPI.kt
@@ -0,0 +1,41 @@
+package moe.nea.firmament.util.skyblock
+
+import org.intellij.lang.annotations.Language
+import net.minecraft.text.Text
+import moe.nea.firmament.util.StringUtil.title
+import moe.nea.firmament.util.StringUtil.unwords
+import moe.nea.firmament.util.mc.MCTabListAPI
+import moe.nea.firmament.util.unformattedString
+
+object TabListAPI {
+
+ fun getWidgetLines(widgetName: WidgetName, includeTitle: Boolean = false, from: MCTabListAPI.CurrentTabList = MCTabListAPI.currentTabList): List<Text> {
+ return from.body
+ .dropWhile { !widgetName.matchesTitle(it) }
+ .takeWhile { it.string.isNotBlank() && !it.string.startsWith(" ") }
+ .let { if (includeTitle) it else it.drop(1) }
+ }
+
+ enum class WidgetName(regex: Regex?) {
+ COMMISSIONS,
+ SKILLS("Skills:( .*)?"),
+ PROFILE("Profile: (.*)"),
+ COLLECTION,
+ ESSENCE,
+ PET
+ ;
+
+ fun matchesTitle(it: Text): Boolean {
+ return regex.matches(it.unformattedString)
+ }
+
+ constructor() : this(null)
+ constructor(@Language("RegExp") regex: String) : this(Regex(regex))
+
+ val label =
+ name.split("_").map { it.lowercase().title() }.unwords()
+ val regex = regex ?: Regex.fromLiteral("$label:")
+
+ }
+
+}
diff --git a/src/main/kotlin/util/textutil.kt b/src/main/kotlin/util/textutil.kt
index 5d95d7a..f7c7d1c 100644
--- a/src/main/kotlin/util/textutil.kt
+++ b/src/main/kotlin/util/textutil.kt
@@ -1,71 +1,18 @@
package moe.nea.firmament.util
+import java.util.Optional
import net.minecraft.text.ClickEvent
+import net.minecraft.text.HoverEvent
import net.minecraft.text.MutableText
+import net.minecraft.text.OrderedText
import net.minecraft.text.PlainTextContent
+import net.minecraft.text.StringVisitable
+import net.minecraft.text.Style
import net.minecraft.text.Text
import net.minecraft.text.TextColor
import net.minecraft.text.TranslatableTextContent
import net.minecraft.util.Formatting
-import moe.nea.firmament.Firmament
-
-
-class TextMatcher(text: Text) {
- data class State(
- var iterator: MutableList<Text>,
- var currentText: Text?,
- var offset: Int,
- var textContent: String,
- )
-
- var state = State(
- mutableListOf(text),
- null,
- 0,
- ""
- )
-
- fun pollChunk(): Boolean {
- val firstOrNull = state.iterator.removeFirstOrNull() ?: return false
- state.offset = 0
- state.currentText = firstOrNull
- state.textContent = when (val content = firstOrNull.content) {
- is PlainTextContent.Literal -> content.string
- else -> {
- Firmament.logger.warn("TextContent of type ${content.javaClass} not understood.")
- return false
- }
- }
- state.iterator.addAll(0, firstOrNull.siblings)
- return true
- }
-
- fun pollChunks(): Boolean {
- while (state.offset !in state.textContent.indices) {
- if (!pollChunk()) {
- return false
- }
- }
- return true
- }
-
- fun pollChar(): Char? {
- if (!pollChunks()) return null
- return state.textContent[state.offset++]
- }
-
- fun expectString(string: String): Boolean {
- var found = ""
- while (found.length < string.length) {
- if (!pollChunks()) return false
- val takeable = state.textContent.drop(state.offset).take(string.length - found.length)
- state.offset += takeable.length
- found += takeable
- }
- return found == string
- }
-}
val formattingChars = "kmolnrKMOLNR".toSet()
fun CharSequence.removeColorCodes(keepNonColorCodes: Boolean = false): String {
@@ -89,20 +36,94 @@ fun CharSequence.removeColorCodes(keepNonColorCodes: Boolean = false): String {
return stringBuffer.toString()
}
+fun OrderedText.reconstitute(): MutableText {
+ val base = Text.literal("")
+ base.setStyle(Style.EMPTY.withItalic(false))
+ var lastColorCode = Style.EMPTY
+ val text = StringBuilder()
+ this.accept { index, style, codePoint ->
+ if (style != lastColorCode) {
+ if (text.isNotEmpty())
+ base.append(Text.literal(text.toString()).setStyle(lastColorCode))
+ lastColorCode = style
+ text.clear()
+ }
+ text.append(codePoint.toChar())
+ true
+ }
+ if (text.isNotEmpty())
+ base.append(Text.literal(text.toString()).setStyle(lastColorCode))
+ return base
+
+}
+
+fun StringVisitable.reconstitute(): MutableText {
+ val base = Text.literal("")
+ base.setStyle(Style.EMPTY.withItalic(false))
+ var lastColorCode = Style.EMPTY
+ val text = StringBuilder()
+ this.visit({ style, string ->
+ if (style != lastColorCode) {
+ if (text.isNotEmpty())
+ base.append(Text.literal(text.toString()).setStyle(lastColorCode))
+ lastColorCode = style
+ text.clear()
+ }
+ text.append(string)
+ Optional.empty<Unit>()
+ }, Style.EMPTY)
+ if (text.isNotEmpty())
+ base.append(Text.literal(text.toString()).setStyle(lastColorCode))
+ return base
+
+}
+
val Text.unformattedString: String
get() = string.removeColorCodes() // TODO: maybe shortcircuit this with .visit
val Text.directLiteralStringContent: String? get() = (this.content as? PlainTextContent)?.string()
-fun Text.getLegacyFormatString() =
+fun Text.getLegacyFormatString(trimmed: Boolean = false): String =
run {
+ var lastCode = "§r"
val sb = StringBuilder()
+ fun appendCode(code: String) {
+ if (code != lastCode || !trimmed) {
+ sb.append(code)
+ lastCode = code
+ }
+ }
for (component in iterator()) {
- sb.append(component.style.color?.toChatFormatting()?.toString() ?: "§r")
+ if (component.directLiteralStringContent.isNullOrEmpty() && component.siblings.isEmpty()) {
+ continue
+ }
+ appendCode(component.style.let { style ->
+ var color = style.color?.toChatFormatting()?.toString() ?: "§r"
+ if (style.isBold)
+ color += LegacyFormattingCode.BOLD.formattingCode
+ if (style.isItalic)
+ color += LegacyFormattingCode.ITALIC.formattingCode
+ if (style.isUnderlined)
+ color += LegacyFormattingCode.UNDERLINE.formattingCode
+ if (style.isObfuscated)
+ color += LegacyFormattingCode.OBFUSCATED.formattingCode
+ if (style.isStrikethrough)
+ color += LegacyFormattingCode.STRIKETHROUGH.formattingCode
+ color
+ })
sb.append(component.directLiteralStringContent)
- sb.append("§r")
+ if (!trimmed)
+ appendCode("§r")
}
sb.toString()
+ }.also {
+ var it = it
+ if (trimmed) {
+ it = it.removeSuffix("§r")
+ if (it.length == 2 && it.startsWith("§"))
+ it = ""
+ }
+ it
}
private val textColorLUT = Formatting.entries
@@ -133,28 +154,41 @@ 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.darkGrey() = withColor(Formatting.DARK_GRAY)
fun MutableText.red() = withColor(Formatting.RED)
fun MutableText.white() = withColor(Formatting.WHITE)
fun MutableText.bold(): MutableText = styled { it.withBold(true) }
-
+fun MutableText.hover(text: Text): MutableText = styled { it.withHoverEvent(HoverEvent.ShowText(text)) }
+fun MutableText.boolColour(
+ bool: Boolean,
+ ifTrue: Formatting = Formatting.GREEN,
+ ifFalse: Formatting = Formatting.DARK_RED
+) =
+ if (bool) withColor(ifTrue) else withColor(ifFalse)
fun MutableText.clickCommand(command: String): MutableText {
require(command.startsWith("/"))
return this.styled {
- it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND,
- "/firm disablereiwarning"))
+ it.withClickEvent(ClickEvent.RunCommand(command))
}
}
+fun MutableText.prepend(text: Text): MutableText {
+ siblings.addFirst(text)
+ return this
+}
+
fun Text.transformEachRecursively(function: (Text) -> Text): Text {
val c = this.content
if (c is TranslatableTextContent) {
return Text.translatableWithFallback(c.key, c.fallback, *c.args.map {
- (if (it is Text) it else Text.literal(it.toString())).transformEachRecursively(function)
+ (it as? Text ?: Text.literal(it.toString())).transformEachRecursively(function)
}.toTypedArray()).also { new ->
new.style = this.style
new.siblings.clear()
+ val new = function(new)
this.siblings.forEach { child ->
new.siblings.add(child.transformEachRecursively(function))
}
@@ -169,4 +203,14 @@ fun Text.transformEachRecursively(function: (Text) -> Text): Text {
fun tr(key: String, default: String): MutableText = error("Compiler plugin did not run.")
fun trResolved(key: String, vararg args: Any): MutableText = Text.stringifiedTranslatable(key, *args)
+fun titleCase(str: String): String {
+ return str
+ .lowercase()
+ .replace("_", " ")
+ .split(" ")
+ .joinToString(" ") { word ->
+ word.replaceFirstChar { if (it.isLowerCase()) it.titlecase() else it.toString() }
+ }
+}
+
diff --git a/src/main/kotlin/util/uuid.kt b/src/main/kotlin/util/uuid.kt
index cccfdd2..14aa83d 100644
--- a/src/main/kotlin/util/uuid.kt
+++ b/src/main/kotlin/util/uuid.kt
@@ -3,6 +3,12 @@ package moe.nea.firmament.util
import java.math.BigInteger
import java.util.UUID
+fun parsePotentiallyDashlessUUID(unknownFormattedUUID: String): UUID {
+ if ("-" in unknownFormattedUUID)
+ return UUID.fromString(unknownFormattedUUID)
+ return parseDashlessUUID(unknownFormattedUUID)
+}
+
fun parseDashlessUUID(dashlessUuid: String): UUID {
val most = BigInteger(dashlessUuid.substring(0, 16), 16)
val least = BigInteger(dashlessUuid.substring(16, 32), 16)