aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.editorconfig1
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.yml29
-rw-r--r--.github/ISSUE_TEMPLATE/config.yml9
-rw-r--r--.github/ISSUE_TEMPLATE/feature.yml31
-rw-r--r--README.md23
-rw-r--r--REUSE.toml4
-rw-r--r--build.gradle.kts178
-rw-r--r--docs/firmament_logo.webpbin0 -> 474 bytes
-rw-r--r--docs/firmament_logo.webp.license3
-rw-r--r--docs/firmament_logo_256.webpbin0 -> 740 bytes
-rw-r--r--docs/firmament_logo_256.webp.license3
-rw-r--r--docs/firmament_logo_256_nobg.webpbin0 -> 9840 bytes
-rw-r--r--docs/firmament_logo_256_nobg.webp.license3
-rw-r--r--docs/firmament_logo_256_trans.webpbin0 -> 9972 bytes
-rw-r--r--docs/firmament_logo_256_trans.webp.license3
-rw-r--r--docs/firmament_logo_nobg.webpbin0 -> 9632 bytes
-rw-r--r--docs/firmament_logo_nobg.webp.license3
-rw-r--r--docs/firmament_logo_trans.webpbin0 -> 9706 bytes
-rw-r--r--docs/firmament_logo_trans.webp.license3
-rwxr-xr-xdocs/release_script.sh82
-rw-r--r--gradle/libs.versions.toml65
-rw-r--r--gradle/wrapper/gradle-wrapper.properties2
-rw-r--r--settings.gradle.kts1
-rw-r--r--src/compat/jade/java/moe/nea/firmament/compat/jade/Compat.kt12
-rw-r--r--src/compat/jade/java/moe/nea/firmament/compat/jade/CustomFakeBlockProvider.kt41
-rw-r--r--src/compat/jade/java/moe/nea/firmament/compat/jade/CustomMiningHardnessProvider.kt97
-rw-r--r--src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt77
-rw-r--r--src/compat/jade/java/moe/nea/firmament/compat/jade/FirmamentJadePlugin.kt21
-rw-r--r--src/compat/jade/java/moe/nea/firmament/compat/jade/JadeIntegration.kt50
-rw-r--r--src/compat/jade/java/moe/nea/firmament/compat/jade/utils.kt6
-rw-r--r--src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/ElementAccessor.java12
-rw-r--r--src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/EnforceToolDisplayForCustomBlocksInHarvestToolProvider.java33
-rw-r--r--src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/OnUpdateBreakProgress.java22
-rw-r--r--src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/PatchBreakingBarSpeedJade.java25
-rw-r--r--src/compat/modmenu/java/moe/nea/firmament/compat/modmenu/FirmamentModMenuPlugin.kt2
-rw-r--r--src/compat/moulconfig/java/MCConfigEditorIntegration.kt193
-rw-r--r--src/compat/moulconfig/java/ProcessedOptionFirm.kt3
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/Compat.kt12
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt54
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt59
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/REIRecipeLayouter.kt62
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt28
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt77
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt4
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/GenericREIRecipeCategory.kt67
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt56
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt61
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt71
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt48
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBShopRecipe.kt61
-rw-r--r--src/compat/wildfireGender/java/moe/nea/firmament/compat/gender/Compat.kt13
-rw-r--r--src/compat/yacl/java/YaclIntegration.kt2
-rw-r--r--src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java36
-rw-r--r--src/main/java/moe/nea/firmament/init/MixinPlugin.java103
-rw-r--r--src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java183
-rw-r--r--src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java24
-rw-r--r--src/main/java/moe/nea/firmament/mixins/CopyChatPatch.java44
-rw-r--r--src/main/java/moe/nea/firmament/mixins/DisableHurtCam.java18
-rw-r--r--src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java17
-rw-r--r--src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java34
-rw-r--r--src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java7
-rw-r--r--src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java13
-rw-r--r--src/main/java/moe/nea/firmament/mixins/LenientProfileComponentPatch.java25
-rw-r--r--src/main/java/moe/nea/firmament/mixins/MinecraftInitLevelListener.java26
-rw-r--r--src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java16
-rw-r--r--src/main/java/moe/nea/firmament/mixins/MixinRecipeBookScreen.java16
-rw-r--r--src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java20
-rw-r--r--src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatch.java36
-rw-r--r--src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java8
-rw-r--r--src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java30
-rw-r--r--src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java6
-rw-r--r--src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java10
-rw-r--r--src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java2
-rw-r--r--src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java31
-rw-r--r--src/main/java/moe/nea/firmament/mixins/accessor/AccessorWorldRenderer.java17
-rw-r--r--src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java25
-rw-r--r--src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeFeatureRenderer.java43
-rw-r--r--src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeStorage.java23
-rw-r--r--src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/SaveCapeToPlayerEntityRenderState.java19
-rw-r--r--src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java62
-rw-r--r--src/main/java/moe/nea/firmament/mixins/render/entitytints/EntityRenderStateTint.java55
-rw-r--r--src/main/java/moe/nea/firmament/mixins/render/entitytints/InjectIntoRenderState.java30
-rw-r--r--src/main/java/moe/nea/firmament/mixins/render/entitytints/ReplaceOverlayTexture.java24
-rw-r--r--src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableEquipmentRenderer.java34
-rw-r--r--src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java25
-rw-r--r--src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java25
-rw-r--r--src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableSkullBlockEntityRenderer.java25
-rw-r--r--src/main/kotlin/Compat.kt11
-rw-r--r--src/main/kotlin/Firmament.kt29
-rw-r--r--src/main/kotlin/apis/Profiles.kt6
-rw-r--r--src/main/kotlin/apis/Routes.kt16
-rw-r--r--src/main/kotlin/commands/rome.kt50
-rw-r--r--src/main/kotlin/events/CustomItemModelEvent.kt60
-rw-r--r--src/main/kotlin/events/EntityRenderTintEvent.kt66
-rw-r--r--src/main/kotlin/events/EntityUpdateEvent.kt53
-rw-r--r--src/main/kotlin/events/FirmamentEventBus.kt3
-rw-r--r--src/main/kotlin/events/IsSlotProtectedEvent.kt81
-rw-r--r--src/main/kotlin/events/PlayerInventoryUpdate.kt19
-rw-r--r--src/main/kotlin/events/WorldKeyboardEvent.kt17
-rw-r--r--src/main/kotlin/events/WorldMouseMoveEvent.kt5
-rw-r--r--src/main/kotlin/features/FeatureManager.kt158
-rw-r--r--src/main/kotlin/features/chat/ChatLinks.kt18
-rw-r--r--src/main/kotlin/features/chat/CopyChat.kt31
-rw-r--r--src/main/kotlin/features/debug/AnimatedClothingScanner.kt193
-rw-r--r--src/main/kotlin/features/debug/DebugLogger.kt2
-rw-r--r--src/main/kotlin/features/debug/DeveloperFeatures.kt53
-rw-r--r--src/main/kotlin/features/debug/ExportedTestConstantMeta.kt27
-rw-r--r--src/main/kotlin/features/debug/PowerUserTools.kt53
-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.kt184
-rw-r--r--src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt75
-rw-r--r--src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt270
-rw-r--r--src/main/kotlin/features/debug/itemeditor/PromptScreen.kt15
-rw-r--r--src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt4
-rw-r--r--src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt63
-rw-r--r--src/main/kotlin/features/events/carnival/MinesweeperHelper.kt4
-rw-r--r--src/main/kotlin/features/fixes/Fixes.kt34
-rw-r--r--src/main/kotlin/features/garden/HideComposterNoises.kt32
-rw-r--r--src/main/kotlin/features/inventory/CraftingOverlay.kt2
-rw-r--r--src/main/kotlin/features/inventory/ItemHotkeys.kt5
-rw-r--r--src/main/kotlin/features/inventory/PetFeatures.kt58
-rw-r--r--src/main/kotlin/features/inventory/PriceData.kt147
-rw-r--r--src/main/kotlin/features/inventory/REIDependencyWarner.kt4
-rw-r--r--src/main/kotlin/features/inventory/SlotLocking.kt154
-rw-r--r--src/main/kotlin/features/inventory/TimerInLore.kt35
-rw-r--r--src/main/kotlin/features/inventory/WardrobeKeybinds.kt80
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButton.kt129
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt126
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtons.kt60
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageBackingHandle.kt4
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt5
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlayCustom.kt3
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt220
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt7
-rw-r--r--src/main/kotlin/features/items/BonemerangOverlay.kt101
-rw-r--r--src/main/kotlin/features/items/EtherwarpOverlay.kt51
-rw-r--r--src/main/kotlin/features/macros/ComboProcessor.kt114
-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.kt12
-rw-r--r--src/main/kotlin/features/macros/MacroUI.kt285
-rw-r--r--src/main/kotlin/features/macros/RadialMenu.kt149
-rw-r--r--src/main/kotlin/features/mining/MiningBlockInfoUi.kt54
-rw-r--r--src/main/kotlin/features/mining/PickaxeAbility.kt28
-rw-r--r--src/main/kotlin/features/misc/CustomCapes.kt192
-rw-r--r--src/main/kotlin/features/misc/Devs.kt38
-rw-r--r--src/main/kotlin/features/misc/Hud.kt77
-rw-r--r--src/main/kotlin/features/misc/LicenseViewer.kt128
-rw-r--r--src/main/kotlin/features/world/ColeWeightCompat.kt125
-rw-r--r--src/main/kotlin/features/world/FairySouls.kt3
-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.kt74
-rw-r--r--src/main/kotlin/features/world/Waypoints.kt306
-rw-r--r--src/main/kotlin/gui/CheckboxComponent.kt5
-rw-r--r--src/main/kotlin/gui/config/AllConfigsGui.kt24
-rw-r--r--src/main/kotlin/gui/config/BuiltInConfigScreenProvider.kt2
-rw-r--r--src/main/kotlin/gui/config/FirmamentConfigScreenProvider.kt2
-rw-r--r--src/main/kotlin/gui/config/KeyBindingHandler.kt29
-rw-r--r--src/main/kotlin/gui/config/KeyBindingStateManager.kt44
-rw-r--r--src/main/kotlin/gui/config/ManagedConfig.kt3
-rw-r--r--src/main/kotlin/gui/config/ManagedOption.kt2
-rw-r--r--src/main/kotlin/gui/entity/EntityRenderer.kt94
-rw-r--r--src/main/kotlin/gui/entity/FakeWorld.kt343
-rw-r--r--src/main/kotlin/gui/entity/ModifyEquipment.kt8
-rw-r--r--src/main/kotlin/gui/entity/ModifyHorse.kt73
-rw-r--r--src/main/kotlin/keybindings/IKeyBinding.kt31
-rw-r--r--src/main/kotlin/keybindings/SavedKeyBinding.kt203
-rw-r--r--src/main/kotlin/repo/BetterRepoRecipeCache.kt5
-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/HypixelStaticData.kt25
-rw-r--r--src/main/kotlin/repo/ItemCache.kt152
-rw-r--r--src/main/kotlin/repo/MiningRepoData.kt133
-rw-r--r--src/main/kotlin/repo/ModernOverlaysData.kt41
-rw-r--r--src/main/kotlin/repo/RepoDownloadManager.kt193
-rw-r--r--src/main/kotlin/repo/RepoManager.kt44
-rw-r--r--src/main/kotlin/repo/RepoModResourcePack.kt7
-rw-r--r--src/main/kotlin/repo/SBItemStack.kt116
-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.kt24
-rw-r--r--src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt76
-rw-r--r--src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt83
-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/ErrorUtil.kt25
-rw-r--r--src/main/kotlin/util/FirmFormatters.kt36
-rw-r--r--src/main/kotlin/util/HoveredItemStack.kt2
-rw-r--r--src/main/kotlin/util/IntUtil.kt12
-rw-r--r--src/main/kotlin/util/LegacyTagWriter.kt103
-rw-r--r--src/main/kotlin/util/MC.kt30
-rw-r--r--src/main/kotlin/util/MoulConfigUtils.kt80
-rw-r--r--src/main/kotlin/util/SBData.kt13
-rw-r--r--src/main/kotlin/util/ScoreboardUtil.kt72
-rw-r--r--src/main/kotlin/util/SkyBlockIsland.kt62
-rw-r--r--src/main/kotlin/util/SkyblockId.kt147
-rw-r--r--src/main/kotlin/util/StringUtil.kt2
-rw-r--r--src/main/kotlin/util/TestUtil.kt1
-rw-r--r--src/main/kotlin/util/asm/AsmAnnotationUtil.kt89
-rw-r--r--src/main/kotlin/util/async/input.kt108
-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/MultiFileDataHolder.kt63
-rw-r--r--src/main/kotlin/util/json/DashlessUUIDSerializer.kt6
-rw-r--r--src/main/kotlin/util/json/KJsonUtils.kt11
-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/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/MCTabListAPI.kt96
-rw-r--r--src/main/kotlin/util/mc/NbtPrism.kt91
-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.kt3
-rw-r--r--src/main/kotlin/util/mc/SlotUtils.kt18
-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.kt43
-rw-r--r--src/main/kotlin/util/render/FacingThePlayerContext.kt6
-rw-r--r--src/main/kotlin/util/render/FirmamentShaders.kt18
-rw-r--r--src/main/kotlin/util/render/LerpUtils.kt35
-rw-r--r--src/main/kotlin/util/render/RenderCircleProgress.kt124
-rw-r--r--src/main/kotlin/util/render/RenderInWorldContext.kt114
-rw-r--r--src/main/kotlin/util/render/TintedOverlayTexture.kt35
-rw-r--r--src/main/kotlin/util/render/TranslatedScissors.kt6
-rw-r--r--src/main/kotlin/util/skyblock/DungeonUtil.kt33
-rw-r--r--src/main/kotlin/util/skyblock/ItemType.kt2
-rw-r--r--src/main/kotlin/util/skyblock/SackUtil.kt2
-rw-r--r--src/main/kotlin/util/skyblock/SkyBlockItems.kt11
-rw-r--r--src/main/kotlin/util/skyblock/TabListAPI.kt41
-rw-r--r--src/main/kotlin/util/textutil.kt157
-rw-r--r--src/main/kotlin/util/uuid.kt6
-rw-r--r--src/main/resources/assets/firmament/gui/config/macros/combos.xml55
-rw-r--r--src/main/resources/assets/firmament/gui/config/macros/editor_combo.xml42
-rw-r--r--src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml43
-rw-r--r--src/main/resources/assets/firmament/gui/config/macros/index.xml27
-rw-r--r--src/main/resources/assets/firmament/gui/config/macros/wheel.xml54
-rw-r--r--src/main/resources/assets/firmament/gui/license_viewer/index.xml65
-rw-r--r--src/main/resources/assets/firmament/gui/mining_block_info/index.xml38
-rw-r--r--src/main/resources/assets/firmament/logo.pngbin16321 -> 19770 bytes
-rw-r--r--src/main/resources/assets/firmament/shaders/cape/parallax.fsh53
-rw-r--r--src/main/resources/assets/firmament/shaders/circle_discard_color.fsh22
-rw-r--r--src/main/resources/assets/firmament/textures/cape/REUSE.toml19
-rw-r--r--src/main/resources/assets/firmament/textures/cape/firm_static.pngbin0 -> 42249 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/cape/firmament_star.pngbin0 -> 1141 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/cape/fsr_static.pngbin0 -> 21747 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/cape/parallax_background.pngbin0 -> 2053 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/cape/parallax_template.pngbin0 -> 352 bytes
-rw-r--r--src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.pngbin4766 -> 745 bytes
-rw-r--r--src/main/resources/fabric.mod.json5
-rw-r--r--src/main/resources/firmament.accesswidener15
-rw-r--r--src/main/resources/legacy_data/enchantments.json560
-rw-r--r--src/main/resources/legacy_data/items.json3733
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.pngbin0 -> 122 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.pngbin0 -> 639 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.pngbin0 -> 147 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta10
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.pngbin0 -> 768 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta10
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.pngbin0 -> 203 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta9
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.pngbin0 -> 795 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta10
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta10
-rw-r--r--src/test/kotlin/MixinTest.kt34
-rw-r--r--src/test/kotlin/features/macros/KeyComboTrieCreation.kt103
-rw-r--r--src/test/kotlin/root.kt1
-rw-r--r--src/test/kotlin/testutil/AutoBootstrapExtension.kt14
-rw-r--r--src/test/kotlin/testutil/ItemResources.kt65
-rw-r--r--src/test/kotlin/testutil/KotestPlugin.kt16
-rw-r--r--src/test/kotlin/util/ColorCodeTest.kt100
-rw-r--r--src/test/kotlin/util/TextUtilText.kt10
-rw-r--r--src/test/kotlin/util/math/GChainReconciliationTest.kt75
-rw-r--r--src/test/kotlin/util/math/ProjectionsBoxTest.kt28
-rw-r--r--src/test/kotlin/util/skyblock/AbilityUtilsTest.kt32
-rw-r--r--src/test/kotlin/util/skyblock/ItemTypeTest.kt40
-rw-r--r--src/test/kotlin/util/skyblock/SackUtilTest.kt4
-rw-r--r--src/test/kotlin/util/skyblock/TabListAPITest.kt48
-rw-r--r--src/test/kotlin/util/skyblock/TimestampTest.kt28
-rw-r--r--src/test/resources/testdata/chat/all-chat.snbt3
-rw-r--r--src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt3
-rw-r--r--src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt3
-rw-r--r--src/test/resources/testdata/items/aspect-of-the-void.snbt3
-rw-r--r--src/test/resources/testdata/items/backpack-in-menu.snbt122
-rw-r--r--src/test/resources/testdata/items/books/feather_falling.snbt39
-rw-r--r--src/test/resources/testdata/items/diamond-pickaxe.snbt3
-rw-r--r--src/test/resources/testdata/items/gemstone-gauntlet.snbt3
-rw-r--r--src/test/resources/testdata/items/hyperion.snbt3
-rw-r--r--src/test/resources/testdata/items/implosion-belt.snbt3
-rw-r--r--src/test/resources/testdata/items/necron-boots.snbt71
-rw-r--r--src/test/resources/testdata/items/pets/lion-item.snbt3
-rw-r--r--src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt3
-rw-r--r--src/test/resources/testdata/items/pets/rabbit-selected.snbt3
-rw-r--r--src/test/resources/testdata/items/rune-in-sack.snbt3
-rw-r--r--src/test/resources/testdata/items/titanium-drill.snbt3
-rw-r--r--src/test/resources/testdata/tablist/dungeon_hub.snbt1170
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/Compat.kt11
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt553
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt25
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt5
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt15
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomScreenLayouts.kt224
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt8
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt24
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt7
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt90
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt13
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt3
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt7
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt9
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt454
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt58
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt2
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PullingPredicate.kt26
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/SkullPredicate.kt63
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/BuildExtraBlockStateModels.java24
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/InsertExtraBlockModelDependencies.java28
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ItemRenderStateExtraInfo.java28
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java15
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java37
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java3
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java47
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java4
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceHeadModel.java51
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java28
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextColorInHandledScreen.java48
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java17
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ExpandScreenBoundaries.java21
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceAnvilScreen.java55
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceForgingScreen.java9
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceFurnaceBackgrounds.java31
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceGenericBackgrounds.java28
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplacePlayerBackgrounds.java50
-rw-r--r--src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceTextColorInHandledScreen.java65
-rw-r--r--symbols/src/main/kotlin/process/CompatMetaProcessor.kt63
-rw-r--r--symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt21
-rw-r--r--translations/en_us.json169
-rw-r--r--translations/extra.json6
-rw-r--r--web/src/pages/docs/_texture-pack-format.md246
347 files changed, 18070 insertions, 3499 deletions
diff --git a/.editorconfig b/.editorconfig
index 109d855..1c59d9d 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -17,3 +17,4 @@ max_line_length = 120
ij_kotlin_name_count_to_use_star_import = 99999
ij_kotlin_name_count_to_use_star_import_for_members = 99999
ij_kotlin_imports_layout = *, |, kotlinx.**, kotlin.**, net.minecraft.**, moe.nea.firmament.**, |, $*
+ij_kotlin_packages_to_use_import_on_demand = false
diff --git a/.github/ISSUE_TEMPLATE/bug_report.yml b/.github/ISSUE_TEMPLATE/bug_report.yml
new file mode 100644
index 0000000..dfc9c09
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/bug_report.yml
@@ -0,0 +1,29 @@
+# SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+#
+# SPDX-License-Identifier: CC0-1.0
+
+name: Mod Bug Report
+description: Report a bug or crash in Firmament
+labels:
+ - bug
+body:
+ - type: input
+ id: version
+ attributes:
+ label: Firmament Version
+ description: Which version of Firmament are you using. Please provide the full version name (found in the file name, or the mod list). Please **don't** say latest.
+ validations:
+ required: true
+ - type: textarea
+ id: description
+ attributes:
+ label: Bug Description
+ description: Please describe the bug you encountered and what you did before you encountered it.
+ validations:
+ required: true
+ - type: textarea
+ attributes:
+ label: Minecraft Log
+ description: Please provide the complete Minecraft Log. Please do not truncate your log, or extract only parts. Earlier parts of the log can contain important information.
+ render: plain text
+ placeholder: If you are worried about your log containing private information, you can use an external uploader like https://mclo.gs/ to strip all personal information, and paste the link in here instead.
diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml
new file mode 100644
index 0000000..a3a11ba
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/config.yml
@@ -0,0 +1,9 @@
+# SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+#
+# SPDX-License-Identifier: CC0-1.0
+
+blank_issues_enabled: true
+contact_links:
+ - name: Discord Server
+ url: https://discord.gg/64pFP94AWA
+ about: For non bug support, check out the Discord.
diff --git a/.github/ISSUE_TEMPLATE/feature.yml b/.github/ISSUE_TEMPLATE/feature.yml
new file mode 100644
index 0000000..29fa766
--- /dev/null
+++ b/.github/ISSUE_TEMPLATE/feature.yml
@@ -0,0 +1,31 @@
+# SPDX-FileCopyrightText: 2024 Linnea Gräf <nea@nea.moe>
+#
+# SPDX-License-Identifier: CC0-1.0
+
+name: Feature Request
+description: Request a new feature or for a feature to be expanded.
+labels:
+ - enhancement
+body:
+ - type: markdown
+ attributes:
+ value: |
+ Thank you for expressing your interest in a new Firmament feature.
+ Please keep in mind that not every feature is a good fit for Firmament,
+ *especially* if there is already another mod for Firmaments versions
+ which covers that feature very well. It doesn't hurt to ask, but please
+ understand that even features I want to implement, might take some time
+ to be fully realized.
+
+ Before creating a feature request, please check if the feature complies
+ with [HyPixel rules](https://hypixel.net/skyblock-rules).
+ - type: textarea
+ attributes:
+ label: Describe the feature
+ description: Please describe the feature you are looking for.
+ validations:
+ required: true
+ - type: input
+ attributes:
+ label: Other mods
+ description: Do you know of other mods that have this feature already? If so, please list them here.
diff --git a/README.md b/README.md
index 04c5aff..f23354d 100644
--- a/README.md
+++ b/README.md
@@ -4,24 +4,31 @@ SPDX-FileCopyrightText: 2023 Linnea Gräf <nea@nea.moe>
SPDX-License-Identifier: CC0-1.0
-->
+
+
+<div align="center">
+
# Firmament
-<small><i>Powered by NEU</i></small>
+![firmament logo](./docs/firmament_logo_256_nobg.webp)
+
+<hr>
[![Forum Thread](https://img.shields.io/badge/Forum%20Thread-blue?style=flat-square)](https://hypixel.net/threads/firmament-a-skyblock-mod-for-1-20-1.5446366/)
[![Discord](https://img.shields.io/discord/1088154030628417616?style=flat-square&logo=discord)](https://discord.gg/64pFP94AWA)
[![Modrinth](https://img.shields.io/modrinth/dt/IJNUBZ2a?style=flat-square&logo=modrinth)](https://modrinth.com/mod/firmament)
-[![Github Releases](https://img.shields.io/github/downloads/romangraef/Firmament/total?style=flat-square&logo=github)](https://github.com/romangraef/firmament/releases)
+[![Github Releases](https://img.shields.io/github/downloads/nea89o/Firmament/total?style=flat-square&logo=github)](https://github.com/nea89o/firmament/releases)
+
+</div>
+
## Currently working features
- Item List of all SkyBlock Items
-- Grouping Items that belong together like minions
- Recipe Viewer for Crafting Recipes
- Recipe Viewer for Forge Recipes
- ... as well as many more custom recipe types.
- NPC waypoints
-- Image Preview in chat
- A storage overview as well as a full storage overlay
- A crafting overlay when clicking the "Move Item" plus in a crafting recipe
- Cursor position saver
@@ -38,11 +45,15 @@ SPDX-License-Identifier: CC0-1.0
Firmament needs the following libraries to work:
+- [Fabric API](https://modrinth.com/mod/fabric-api)
+- [Fabric Language Kotlin](https://modrinth.com/mod/fabric-language-kotlin)
+
+As well as (for the item list):
+-
- [RoughlyEnoughItems](https://modrinth.com/mod/rei)
- [Architectury](https://modrinth.com/mod/architectury-api)
- [Cloth Config](https://modrinth.com/mod/cloth-config)
-- [Fabric API](https://modrinth.com/mod/fabric-api)
-- [Fabric Language Kotlin](https://modrinth.com/mod/fabric-language-kotlin)
+
You can download Firmament itself on [Modrinth](https://modrinth.com/mod/firmament) or on
[GitHub](https://github.com/romangraef/firmament/releases).
diff --git a/REUSE.toml b/REUSE.toml
index 80bf77d..9a6cf0f 100644
--- a/REUSE.toml
+++ b/REUSE.toml
@@ -47,3 +47,7 @@ path = ["src/test/resources/testdata/**/*.snbt"]
SPDX-License-Identifier = "CC-BY-4.0"
SPDX-FileCopyrightText = ["Linnea Gräf <nea@nea.moe>", "Firmament Contributors"]
+[[annotations]]
+path = ["src/main/resources/legacy_data/*.json"]
+SPDX-License-Identifier = "MIT"
+SPDX-FileCopyrightText = ["PrismarineJS Minecraft Data"]
diff --git a/build.gradle.kts b/build.gradle.kts
index 1a31f16..3a72ed0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -6,15 +6,18 @@
* SPDX-License-Identifier: GPL-3.0-or-later
*/
-import com.google.devtools.ksp.gradle.KspTaskJvm
+import com.google.common.hash.Hashing
+import com.google.devtools.ksp.gradle.KspAATask
import com.google.gson.Gson
import com.google.gson.JsonObject
import moe.nea.licenseextractificator.LicenseDiscoveryTask
import moe.nea.mcautotranslations.gradle.CollectTranslations
import net.fabricmc.loom.LoomGradleExtension
+import org.apache.tools.ant.taskdefs.condition.Os
import org.jetbrains.kotlin.gradle.dsl.JvmTarget
-import org.jetbrains.kotlin.gradle.plugin.SubpluginOption
import org.jetbrains.kotlin.gradle.tasks.KotlinCompile
+import java.nio.charset.StandardCharsets
+import java.util.*
plugins {
java
@@ -25,10 +28,10 @@ plugins {
alias(libs.plugins.kotlin.plugin.ksp)
// alias(libs.plugins.loom)
// TODO: use arch loom once they update to 1.8
- id("fabric-loom") version "1.9.2"
+ id("fabric-loom") version "1.10.1"
alias(libs.plugins.shadow)
id("moe.nea.licenseextractificator")
- id("moe.nea.mc-auto-translations") version "0.1.0"
+ alias(libs.plugins.mcAutoTranslations)
}
version = getGitTagInfo(libs.versions.minecraft.get())
@@ -123,6 +126,7 @@ fun innerJarsOf(name: String, dependency: Dependency): Provider<FileTree> {
val collectTranslations by tasks.registering(CollectTranslations::class) {
this.baseTranslations.from(file("translations/en_us.json"))
+ this.baseTranslations.from(file("translations/extra.json"))
this.classes.from(sourceSets.main.get().kotlin.classesDirectory)
}
@@ -135,8 +139,10 @@ fun createIsolatedSourceSet(name: String, path: String = "compat/$name", isEnabl
val mainSS = sourceSets.main.get()
val upperName = ss.name.capitalizeN()
afterEvaluate {
- tasks.named("ksp${upperName}Kotlin", KspTaskJvm::class) {
- this.options.add(SubpluginOption("apoption", "firmament.sourceset=${ss.name}"))
+ tasks.named("ksp${upperName}Kotlin", KspAATask::class) {
+ this.commandLineArgumentProviders.add { // TODO: update https://github.com/google/ksp/issues/2075
+ listOf("firmament.sourceset=${ss.name}")
+ }
}
tasks.named("compile${upperName}Kotlin", KotlinCompile::class) {
this.enabled = isEnabled
@@ -159,15 +165,18 @@ fun createIsolatedSourceSet(name: String, path: String = "compat/$name", isEnabl
extendsFrom(getByName(mainSS.annotationProcessorConfigurationName))
}
(mainSS.runtimeOnlyConfigurationName) {
- extendsFrom(getByName(ss.runtimeClasspathConfigurationName))
+ if (isEnabled)
+ extendsFrom(getByName(ss.runtimeClasspathConfigurationName))
}
("ksp$upperName") {
extendsFrom(ksp.get())
}
}
dependencies {
- runtimeOnly(ss.output)
- (ss.implementationConfigurationName)(sourceSets.main.get().output)
+ if (isEnabled)
+ runtimeOnly(ss.output)
+ (ss.implementationConfigurationName)(project.files(tasks.compileKotlin.map { it.destinationDirectory }))
+ (ss.implementationConfigurationName)(project.files(tasks.compileJava.map { it.destinationDirectory }))
}
tasks.shadowJar {
from(ss.output)
@@ -177,8 +186,7 @@ fun createIsolatedSourceSet(name: String, path: String = "compat/$name", isEnabl
classpath.from(configurations.getByName(ss.compileClasspathConfigurationName))
}
collectTranslations {
- // TODO: this does not work, somehow
- this.classes.from(sourceSets.main.get().kotlin.classesDirectory)
+ this.classes.from(ss.kotlin.classesDirectory)
}
return ss
}
@@ -188,6 +196,11 @@ val SourceSet.modImplementationConfigurationName
loom.remapConfigurations.find {
it.targetConfigurationName.get() == this.implementationConfigurationName
}!!.sourceConfiguration
+val SourceSet.modRuntimeOnlyConfigurationName
+ get() =
+ loom.remapConfigurations.find {
+ it.targetConfigurationName.get() == this.runtimeOnlyConfigurationName
+ }!!.sourceConfiguration
val shadowMe by configurations.creating {
exclude(group = "org.jetbrains.kotlin")
@@ -215,14 +228,17 @@ val testAgent by configurations.creating {
}
-val configuredSourceSet = createIsolatedSourceSet("configured",
- isEnabled = false) // Wait for update (also low prio, because configured sucks)
+val configuredSourceSet = createIsolatedSourceSet(
+ "configured",
+ isEnabled = false
+) // Wait for update (also low prio, because configured sucks)
val sodiumSourceSet = createIsolatedSourceSet("sodium", isEnabled = false)
val citResewnSourceSet = createIsolatedSourceSet("citresewn", isEnabled = false) // TODO: Wait for update
val yaclSourceSet = createIsolatedSourceSet("yacl")
val explosiveEnhancementSourceSet =
createIsolatedSourceSet("explosiveEnhancement", isEnabled = false) // TODO: wait for their port
val wildfireGenderSourceSet = createIsolatedSourceSet("wildfireGender")
+val jadeSourceSet = createIsolatedSourceSet("jade")
val modmenuSourceSet = createIsolatedSourceSet("modmenu")
val reiSourceSet = createIsolatedSourceSet("rei")
val moulconfigSourceSet = createIsolatedSourceSet("moulconfig")
@@ -247,16 +263,16 @@ dependencies {
(explosiveEnhancementSourceSet.modImplementationConfigurationName)(libs.explosiveenhancement)
modImplementation(libs.hypixelmodapi)
include(libs.hypixelmodapi.fabric)
- compileOnly(project(":javaplugin"))
- annotationProcessor(project(":javaplugin"))
- implementation("com.google.auto.service:auto-service-annotations:1.1.1")
+ compileOnly(projects.javaplugin)
+ annotationProcessor(projects.javaplugin)
+ nonModImplentation("com.google.auto.service:auto-service-annotations:1.1.1")
ksp("dev.zacsweers.autoservice:auto-service-ksp:1.2.0")
include(libs.manninghamMills)
include(libs.moulconfig)
annotationProcessor(libs.mixinextras)
- implementation(libs.mixinextras)
+ nonModImplentation(libs.mixinextras)
include(libs.mixinextras)
nonModImplentation(libs.nealisp)
@@ -264,7 +280,6 @@ dependencies {
modCompileOnly(libs.fabric.api)
modRuntimeOnly(libs.fabric.api.deprecated)
- modApi(libs.architectury)
modCompileOnly(libs.jarvis.api)
include(libs.jarvis.fabric)
@@ -272,6 +287,7 @@ dependencies {
(wildfireGenderSourceSet.implementationConfigurationName)(customTexturesSourceSet.output)
(configuredSourceSet.modImplementationConfigurationName)(libs.configured)
(sodiumSourceSet.modImplementationConfigurationName)(libs.sodium)
+ (jadeSourceSet.modImplementationConfigurationName)(libs.jade)
(citResewnSourceSet.modImplementationConfigurationName)(
innerJarsOf("citresewn", dependencies.create(libs.citresewn.get()))
@@ -280,10 +296,8 @@ dependencies {
(yaclSourceSet.modImplementationConfigurationName)(libs.yacl)
// Actual dependencies
- (reiSourceSet.modImplementationConfigurationName)(libs.rei.api) {
- exclude(module = "architectury")
- exclude(module = "architectury-fabric")
- }
+ (reiSourceSet.modImplementationConfigurationName)(libs.rei.api)
+ (reiSourceSet.modRuntimeOnlyConfigurationName)(libs.rei.fabric)
nonModImplentation(libs.repoparser)
shadowMe(libs.repoparser)
fun ktor(mod: String) = "io.ktor:ktor-$mod-jvm:${libs.versions.ktor.get()}"
@@ -306,11 +320,11 @@ dependencies {
}
- testImplementation("io.kotest:kotest-runner-junit5:6.0.0.M1")
- testAgent(project(":testagent", configuration = "shadow"))
+ testImplementation("net.fabricmc:fabric-loader-junit:${libs.versions.fabric.loader.get()}")
+ testAgent(files(tasks.getByPath(":testagent:jar")))
- implementation(project(":symbols"))
- ksp(project(":symbols"))
+ implementation(projects.symbols)
+ ksp(projects.symbols)
}
loom {
@@ -321,11 +335,13 @@ loom {
configureEach {
property("fabric.log.level", "info")
property("firmament.debug", "true")
- property("firmament.classroots",
- compatSourceSets.joinToString(File.pathSeparator) {
- File(it.output.classesDirs.asPath).absolutePath
- })
+ property(
+ "firmament.classroots",
+ compatSourceSets.joinToString(File.pathSeparator) {
+ File(it.output.classesDirs.asPath).absolutePath
+ })
property("mixin.debug.export", "true")
+ property("mixin.debug", "true")
parseEnvFile(file(".env")).forEach { (t, u) ->
environmentVariable(t, u)
@@ -337,9 +353,9 @@ loom {
named("client") {
property("devauth.enabled", "true")
vmArg("-ea")
- vmArg("-XX:+AllowEnhancedClassRedefinition")
- vmArg("-XX:HotswapAgent=external")
- vmArg("-javaagent:${hotswap.resolve().single().absolutePath}")
+// vmArg("-XX:+AllowEnhancedClassRedefinition")
+// vmArg("-XX:HotswapAgent=external")
+// vmArg("-javaagent:${hotswap.resolve().single().absolutePath}")
}
}
}
@@ -358,12 +374,16 @@ val updateTestRepo by tasks.registering {
doLast {
val propertiesFile = rootProject.file("gradle.properties")
val json =
- Gson().fromJson(uri("https://api.github.com/repos/NotEnoughUpdates/NotEnoughUpdates-REPO/branches/master")
- .toURL().readText(), JsonObject::class.java)
+ Gson().fromJson(
+ uri("https://api.github.com/repos/NotEnoughUpdates/NotEnoughUpdates-REPO/branches/master")
+ .toURL().readText(), JsonObject::class.java
+ )
val latestSha = json["commit"].asJsonObject["sha"].asString
var text = propertiesFile.readText()
- text = text.replace("firmament\\.compiletimerepohash=[^\n]*".toRegex(),
- "firmament.compiletimerepohash=$latestSha")
+ text = text.replace(
+ "firmament\\.compiletimerepohash=[^\n]*".toRegex(),
+ "firmament.compiletimerepohash=$latestSha"
+ )
propertiesFile.writeText(text)
}
}
@@ -377,8 +397,10 @@ tasks.test {
doFirst {
wd.mkdirs()
wd.resolve("config").deleteRecursively()
- systemProperty("firmament.testrepo",
- downloadTestRepo.flatMap { it.outputDirectory.asFile }.map { it.absolutePath }.get())
+ systemProperty(
+ "firmament.testrepo",
+ downloadTestRepo.flatMap { it.outputDirectory.asFile }.map { it.absolutePath }.get()
+ )
jvmArgs("-javaagent:${testAgent.singleFile.absolutePath}")
}
systemProperty("jdk.attach.allowAttachSelf", "true")
@@ -396,13 +418,15 @@ tasks.withType<JavaCompile> {
this.targetCompatibility = "21"
options.encoding = "UTF-8"
val module = "ALL-UNNAMED"
- options.forkOptions.jvmArgs!!.addAll(listOf(
- "--add-exports=jdk.compiler/com.sun.tools.javac.util=$module",
- "--add-exports=jdk.compiler/com.sun.tools.javac.comp=$module",
- "--add-exports=jdk.compiler/com.sun.tools.javac.tree=$module",
- "--add-exports=jdk.compiler/com.sun.tools.javac.api=$module",
- "--add-exports=jdk.compiler/com.sun.tools.javac.code=$module",
- ))
+ options.forkOptions.jvmArgs!!.addAll(
+ listOf(
+ "--add-exports=jdk.compiler/com.sun.tools.javac.util=$module",
+ "--add-exports=jdk.compiler/com.sun.tools.javac.comp=$module",
+ "--add-exports=jdk.compiler/com.sun.tools.javac.tree=$module",
+ "--add-exports=jdk.compiler/com.sun.tools.javac.api=$module",
+ "--add-exports=jdk.compiler/com.sun.tools.javac.code=$module",
+ )
+ )
options.isFork = true
afterEvaluate {
options.compilerArgs.add("-Xplugin:IntermediaryNameReplacement mappingFile=${LoomGradleExtension.get(project).mappingsFile.absolutePath} sourceNs=named")
@@ -434,6 +458,7 @@ tasks.processResources {
"version" to project.version.toString(),
"minecraft_version" to libs.versions.minecraft.get(),
"fabric_kotlin_version" to libs.versions.fabric.kotlin.get(),
+ "fabric_api_version" to libs.versions.fabric.api.get(),
"rei_version" to libs.versions.rei.get()
)
replacements.forEach { (key, value) -> inputs.property(key, value) }
@@ -450,12 +475,18 @@ tasks.processResources {
tasks.scanLicenses {
scanConfiguration(nonModImplentation)
scanConfiguration(configurations.modCompileClasspath.get())
+ compatSourceSets.forEach {
+ scanConfiguration(it.modImplementationConfigurationName.get())
+ }
outputFile.set(layout.buildDirectory.file("LICENSES-FIRMAMENT.json"))
licenseFormatter.set(moe.nea.licenseextractificator.JsonLicenseFormatter())
}
-tasks.create("printAllLicenses", LicenseDiscoveryTask::class.java, licensing).apply {
+tasks.register("printAllLicenses", LicenseDiscoveryTask::class.java, licensing).configure {
outputFile.set(layout.buildDirectory.file("LICENSES-FIRMAMENT.txt"))
licenseFormatter.set(moe.nea.licenseextractificator.TextLicenseFormatter())
+ compatSourceSets.forEach {
+ scanConfiguration(it.modImplementationConfigurationName.get())
+ }
scanConfiguration(nonModImplentation)
scanConfiguration(configurations.modCompileClasspath.get())
doLast {
@@ -463,6 +494,59 @@ tasks.create("printAllLicenses", LicenseDiscoveryTask::class.java, licensing).ap
}
outputs.upToDateWhen { false }
}
+fun patchRenderDoc(
+ javaLauncher: JavaLauncher,
+): JavaLauncher {
+ val wrappedJavaExecutable = javaLauncher.executablePath.asFile.absolutePath
+ require("\"" !in wrappedJavaExecutable)
+ val hashBytes = Hashing.sha256().hashString(wrappedJavaExecutable, StandardCharsets.UTF_8)
+ val hash = Base64.getUrlEncoder().encodeToString(hashBytes.asBytes())
+ .replace("=", "")
+ val wrapperJavaRoot = rootProject.layout.buildDirectory
+ .dir("binaries/renderdoc-wrapped-java/$hash/")
+ .get()
+ val isWindows = Os.isFamily(Os.FAMILY_WINDOWS)
+ val wrapperJavaExe =
+ if (isWindows) wrapperJavaRoot.file("java.cmd")
+ else wrapperJavaRoot.file("java")
+ return object : JavaLauncher {
+ override fun getMetadata(): JavaInstallationMetadata {
+ return object : JavaInstallationMetadata by javaLauncher.metadata {
+ override fun isCurrentJvm(): Boolean {
+ return false
+ }
+ }
+ }
+
+ override fun getExecutablePath(): RegularFile {
+ val fileF = wrapperJavaExe.asFile
+ if (!fileF.exists()) {
+ fileF.parentFile.mkdirs()
+ if (isWindows) {
+ fileF.writeText(
+ """
+ setlocal enableextensions
+ start "" renderdoccmd.exe capture --opt-hook-children --wait-for-exit --working-dir . "$wrappedJavaExecutable" %*
+ endlocal
+ """.trimIndent()
+ )
+ } else {
+ fileF.writeText(
+ """
+ #!/usr/bin/env bash
+ exec renderdoccmd capture --opt-hook-children --wait-for-exit --working-dir . "$wrappedJavaExecutable" "$@"
+ """.trimIndent()
+ )
+ fileF.setExecutable(true)
+ }
+ }
+ return wrapperJavaExe
+ }
+ }
+}
+tasks.runClient {
+ javaLauncher.set(javaToolchains.launcherFor(java.toolchain).map { patchRenderDoc(it) })
+}
tasks.withType<AbstractArchiveTask>().configureEach {
isPreserveFileTimestamps = false
diff --git a/docs/firmament_logo.webp b/docs/firmament_logo.webp
new file mode 100644
index 0000000..d70327a
--- /dev/null
+++ b/docs/firmament_logo.webp
Binary files differ
diff --git a/docs/firmament_logo.webp.license b/docs/firmament_logo.webp.license
new file mode 100644
index 0000000..8b77b1b
--- /dev/null
+++ b/docs/firmament_logo.webp.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2025 ic22487
+
+SPDX-License-Identifier: CC-BY-4.0
diff --git a/docs/firmament_logo_256.webp b/docs/firmament_logo_256.webp
new file mode 100644
index 0000000..2aba841
--- /dev/null
+++ b/docs/firmament_logo_256.webp
Binary files differ
diff --git a/docs/firmament_logo_256.webp.license b/docs/firmament_logo_256.webp.license
new file mode 100644
index 0000000..8b77b1b
--- /dev/null
+++ b/docs/firmament_logo_256.webp.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2025 ic22487
+
+SPDX-License-Identifier: CC-BY-4.0
diff --git a/docs/firmament_logo_256_nobg.webp b/docs/firmament_logo_256_nobg.webp
new file mode 100644
index 0000000..c557fca
--- /dev/null
+++ b/docs/firmament_logo_256_nobg.webp
Binary files differ
diff --git a/docs/firmament_logo_256_nobg.webp.license b/docs/firmament_logo_256_nobg.webp.license
new file mode 100644
index 0000000..8b77b1b
--- /dev/null
+++ b/docs/firmament_logo_256_nobg.webp.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2025 ic22487
+
+SPDX-License-Identifier: CC-BY-4.0
diff --git a/docs/firmament_logo_256_trans.webp b/docs/firmament_logo_256_trans.webp
new file mode 100644
index 0000000..6e05b83
--- /dev/null
+++ b/docs/firmament_logo_256_trans.webp
Binary files differ
diff --git a/docs/firmament_logo_256_trans.webp.license b/docs/firmament_logo_256_trans.webp.license
new file mode 100644
index 0000000..8b77b1b
--- /dev/null
+++ b/docs/firmament_logo_256_trans.webp.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2025 ic22487
+
+SPDX-License-Identifier: CC-BY-4.0
diff --git a/docs/firmament_logo_nobg.webp b/docs/firmament_logo_nobg.webp
new file mode 100644
index 0000000..9b76f3c
--- /dev/null
+++ b/docs/firmament_logo_nobg.webp
Binary files differ
diff --git a/docs/firmament_logo_nobg.webp.license b/docs/firmament_logo_nobg.webp.license
new file mode 100644
index 0000000..8b77b1b
--- /dev/null
+++ b/docs/firmament_logo_nobg.webp.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2025 ic22487
+
+SPDX-License-Identifier: CC-BY-4.0
diff --git a/docs/firmament_logo_trans.webp b/docs/firmament_logo_trans.webp
new file mode 100644
index 0000000..73a15f7
--- /dev/null
+++ b/docs/firmament_logo_trans.webp
Binary files differ
diff --git a/docs/firmament_logo_trans.webp.license b/docs/firmament_logo_trans.webp.license
new file mode 100644
index 0000000..8b77b1b
--- /dev/null
+++ b/docs/firmament_logo_trans.webp.license
@@ -0,0 +1,3 @@
+SPDX-FileCopyrightText: 2025 ic22487
+
+SPDX-License-Identifier: CC-BY-4.0
diff --git a/docs/release_script.sh b/docs/release_script.sh
index 43663b4..8d87a09 100755
--- a/docs/release_script.sh
+++ b/docs/release_script.sh
@@ -4,6 +4,8 @@
# SPDX-License-Identifier: GPL-3.0-or-later
#
# ARG_OPTIONAL_BOOLEAN([no-check],[n],[Skip checking preconditions, such as a clean git working directory])
+# ARG_OPTIONAL_BOOLEAN([no-test],[t],[Skip running gradle tests.])
+# ARG_OPTIONAL_BOOLEAN([dry],[d],[Dry run])
# ARG_HELP([Script to help creating releases])
# ARGBASH_GO()
# needed because of Argbash --> m4_ignore([
@@ -23,20 +25,24 @@ die()
begins_with_short_option()
{
- local first_option all_short_options='nh'
+ local first_option all_short_options='ntdh'
first_option="${1:0:1}"
test "$all_short_options" = "${all_short_options/$first_option/}" && return 1 || return 0
}
# THE DEFAULTS INITIALIZATION - OPTIONALS
_arg_no_check="off"
+_arg_no_test="off"
+_arg_dry="off"
print_help()
{
printf '%s\n' "Script to help creating releases"
- printf 'Usage: %s [-n|--(no-)no-check] [-h|--help]\n' "$0"
+ printf 'Usage: %s [-n|--(no-)no-check] [-t|--(no-)no-test] [-d|--(no-)dry] [-h|--help]\n' "$0"
printf '\t%s\n' "-n, --no-check, --no-no-check: Skip checking preconditions, such as a clean git working directory (off by default)"
+ printf '\t%s\n' "-t, --no-test, --no-no-test: Skip running gradle tests. (off by default)"
+ printf '\t%s\n' "-d, --dry, --no-dry: Dry run (off by default)"
printf '\t%s\n' "-h, --help: Prints help"
}
@@ -59,6 +65,30 @@ parse_commandline()
{ begins_with_short_option "$_next" && shift && set -- "-n" "-${_next}" "$@"; } || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option."
fi
;;
+ -t|--no-no-test|--no-test)
+ _arg_no_test="on"
+ test "${1:0:5}" = "--no-" && _arg_no_test="off"
+ ;;
+ -t*)
+ _arg_no_test="on"
+ _next="${_key##-t}"
+ if test -n "$_next" -a "$_next" != "$_key"
+ then
+ { begins_with_short_option "$_next" && shift && set -- "-t" "-${_next}" "$@"; } || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option."
+ fi
+ ;;
+ -d|--no-dry|--dry)
+ _arg_dry="on"
+ test "${1:0:5}" = "--no-" && _arg_dry="off"
+ ;;
+ -d*)
+ _arg_dry="on"
+ _next="${_key##-d}"
+ if test -n "$_next" -a "$_next" != "$_key"
+ then
+ { begins_with_short_option "$_next" && shift && set -- "-d" "-${_next}" "$@"; } || die "The short option '$_key' can't be decomposed to ${_key:0:2} and -${_key:2}, because ${_key:0:2} doesn't accept value and '-${_key:2:1}' doesn't correspond to a short option."
+ fi
+ ;;
-h|--help)
print_help
exit 0
@@ -129,19 +159,28 @@ fi
echo "Confirming new version as $newversion"
-echo Committing release commit
-git commit --allow-empty -m 'Prepare release '"$newversion"'
+if [ "$_arg_dry" == off ]; then
+ echo Committing release commit
+ git commit --allow-empty -m 'Prepare release '"$newversion"'
[no changelog]'
-echo Tagging release commit
-git tag "$newversion"
+ echo Tagging release commit
+ git tag "$newversion"
+fi
mkdir -p "$basedir/.gradle"
releasenotes="$basedir/.gradle/releasenotes.md"
+comparetag="$(
+if [ "$_arg_dry" == off ]; then
+ echo "$newversion"
+else
+ echo "HEAD"
+fi)"
+
echo Building release notes
-echo "**Full Changelog**: <https://github.com/nea89o/Firmament/compare/$oldversion...$newversion>" > "$releasenotes"
+echo "**Full Changelog**: <https://github.com/nea89o/Firmament/compare/$oldversion...$comparetag>" > "$releasenotes"
echo >> "$releasenotes"
-git log --pretty='- %s' --grep '[no changelog]' --invert-grep --fixed-strings "$oldversion..$newversion" | tac >> "$releasenotes"
+git log --pretty='- %s' --grep '[no changelog]' --invert-grep --fixed-strings "$oldversion..$comparetag" | tac >> "$releasenotes"
echo >> "$releasenotes"
echo Check Release notes:
@@ -153,22 +192,29 @@ read
echo Building JAR
"$basedir"/gradlew --stop
-"$basedir"/gradlew clean build
+if [ "$_arg_no_test" == off ]; then
+ echo Building and testing
+ "$basedir"/gradlew clean build
+else
+ echo Building without testing
+ "$basedir"/gradlew clean assemble
+fi
echo Release notes:
echo ----------------------------------------------
cat "$releasenotes"
echo ----------------------------------------------
-echo Pushing to github
-git push "$REMOTE" "HEAD" "$newversion"
-
-if command -v gh; then
- echo Creating github release
- (set -x; gh release create -t "Firmament $newversion" "$newversion" -F "$releasenotes" "$basedir/build/libs/Firmament-$newversion.jar")
-else
- echo Could not find github command utility. Opening github releases
- xdg-open "https://github.com/nea89o/firmament/releases/new"
+if [ "$_arg_dry" == off ]; then
+ echo Pushing to github
+ git push "$REMOTE" "HEAD" "$newversion"
+ if command -v gh; then
+ echo Creating github release
+ (set -x; gh release create -t "Firmament $newversion" "$newversion" -F "$releasenotes" "$basedir/build/libs/Firmament-$newversion.jar")
+ else
+ echo Could not find github command utility. Opening github releases
+ xdg-open "https://github.com/nea89o/firmament/releases/new"
+ fi
fi
echo Opening modrinth releases
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index 1c9c1c1..5450cb5 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -3,23 +3,24 @@
# SPDX-License-Identifier: CC0-1.0
[versions]
-minecraft = "1.21.4"
+minecraft = "1.21.5"
# Update from https://kotlinlang.org/
-kotlin = "2.1.0"
+kotlin = "2.1.20"
# Update from https://github.com/google/ksp/releases
-kotlin_ksp = "2.1.0-1.0.29"
+kotlin_ksp = "2.1.20-2.0.0"
# Update from https://linkie.shedaniel.me/dependencies?loader=fabric
-fabric_loader = "0.16.9"
-fabric_api = "0.114.0+1.21.4"
-yarn = "1.21.4+build.4"
-modmenu = "13.0.0-beta.1"
-architectury = "15.0.1"
-rei = "18.0.796"
+fabric_loader = "0.16.13"
+fabric_api = "0.119.9+1.21.5"
+yarn = "1.21.5+build.1"
+modmenu = "14.0.0-rc.2"
+architectury = "16.0.3"
+# Update from https://maven.architectury.dev/me/shedaniel/RoughlyEnoughItems-fabric/ (but is typically late)
+rei = "19.0.805"
# Update from https://maven.fabricmc.net/net/fabricmc/fabric-language-kotlin/
-fabric_kotlin = "1.13.0+kotlin.2.1.0"
+fabric_kotlin = "1.13.2+kotlin.2.1.20"
# Update from https://maven.architectury.dev/dev/architectury/loom/dev.architectury.loom.gradle.plugin/
loom = "1.7.414" # TODO: port back to architectury (and) 1.9.424
@@ -28,33 +29,36 @@ loom = "1.7.414" # TODO: port back to architectury (and) 1.9.424
qolify = "1.6.0-1.21.1"
# Update from https://modrinth.com/mod/sodium/versions?l=fabric
-sodium = "mc1.21.4-0.6.6-fabric"
+sodium = "mc1.21.5-0.6.13-fabric"
# Update from https://modrinth.com/mod/freecam/versions?l=fabric
-freecammod = "1.3.2+mc1.21.4"
+freecammod = "1.3.3+mc1.21.5"
# Update from https://modrinth.com/mod/no-chat-reports/versions?l=fabric
-ncr = "Fabric-1.21.4-v2.11.0"
+ncr = "Fabric-1.21.5-v2.12.0"
# Update from https://modrinth.com/mod/female-gender/versions?l=fabric
-femalegender = "4.3.3+1.21.4"
+femalegender = "4.3.4+1.21.5"
# Update from https://modrinth.com/mod/explosive-enhancement/versions?l=fabric
explosiveenhancement = "1.2.3-1.21.0"
# Update from https://modrinth.com/mod/not-enough-animations/versions?l=fabric
-notenoughanimations = "eZykTicT"
+notenoughanimations = "prj4BdjU"
# Update from https://modrinth.com/mod/cit-resewn/versions?l=fabric
citresewn = "1.2.0+1.21"
+# Update from https://modrinth.com/mod/jade/versions?l=fabric
+jade = "18.1.0+fabric"
+
devauth = "1.2.1"
-# Update from https://ktor.io/
-ktor = "3.0.3"
+# Update from https://ktor.io/docs/
+ktor = "3.1.2"
# Update from https://repo.nea.moe/#/releases/moe/nea/neurepoparser
-neurepoparser = "1.6.0"
+neurepoparser = "1.8.0"
# Update from https://github.com/HotswapProjects/HotswapAgent/releases
# TODO: bump to 2.0.1
@@ -67,12 +71,15 @@ jarvis = "1.1.4"
nealisp = "1.1.0"
# Update from https://github.com/NotEnoughUpdates/MoulConfig/tags
-moulconfig = "3.3.0"
+moulconfig = "3.11.0"
+
+# Update from https://repo.nea.moe/#/releases/moe/nea/mc-auto-translations/moe.nea.mc-auto-translations.gradle.plugin
+mcAutoTranslations = "0.3.0"
# Update from https://www.curseforge.com/minecraft/mc-mods/configured/files/all?page=1&pageSize=20
configured = "6023970"
-# Update from https://modrinth.com/mod/hypixel-mod-api/versions
+# Update from https://modrinth.com/mod/hypixel-mod-api/versions?l=fabric
hypixelmodapi = "1.0.1"
hypixelmodapi_fabric = "1.0.1+build.1+mc1.21"
@@ -81,16 +88,16 @@ manninghamMills = "2.4.1"
# Update from https://docs.isxander.dev/yet-another-config-lib/installing-yacl
# Nvm, they just don't update docs: https://modrinth.com/mod/yacl/versions?l=fabric
-yacl = "3.6.2+1.21.4-fabric"
+yacl = "3.6.6+1.21.5-fabric"
-# Update from https://maven.shedaniel.me/me/shedaniel/cloth/basic-math/0.6.1/
+# Update from https://maven.shedaniel.me/me/shedaniel/cloth/basic-math/
basicMath = "0.6.1"
# Update from https://mvnrepository.com/artifact/net.lenni0451.classtransform/core
-classtransform = "1.14.0"
+classtransform = "1.14.1"
# Update from https://mvnrepository.com/artifact/org.ow2.asm/asm/
-asm = "9.7.1"
+asm = "9.8"
[libraries]
minecraft = { module = "com.mojang:minecraft", version.ref = "minecraft" }
@@ -100,7 +107,7 @@ fabric_api_deprecated = { module = "net.fabricmc.fabric-api:fabric-api-deprecate
fabric_kotlin = { module = "net.fabricmc:fabric-language-kotlin", version.ref = "fabric_kotlin" }
architectury = { module = "dev.architectury:architectury", version.ref = "architectury" }
rei_api = { module = "me.shedaniel:RoughlyEnoughItems-api", version.ref = "rei" }
-moulconfig = { module = "org.notenoughupdates.moulconfig:modern", version.ref = "moulconfig" }
+moulconfig = { module = "org.notenoughupdates.moulconfig:modern-1.21.5", version.ref = "moulconfig" }
repoparser = { module = "moe.nea:neurepoparser", version.ref = "neurepoparser" }
mixinextras = { module = "io.github.llamalad7:mixinextras-fabric", version.ref = "mixinextras" }
jarvis_api = { module = "moe.nea.jarvis:jarvis-api", version.ref = "jarvis" }
@@ -125,6 +132,7 @@ sodium = { module = "maven.modrinth:sodium", version.ref = "sodium" }
freecammod = { module = "maven.modrinth:freecam", version.ref = "freecammod" }
citresewn = { module = "maven.modrinth:cit-resewn", version.ref = "citresewn" }
femalegender = { module = "maven.modrinth:female-gender", version.ref = "femalegender" }
+jade = { module = "maven.modrinth:jade", version.ref = "jade" }
yacl = { module = "dev.isxander:yet-another-config-lib", version.ref = "yacl" }
basicMath = { module = "me.shedaniel.cloth:basic-math", version.ref = "basicMath" }
@@ -135,7 +143,7 @@ asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
[bundles]
runtime_required = [
- "rei_fabric",
+ # "rei_fabric",
# "notenoughanimations",
"hypixelmodapi_fabric",
]
@@ -143,8 +151,8 @@ runtime_optional = [
"devauth",
# "freecammod",
# "sodium",
- # "qolify",
- "ncr",
+ # "qolify",
+ # "ncr",
# "citresewn",
]
@@ -155,3 +163,4 @@ kotlin_plugin_powerassert = { id = "org.jetbrains.kotlin.plugin.power-assert", v
kotlin_plugin_ksp = { id = "com.google.devtools.ksp", version.ref = "kotlin_ksp" }
loom = { id = "dev.architectury.loom", version.ref = "loom" }
shadow = { id = "com.github.johnrengelman.shadow", version = "8.1.1" }
+mcAutoTranslations = { id = "moe.nea.mc-auto-translations", version.ref = "mcAutoTranslations" }
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
index 6e2ced2..4cdd0fb 100644
--- a/gradle/wrapper/gradle-wrapper.properties
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -4,6 +4,6 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
-distributionUrl=https\://services.gradle.org/distributions/gradle-8.11.1-bin.zip
+distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
diff --git a/settings.gradle.kts b/settings.gradle.kts
index aef468a..8a5dc47 100644
--- a/settings.gradle.kts
+++ b/settings.gradle.kts
@@ -35,6 +35,7 @@ pluginManagement {
rootProject.name = "Firmament"
+enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
include("symbols")
include("javaplugin")
include("testagent")
diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/Compat.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/Compat.kt
new file mode 100644
index 0000000..d1cfef4
--- /dev/null
+++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/Compat.kt
@@ -0,0 +1,12 @@
+package moe.nea.firmament.compat.jade
+
+import net.fabricmc.loader.api.FabricLoader
+import moe.nea.firmament.util.compatloader.CompatMeta
+import moe.nea.firmament.util.compatloader.ICompatMeta
+
+@CompatMeta
+object Compat : ICompatMeta {
+ override fun shouldLoad(): Boolean {
+ return FabricLoader.getInstance().isModLoaded("jade")
+ }
+}
diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomFakeBlockProvider.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomFakeBlockProvider.kt
new file mode 100644
index 0000000..53e3255
--- /dev/null
+++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomFakeBlockProvider.kt
@@ -0,0 +1,41 @@
+package moe.nea.firmament.compat.jade
+
+import snownee.jade.api.Accessor
+import snownee.jade.api.BlockAccessor
+import snownee.jade.api.IWailaClientRegistration
+import snownee.jade.api.callback.JadeRayTraceCallback
+import net.minecraft.util.hit.HitResult
+import moe.nea.firmament.repo.MiningRepoData
+import moe.nea.firmament.util.mc.FirmamentDataComponentTypes
+
+class CustomFakeBlockProvider(val registration: IWailaClientRegistration) : JadeRayTraceCallback {
+
+ override fun onRayTrace(
+ hitResult: HitResult,
+ accessor: Accessor<*>?,
+ originalAccessor: Accessor<*>?
+ ): Accessor<*>? {
+ if (!JadeIntegration.TConfig.blockDetection) return accessor
+ if (accessor !is BlockAccessor) return accessor
+ val customBlock = JadeIntegration.customBlocks[accessor.block]
+ if (customBlock == null) return accessor
+ return registration.blockAccessor()
+ .from(accessor)
+ .fakeBlock(customBlock.getDisplayItem(accessor.block))
+ .build()
+ }
+
+ companion object {
+ @JvmStatic
+ fun hasCustomBlock(accessor: BlockAccessor): Boolean {
+ return getCustomBlock(accessor) != null
+ }
+
+ @JvmStatic
+ fun getCustomBlock(accessor: BlockAccessor): MiningRepoData.CustomMiningBlock? {
+ if (!accessor.isFakeBlock) return null
+ val item = accessor.fakeBlock
+ return item.get(FirmamentDataComponentTypes.CUSTOM_MINING_BLOCK_DATA)
+ }
+ }
+}
diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomMiningHardnessProvider.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomMiningHardnessProvider.kt
new file mode 100644
index 0000000..29fecd2
--- /dev/null
+++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/CustomMiningHardnessProvider.kt
@@ -0,0 +1,97 @@
+package moe.nea.firmament.compat.jade
+
+import snownee.jade.api.BlockAccessor
+import snownee.jade.api.IBlockComponentProvider
+import snownee.jade.api.ITooltip
+import snownee.jade.api.config.IPluginConfig
+import kotlin.time.DurationUnit
+import net.minecraft.block.BlockState
+import net.minecraft.util.Identifier
+import net.minecraft.util.math.BlockPos
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.TickEvent
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.TimeMark
+import moe.nea.firmament.util.tr
+
+object CustomMiningHardnessProvider : IBlockComponentProvider {
+
+ override fun appendTooltip(
+ tooltip: ITooltip,
+ block: BlockAccessor,
+ config: IPluginConfig?
+ ) {
+ val customBlock = CustomFakeBlockProvider.getCustomBlock(block) ?: return
+ if (customBlock.breakingPower > 0)
+ tooltip.add(tr("firmament.jade.breaking_power", "Required Breaking Power: ${customBlock.breakingPower}"))
+ }
+
+ override fun getUid(): Identifier =
+ Firmament.identifier("custom_mining_hardness")
+
+ data class BreakingInfo(
+ val blockPos: BlockPos, val stage: Int,
+ val state: BlockState?,
+ val ts: TimeMark = TimeMark.now()
+ )
+
+ var previousBreakingInfo: BreakingInfo? = null
+ var currentBreakingInfo: BreakingInfo? = null
+
+ @Subscribe
+ fun clearInfoOnStopBreaking(event: TickEvent) {
+ val isBreakingBlock = MC.interactionManager?.isBreakingBlock ?: false
+ if (!isBreakingBlock) {
+ previousBreakingInfo = null
+ currentBreakingInfo = null
+ }
+ }
+
+ @JvmStatic
+ fun setBreakingInfo(blockPos: BlockPos, stage: Int) {
+ previousBreakingInfo = currentBreakingInfo
+ val state = MC.world?.getBlockState(blockPos)
+ if (previousBreakingInfo?.let { it.state != state || it.blockPos != blockPos } ?: false)
+ previousBreakingInfo == null
+ currentBreakingInfo = BreakingInfo(blockPos.toImmutable(), stage, state)
+ // For some reason hypixel initially sends a stage 10 packet, and then fixes it up with a stage 0 packet.
+ // Ignore the stage 10 packet if we dont have any previous packets for this block.
+ // This could in theory still have issues if someone perfectly stops breaking a block the tick it finishes and then does not break another block until it respawns, but i deem that to be too much of an edge case.
+ if (stage == 10 && previousBreakingInfo == null) {
+ previousBreakingInfo = null
+ currentBreakingInfo = null
+ }
+ }
+
+ @JvmStatic
+ fun replaceBreakProgress(original: Float): Float {
+ if (!JadeIntegration.TConfig.miningProgress) return original
+ if (!isOnMiningIsland()) return original
+ val pos = MC.interactionManager?.currentBreakingPos ?: return original
+ val info = currentBreakingInfo
+ if (info?.blockPos != pos || info.state != MC.world?.getBlockState(pos)) {
+ currentBreakingInfo = null
+ previousBreakingInfo = null
+ return original
+ }
+ // TODO: improve this interpolation to work across all stages, to alleviate some of the jittery bar.
+ // Maybe introduce a proper mining API that tracks the actual progress with some sort of FSM
+ val interpolatedStage = previousBreakingInfo?.let { prev ->
+ val timeBetweenTicks = (info.ts - prev.ts).toDouble(DurationUnit.SECONDS)
+ val stagesPerUpdate = (info.stage - prev.stage).toDouble()
+ if (stagesPerUpdate < 1) return@let null
+ val stagesPerSecond = stagesPerUpdate / timeBetweenTicks
+ info.stage + (info.ts.passedTime().toDouble(DurationUnit.SECONDS) * stagesPerSecond)
+ .coerceAtMost(stagesPerUpdate)
+ }?.toFloat()
+ val stage = interpolatedStage ?: info.stage.toFloat()
+ return stage / 10F
+ }
+
+ @JvmStatic
+ fun replaceBlockBreakSpeed(original: Float): Float {
+ if (isOnMiningIsland()) return 0F
+ return original
+ }
+}
diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt
new file mode 100644
index 0000000..10bff1b
--- /dev/null
+++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt
@@ -0,0 +1,77 @@
+package moe.nea.firmament.compat.jade
+
+import java.util.Optional
+import java.util.function.UnaryOperator
+import snownee.jade.api.BlockAccessor
+import snownee.jade.api.IBlockComponentProvider
+import snownee.jade.api.ITooltip
+import snownee.jade.api.JadeIds
+import snownee.jade.api.config.IPluginConfig
+import snownee.jade.api.theme.IThemeHelper
+import snownee.jade.api.ui.IElement
+import snownee.jade.api.ui.IElementHelper
+import snownee.jade.impl.ui.ItemStackElement
+import snownee.jade.impl.ui.TextElement
+import kotlin.jvm.optionals.getOrDefault
+import net.minecraft.text.Text
+import net.minecraft.util.Identifier
+import net.minecraft.util.math.Vec2f
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.util.MC
+
+class DrillToolProvider : IBlockComponentProvider {
+ @OptIn(ExpensiveItemCacheApi::class)
+ override fun appendTooltip(
+ tooltip: ITooltip,
+ accessor: BlockAccessor,
+ p2: IPluginConfig?
+ ) {
+ val customBlock = CustomFakeBlockProvider.getCustomBlock(accessor) ?: return
+ val tool = RepoManager.miningData.getToolsThatCanBreak(customBlock.breakingPower).firstOrNull()
+ ?.asImmutableItemStack() ?: return
+ tooltip.replace(JadeIds.MC_HARVEST_TOOL, UnaryOperator { elements ->
+ elements.map { inner ->
+ val lastItemIndex = inner.indexOfLast { it is ItemStackElement }
+ if (lastItemIndex < 0) return@map inner
+ val innerMut = inner.toMutableList()
+ val harvestIndicator = innerMut.indexOfLast {
+ it is TextElement && it.cachedSize == Vec2f.ZERO && it.text.visit {
+ if (it.isEmpty()) Optional.empty() else Optional.of(true)
+ }.getOrDefault(false)
+ }
+ val canHarvest = SBItemStack(MC.stackInHand).breakingPower >= customBlock.breakingPower
+ val lastItem = innerMut[lastItemIndex] as ItemStackElement
+ if (harvestIndicator < 0) {
+ innerMut.add(lastItemIndex + 1, canHarvestIndicator(canHarvest, lastItem.alignment))
+ } else {
+ innerMut.set(harvestIndicator, canHarvestIndicator(canHarvest, lastItem.alignment))
+ }
+ innerMut.set(lastItemIndex, IElementHelper.get()
+ .item(tool, 0.75f)
+ .translate(lastItem.translation)
+ .size(lastItem.size)
+ .message(null)
+ .align(lastItem.alignment))
+ innerMut.subList(0, lastItemIndex - 1).removeIf { it is ItemStackElement }
+ innerMut
+ }
+ })
+ }
+
+ fun canHarvestIndicator(canHarvest: Boolean, align: IElement.Align): IElement {
+ val t = IThemeHelper.get()
+ val text = if (canHarvest) t.success(CHECK) else t.danger(X)
+ return IElementHelper.get().text(text)
+ .scale(0.75F).zOffset(800).size(Vec2f.ZERO).translate(Vec2f(-3F, 3.25F)).align(align)
+ }
+
+ private val CHECK: Text = Text.literal("✔")
+ private val X: Text = Text.literal("✕")
+
+ override fun getUid(): Identifier {
+ return Firmament.identifier("toolprovider")
+ }
+}
diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/FirmamentJadePlugin.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/FirmamentJadePlugin.kt
new file mode 100644
index 0000000..51e2453
--- /dev/null
+++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/FirmamentJadePlugin.kt
@@ -0,0 +1,21 @@
+package moe.nea.firmament.compat.jade
+
+import snownee.jade.api.IWailaClientRegistration
+import snownee.jade.api.IWailaCommonRegistration
+import snownee.jade.api.IWailaPlugin
+import snownee.jade.api.WailaPlugin
+import net.minecraft.block.Block
+import moe.nea.firmament.Firmament
+
+@WailaPlugin
+class FirmamentJadePlugin : IWailaPlugin {
+ override fun register(registration: IWailaCommonRegistration) {
+ Firmament.logger.debug("Registering Jade integration...")
+ }
+
+ override fun registerClient(registration: IWailaClientRegistration) {
+ registration.registerBlockComponent(CustomMiningHardnessProvider, Block::class.java)
+ registration.registerBlockComponent(DrillToolProvider(), Block::class.java)
+ registration.addRayTraceCallback(CustomFakeBlockProvider(registration))
+ }
+}
diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/JadeIntegration.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/JadeIntegration.kt
new file mode 100644
index 0000000..d411c26
--- /dev/null
+++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/JadeIntegration.kt
@@ -0,0 +1,50 @@
+package moe.nea.firmament.compat.jade
+
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.SkyblockServerUpdateEvent
+import moe.nea.firmament.repo.MiningRepoData
+import moe.nea.firmament.repo.RepoManager
+import moe.nea.firmament.util.ErrorUtil
+import net.minecraft.block.Block
+import moe.nea.firmament.events.ReloadRegistrationEvent
+import moe.nea.firmament.gui.config.ManagedConfig
+
+object JadeIntegration {
+ object TConfig : ManagedConfig("jade-integration", Category.INTEGRATIONS) {
+ val miningProgress by toggle("progress") { true }
+ val blockDetection by toggle("blocks") { true }
+ }
+
+ var customBlocks: Map<Block, MiningRepoData.CustomMiningBlock> = mapOf()
+
+ fun refreshBlockInfo() {
+ if (!isOnMiningIsland()) {
+ customBlocks = mapOf()
+ return
+ }
+ val blocks = RepoManager.miningData.customMiningBlocks
+ .flatMap { customBlock ->
+ // TODO: add a lifted helper method for this
+ customBlock.blocks189.filter { it.isCurrentlyActive }
+ .mapNotNull { it.block }
+ .map { customBlock to it }
+ }
+ .groupBy { it.second }
+ customBlocks = blocks.mapNotNull { (block, customBlocks) ->
+ val singleMatch =
+ ErrorUtil.notNullOr(customBlocks.singleOrNull()?.first,
+ "Two custom blocks both want to supply custom mining behaviour for $block.") { return@mapNotNull null }
+ block to singleMatch
+ }.toMap()
+ }
+
+ @Subscribe
+ fun onRepoReload(event: ReloadRegistrationEvent) {
+ event.repo.registerReloadListener { refreshBlockInfo() }
+ }
+
+ @Subscribe
+ fun onWorldSwap(event: SkyblockServerUpdateEvent) {
+ refreshBlockInfo()
+ }
+}
diff --git a/src/compat/jade/java/moe/nea/firmament/compat/jade/utils.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/utils.kt
new file mode 100644
index 0000000..364dc02
--- /dev/null
+++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/utils.kt
@@ -0,0 +1,6 @@
+package moe.nea.firmament.compat.jade
+
+import moe.nea.firmament.util.SBData
+
+fun isOnMiningIsland(): Boolean =
+ SBData.skyblockLocation?.hasCustomMining ?: false
diff --git a/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/ElementAccessor.java b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/ElementAccessor.java
new file mode 100644
index 0000000..1b58e3c
--- /dev/null
+++ b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/ElementAccessor.java
@@ -0,0 +1,12 @@
+package moe.nea.firmament.mixins.compat.jade;
+
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+import snownee.jade.api.ui.Element;
+import snownee.jade.api.ui.IElement;
+
+@Mixin(Element.class)
+public interface ElementAccessor {
+ @Accessor("align")
+ IElement.Align getAlign_firmament();
+}
diff --git a/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/EnforceToolDisplayForCustomBlocksInHarvestToolProvider.java b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/EnforceToolDisplayForCustomBlocksInHarvestToolProvider.java
new file mode 100644
index 0000000..3677d01
--- /dev/null
+++ b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/EnforceToolDisplayForCustomBlocksInHarvestToolProvider.java
@@ -0,0 +1,33 @@
+package moe.nea.firmament.mixins.compat.jade;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import com.llamalad7.mixinextras.sugar.Local;
+import moe.nea.firmament.compat.jade.CustomFakeBlockProvider;
+import net.minecraft.block.Blocks;
+import net.minecraft.item.ItemStack;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import snownee.jade.addon.harvest.HarvestToolProvider;
+import snownee.jade.api.BlockAccessor;
+
+import java.util.List;
+
+@Mixin(HarvestToolProvider.class)
+public class EnforceToolDisplayForCustomBlocksInHarvestToolProvider {
+ @ModifyExpressionValue(method = "getText", at = @At(value = "INVOKE", target = "Lnet/minecraft/block/BlockState;isToolRequired()Z"))
+ private boolean overwriteRequiresTool(boolean original, @Local(argsOnly = true) BlockAccessor accessor) {
+ if (CustomFakeBlockProvider.hasCustomBlock(accessor))
+ return true;
+ return original;
+ }
+
+ private static final List<ItemStack> REPLACEABLE_TOOL = List.of(new ItemStack(Blocks.ENCHANTING_TABLE));
+
+ @ModifyExpressionValue(method = "getText", at = @At(value = "INVOKE", target = "Lcom/google/common/cache/Cache;get(Ljava/lang/Object;Ljava/util/concurrent/Callable;)Ljava/lang/Object;"))
+ private Object overwriteAvailableTools(Object original, @Local(argsOnly = true) BlockAccessor accessor) {
+ var orig = (List<ItemStack>) original;
+ if (orig.isEmpty() && CustomFakeBlockProvider.hasCustomBlock(accessor))
+ return REPLACEABLE_TOOL;
+ return orig;
+ }
+}
diff --git a/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/OnUpdateBreakProgress.java b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/OnUpdateBreakProgress.java
new file mode 100644
index 0000000..7d71ae8
--- /dev/null
+++ b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/OnUpdateBreakProgress.java
@@ -0,0 +1,22 @@
+package moe.nea.firmament.mixins.compat.jade;
+
+import moe.nea.firmament.compat.jade.CustomMiningHardnessProvider;
+import moe.nea.firmament.util.MC;
+import net.minecraft.client.render.WorldRenderer;
+import net.minecraft.util.math.BlockPos;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+import java.util.Objects;
+
+@Mixin(WorldRenderer.class)
+public class OnUpdateBreakProgress {
+ @Inject(method = "setBlockBreakingInfo", at = @At("HEAD"))
+ private void replaceBreakProgress(int entityId, BlockPos pos, int stage, CallbackInfo ci) {
+ if (entityId == 0 && null != MC.INSTANCE.getInteractionManager() && Objects.equals(MC.INSTANCE.getInteractionManager().currentBreakingPos, pos)) {
+ CustomMiningHardnessProvider.setBreakingInfo(pos, stage);
+ }
+ }
+}
diff --git a/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/PatchBreakingBarSpeedJade.java b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/PatchBreakingBarSpeedJade.java
new file mode 100644
index 0000000..203f7e4
--- /dev/null
+++ b/src/compat/jade/java/moe/nea/firmament/mixins/compat/jade/PatchBreakingBarSpeedJade.java
@@ -0,0 +1,25 @@
+package moe.nea.firmament.mixins.compat.jade;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import moe.nea.firmament.compat.jade.CustomMiningHardnessProvider;
+import org.objectweb.asm.Opcodes;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import snownee.jade.JadeClient;
+
+@Mixin(JadeClient.class)
+public class PatchBreakingBarSpeedJade {
+ @ModifyExpressionValue(
+ method = "drawBreakingProgress",
+ at = @At(value = "FIELD", target = "Lnet/minecraft/client/network/ClientPlayerInteractionManager;currentBreakingProgress:F", opcode = Opcodes.GETFIELD)
+ )
+ private static float replaceBlockBreakingProgress(float original) {
+ return CustomMiningHardnessProvider.replaceBreakProgress(original);
+ }
+
+ @ModifyExpressionValue(method = "drawBreakingProgress",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/block/BlockState;calcBlockBreakingDelta(Lnet/minecraft/entity/player/PlayerEntity;Lnet/minecraft/world/BlockView;Lnet/minecraft/util/math/BlockPos;)F"))
+ private static float replacePlayerSpecificBreakingProgress(float original) {
+ return CustomMiningHardnessProvider.replaceBlockBreakSpeed(original);
+ }
+}
diff --git a/src/compat/modmenu/java/moe/nea/firmament/compat/modmenu/FirmamentModMenuPlugin.kt b/src/compat/modmenu/java/moe/nea/firmament/compat/modmenu/FirmamentModMenuPlugin.kt
index b734e2c..ff58c20 100644
--- a/src/compat/modmenu/java/moe/nea/firmament/compat/modmenu/FirmamentModMenuPlugin.kt
+++ b/src/compat/modmenu/java/moe/nea/firmament/compat/modmenu/FirmamentModMenuPlugin.kt
@@ -6,6 +6,6 @@ import moe.nea.firmament.gui.config.AllConfigsGui
class FirmamentModMenuPlugin : ModMenuApi {
override fun getModConfigScreenFactory(): ConfigScreenFactory<*> {
- return ConfigScreenFactory { AllConfigsGui.makeScreen(it) }
+ return ConfigScreenFactory { AllConfigsGui.makeScreen(parent = it) }
}
}
diff --git a/src/compat/moulconfig/java/MCConfigEditorIntegration.kt b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt
index dec2559..2d71aa0 100644
--- a/src/compat/moulconfig/java/MCConfigEditorIntegration.kt
+++ b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt
@@ -35,6 +35,7 @@ import net.minecraft.util.Identifier
import net.minecraft.util.StringIdentifiable
import net.minecraft.util.Util
import moe.nea.firmament.Firmament
+import moe.nea.firmament.gui.config.AllConfigsGui
import moe.nea.firmament.gui.config.BooleanHandler
import moe.nea.firmament.gui.config.ChoiceHandler
import moe.nea.firmament.gui.config.ClickHandler
@@ -96,25 +97,27 @@ class MCConfigEditorIntegration : FirmamentConfigScreenProvider {
val mappedSetter = setter.xmap(fromT, toT)
private val delegateI by lazy {
- wrapComponent(RowComponent(
- AlignComponent(
- TextComponent(
- IMinecraft.instance.defaultFontRenderer,
- { formatter(setter.get()) },
- 25,
- TextComponent.TextAlignment.CENTER, false, false
+ wrapComponent(
+ RowComponent(
+ AlignComponent(
+ TextComponent(
+ IMinecraft.instance.defaultFontRenderer,
+ { formatter(setter.get()) },
+ 25,
+ TextComponent.TextAlignment.CENTER, false, false
+ ),
+ GetSetter.constant(HorizontalAlign.CENTER),
+ GetSetter.constant(VerticalAlign.CENTER)
),
- GetSetter.constant(HorizontalAlign.CENTER),
- GetSetter.constant(VerticalAlign.CENTER)
- ),
- SliderComponent(
- mappedSetter,
- fromT(minValue),
- fromT(maxValue),
- minStep,
- 40
+ SliderComponent(
+ mappedSetter,
+ fromT(minValue),
+ fromT(maxValue),
+ minStep,
+ 40
+ )
)
- ))
+ )
}
}
@@ -302,100 +305,110 @@ class MCConfigEditorIntegration : FirmamentConfigScreenProvider {
}
}
- override fun open(parent: Screen?): Screen {
- val configObject = object : Config() {
- override fun saveNow() {
- ManagedConfig.allManagedConfigs.getAll().forEach { it.save() }
- }
+ val configObject = object : Config() {
+ override fun saveNow() {
+ ManagedConfig.allManagedConfigs.getAll().forEach { it.save() }
+ }
- override fun shouldAutoFocusSearchbar(): Boolean {
- return true
- }
+ override fun shouldAutoFocusSearchbar(): Boolean {
+ return true
+ }
+
+ override fun getTitle(): String {
+ return "Firmament ${Firmament.version.friendlyString}"
+ }
+
+ @Deprecated("Deprecated in java")
+ override fun executeRunnable(runnableId: Int) {
+ if (runnableId >= 0)
+ ErrorUtil.softError("Executed runnable $runnableId")
+ }
- override fun getTitle(): String {
- return "Firmament"
+ override fun getDescriptionBehaviour(option: ProcessedOption?): DescriptionRendereringBehaviour {
+ return DescriptionRendereringBehaviour.EXPAND_PANEL
+ }
+
+ fun mkSocial(name: String, identifier: Identifier, link: String) = object : Social() {
+ override fun onClick() {
+ Util.getOperatingSystem().open(URI(link))
}
- @Deprecated("Deprecated in java")
- override fun executeRunnable(runnableId: Int) {
- if (runnableId >= 0)
- ErrorUtil.softError("Executed runnable $runnableId")
+ override fun getTooltip(): List<String> {
+ return listOf(name)
}
- override fun getDescriptionBehaviour(option: ProcessedOption?): DescriptionRendereringBehaviour {
- return DescriptionRendereringBehaviour.EXPAND_PANEL
+ override fun getIcon(): MyResourceLocation {
+ return identifier.toMoulConfig()
}
+ }
- fun mkSocial(name: String, identifier: Identifier, link: String) = object : Social() {
- override fun onClick() {
- Util.getOperatingSystem().open(URI(link))
+ private val socials = listOf<Social>(
+ mkSocial(
+ "Discord", Firmament.identifier("textures/socials/discord.png"),
+ Firmament.modContainer.metadata.contact.get("discord").get()
+ ),
+ mkSocial(
+ "Source Code", Firmament.identifier("textures/socials/git.png"),
+ Firmament.modContainer.metadata.contact.get("sources").get()
+ ),
+ mkSocial(
+ "Modrinth", Firmament.identifier("textures/socials/modrinth.png"),
+ Firmament.modContainer.metadata.contact.get("modrinth").get()
+ ),
+ )
+
+ override fun getSocials(): List<Social> {
+ return socials
+ }
+ }
+ val categories = ManagedConfig.Category.entries.map {
+ val options = mutableListOf<ProcessedOptionFirm>()
+ var nextAccordionId = 720
+ it.configs.forEach { config ->
+ val categoryAccordionId = nextAccordionId++
+ options.add(object : ProcessedOptionFirm(-1, configObject) {
+ override fun getDebugDeclarationLocation(): String {
+ return "FirmamentConfig:${config.name}"
}
- override fun getTooltip(): List<String> {
- return listOf(name)
+ override fun getName(): String {
+ return config.labelText.string
}
- override fun getIcon(): MyResourceLocation {
- return identifier.toMoulConfig()
+ override fun getDescription(): String {
+ return "Missing description"
}
- }
-
- private val socials = listOf<Social>(
- mkSocial("Discord", Firmament.identifier("textures/socials/discord.png"),
- Firmament.modContainer.metadata.contact.get("discord").get()),
- mkSocial("Source Code", Firmament.identifier("textures/socials/git.png"),
- Firmament.modContainer.metadata.contact.get("sources").get()),
- mkSocial("Modrinth", Firmament.identifier("textures/socials/modrinth.png"),
- Firmament.modContainer.metadata.contact.get("modrinth").get()),
- )
-
- override fun getSocials(): List<Social> {
- return socials
- }
- }
- val categories = ManagedConfig.Category.entries.map {
- val options = mutableListOf<ProcessedOptionFirm>()
- var nextAccordionId = 720
- it.configs.forEach { config ->
- val categoryAccordionId = nextAccordionId++
- options.add(object : ProcessedOptionFirm(-1, configObject) {
- override fun getDebugDeclarationLocation(): String {
- return "FirmamentConfig:${config.name}"
- }
-
- override fun getName(): String {
- return config.labelText.string
- }
-
- override fun getDescription(): String {
- return "Missing description"
- }
- override fun get(): Any {
- return Unit
- }
+ override fun get(): Any {
+ return Unit
+ }
- override fun getType(): Type {
- return Unit.javaClass
- }
+ override fun getType(): Type {
+ return Unit.javaClass
+ }
- override fun set(value: Any?): Boolean {
- return false
- }
+ override fun set(value: Any?): Boolean {
+ return false
+ }
- override fun createEditor(): GuiOptionEditor {
- return GuiOptionEditorAccordion(this, categoryAccordionId)
- }
- })
- config.allOptions.forEach { (key, option) ->
- val processedOption = getHandler(option, categoryAccordionId, configObject)
- options.add(processedOption)
+ override fun createEditor(): GuiOptionEditor {
+ return GuiOptionEditorAccordion(this, categoryAccordionId)
}
+ })
+ config.allOptions.forEach { (key, option) ->
+ val processedOption = getHandler(option, categoryAccordionId, configObject)
+ options.add(processedOption)
}
-
- return@map ProcessedCategoryFirm(it, options)
}
+
+ return@map ProcessedCategoryFirm(it, options)
+ }
+
+ override fun open(search: String?, parent: Screen?): Screen {
val editor = MoulConfigEditor(ProcessedCategory.collect(categories), configObject)
+ if (search != null)
+ editor.search(search)
+ editor.setWide(AllConfigsGui.ConfigConfig.enableWideMC)
return GuiElementWrapper(editor) // TODO : add parent support
}
diff --git a/src/compat/moulconfig/java/ProcessedOptionFirm.kt b/src/compat/moulconfig/java/ProcessedOptionFirm.kt
index 4d0096c..6936048 100644
--- a/src/compat/moulconfig/java/ProcessedOptionFirm.kt
+++ b/src/compat/moulconfig/java/ProcessedOptionFirm.kt
@@ -10,6 +10,9 @@ abstract class ProcessedOptionFirm(
private val accordionId: Int,
private val config: Config
) : ProcessedOption {
+ override fun getPath(): String? {
+ return "nonsense"
+ }
lateinit var category: ProcessedCategoryFirm
override fun getAccordionId(): Int {
return accordionId
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/Compat.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/Compat.kt
new file mode 100644
index 0000000..9ab4d22
--- /dev/null
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/Compat.kt
@@ -0,0 +1,12 @@
+package moe.nea.firmament.compat.rei
+
+import net.fabricmc.loader.api.FabricLoader
+import moe.nea.firmament.util.compatloader.CompatMeta
+import moe.nea.firmament.util.compatloader.ICompatMeta
+
+@CompatMeta
+object Compat : ICompatMeta {
+ override fun shouldLoad(): Boolean {
+ return FabricLoader.getInstance().isModLoaded("roughlyenoughitems")
+ }
+}
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt
index 92f2cfc..89c3e19 100644
--- a/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/FirmamentReiPlugin.kt
@@ -1,5 +1,6 @@
package moe.nea.firmament.compat.rei
+import io.github.moulberry.repo.data.NEUCraftingRecipe
import me.shedaniel.rei.api.client.plugins.REIClientPlugin
import me.shedaniel.rei.api.client.registry.category.CategoryRegistry
import me.shedaniel.rei.api.client.registry.display.DisplayRegistry
@@ -19,17 +20,21 @@ import net.minecraft.item.ItemStack
import net.minecraft.text.Text
import net.minecraft.util.ActionResult
import net.minecraft.util.Identifier
-import moe.nea.firmament.compat.rei.recipes.SBCraftingRecipe
-import moe.nea.firmament.compat.rei.recipes.SBEssenceUpgradeRecipe
-import moe.nea.firmament.compat.rei.recipes.SBForgeRecipe
+import moe.nea.firmament.compat.rei.recipes.GenericREIRecipeCategory
import moe.nea.firmament.compat.rei.recipes.SBKatRecipe
import moe.nea.firmament.compat.rei.recipes.SBMobDropRecipe
+import moe.nea.firmament.compat.rei.recipes.SBRecipe
import moe.nea.firmament.compat.rei.recipes.SBReforgeRecipe
+import moe.nea.firmament.compat.rei.recipes.SBShopRecipe
import moe.nea.firmament.events.HandledScreenPushREIEvent
import moe.nea.firmament.features.inventory.CraftingOverlay
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlayScreen
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.repo.recipes.SBCraftingRecipeRenderer
+import moe.nea.firmament.repo.recipes.SBEssenceUpgradeRecipeRenderer
+import moe.nea.firmament.repo.recipes.SBForgeRecipeRenderer
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.guessRecipeId
@@ -40,6 +45,7 @@ import moe.nea.firmament.util.unformattedString
class FirmamentReiPlugin : REIClientPlugin {
companion object {
+ @ExpensiveItemCacheApi
fun EntryStack<SBItemStack>.asItemEntry(): EntryStack<ItemStack> {
return EntryStack.of(VanillaEntryTypes.ITEM, value.asImmutableItemStack())
}
@@ -47,23 +53,28 @@ class FirmamentReiPlugin : REIClientPlugin {
val SKYBLOCK_ITEM_TYPE_ID = Identifier.of("firmament", "skyblockitems")
}
+ @OptIn(ExpensiveItemCacheApi::class)
override fun registerTransferHandlers(registry: TransferHandlerRegistry) {
registry.register(TransferHandler { context ->
val screen = context.containerScreen
val display = context.display
- if (display !is SBCraftingRecipe) return@TransferHandler TransferHandler.Result.createNotApplicable()
- val neuItem = RepoManager.getNEUItem(SkyblockId(display.neuRecipe.output.itemId))
- ?: error("Could not find neu item ${display.neuRecipe.output.itemId} which is used in a recipe output")
+ if (display !is SBRecipe) return@TransferHandler TransferHandler.Result.createNotApplicable()
+ val recipe = display.neuRecipe
+ if (recipe !is NEUCraftingRecipe) return@TransferHandler TransferHandler.Result.createNotApplicable()
+ val neuItem = RepoManager.getNEUItem(SkyblockId(recipe.output.itemId))
+ ?: error("Could not find neu item ${recipe.output.itemId} which is used in a recipe output")
val useSuperCraft = context.isStackedCrafting || RepoManager.Config.alwaysSuperCraft
if (neuItem.isVanilla && useSuperCraft) return@TransferHandler TransferHandler.Result.createFailed(Text.translatable(
"firmament.recipe.novanilla"))
var shouldReturn = true
if (context.isActuallyCrafting && !useSuperCraft) {
- if (screen !is GenericContainerScreen || screen.title?.unformattedString != CraftingOverlay.CRAFTING_SCREEN_NAME) {
+ val craftingScreen = (screen as? GenericContainerScreen)
+ ?.takeIf { it.title?.unformattedString == CraftingOverlay.CRAFTING_SCREEN_NAME }
+ if (craftingScreen == null) {
MC.sendCommand("craft")
shouldReturn = false
}
- CraftingOverlay.setOverlay(screen as? GenericContainerScreen, display.neuRecipe)
+ CraftingOverlay.setOverlay(craftingScreen, recipe)
}
if (context.isActuallyCrafting && useSuperCraft) {
shouldReturn = false
@@ -74,13 +85,18 @@ class FirmamentReiPlugin : REIClientPlugin {
}
+ val generics = listOf<GenericREIRecipeCategory<*>>( // Order matters: The order in here is the order in which they show up in REI
+ GenericREIRecipeCategory(SBCraftingRecipeRenderer),
+ GenericREIRecipeCategory(SBForgeRecipeRenderer),
+ GenericREIRecipeCategory(SBEssenceUpgradeRecipeRenderer),
+ )
+
override fun registerCategories(registry: CategoryRegistry) {
- registry.add(SBCraftingRecipe.Category)
- registry.add(SBForgeRecipe.Category)
+ registry.add(generics)
registry.add(SBMobDropRecipe.Category)
registry.add(SBKatRecipe.Category)
registry.add(SBReforgeRecipe.Category)
- registry.add(SBEssenceUpgradeRecipe.Category)
+ registry.add(SBShopRecipe.Category)
}
override fun registerExclusionZones(zones: ExclusionZones) {
@@ -89,26 +105,22 @@ class FirmamentReiPlugin : REIClientPlugin {
}
override fun registerDisplays(registry: DisplayRegistry) {
- registry.registerDisplayGenerator(
- SBCraftingRecipe.Category.catIdentifier,
- SkyblockCraftingRecipeDynamicGenerator)
+ generics.forEach {
+ it.registerDynamicGenerator(registry)
+ }
registry.registerDisplayGenerator(
SBReforgeRecipe.catIdentifier,
SBReforgeRecipe.DynamicGenerator
)
registry.registerDisplayGenerator(
- SBForgeRecipe.Category.categoryIdentifier,
- SkyblockForgeRecipeDynamicGenerator)
- registry.registerDisplayGenerator(
SBMobDropRecipe.Category.categoryIdentifier,
SkyblockMobDropRecipeDynamicGenerator)
registry.registerDisplayGenerator(
+ SBShopRecipe.Category.categoryIdentifier,
+ SkyblockShopRecipeDynamicGenerator)
+ registry.registerDisplayGenerator(
SBKatRecipe.Category.categoryIdentifier,
SkyblockKatRecipeDynamicGenerator)
- registry.registerDisplayGenerator(
- SBEssenceUpgradeRecipe.Category.categoryIdentifier,
- SkyblockEssenceRecipeDynamicGenerator
- )
}
override fun registerCollapsibleEntries(registry: CollapsibleEntryRegistry) {
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt
index a7b4c99..d73500a 100644
--- a/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt
@@ -17,17 +17,25 @@ import me.shedaniel.rei.api.common.entry.EntryStack
import net.fabricmc.fabric.api.client.item.v1.ItemTooltipCallback
import net.minecraft.client.MinecraftClient
import net.minecraft.client.gui.DrawContext
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
import net.minecraft.item.tooltip.TooltipType
-import moe.nea.firmament.compat.rei.FirmamentReiPlugin.Companion.asItemEntry
+import net.minecraft.text.Text
import moe.nea.firmament.events.ItemTooltipEvent
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
+import moe.nea.firmament.repo.ItemCache
+import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.repo.SBItemStack
import moe.nea.firmament.util.ErrorUtil
+import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.darkGrey
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
import moe.nea.firmament.util.mc.loreAccordingToNbt
// TODO: make this re implement BatchedEntryRenderer, if possible (likely not, due to no-alloc rendering)
// Also it is probably not even that much faster now, with render layers.
object NEUItemEntryRenderer : EntryRenderer<SBItemStack> {
+ @OptIn(ExpensiveItemCacheApi::class)
override fun render(
entry: EntryStack<SBItemStack>,
context: DrawContext,
@@ -36,20 +44,44 @@ object NEUItemEntryRenderer : EntryRenderer<SBItemStack> {
mouseY: Int,
delta: Float
) {
+ val neuItem = entry.value.neuItem
+ val itemToRender = if(RepoManager.Config.perfectRenders < RepoManager.PerfectRender.RENDER && !entry.value.isWarm() && neuItem != null) {
+ ItemCache.recacheSoon(neuItem)
+ ItemStack(Items.PAINTING)
+ } else {
+ entry.value.asImmutableItemStack()
+ }
+
context.matrices.push()
context.matrices.translate(bounds.centerX.toFloat(), bounds.centerY.toFloat(), 0F)
context.matrices.scale(bounds.width.toFloat() / 16F, bounds.height.toFloat() / 16F, 1f)
- context.drawItemWithoutEntity(
- entry.asItemEntry().value,
- -8, -8,
+ context.drawItemWithoutEntity(itemToRender, -8, -8)
+ context.drawStackOverlay(
+ minecraft.textRenderer, itemToRender, -8, -8,
+ if (entry.value.getStackSize() > 1000) FirmFormatters.shortFormat(
+ entry.value.getStackSize()
+ .toDouble()
+ )
+ else null
)
context.matrices.pop()
}
val minecraft = MinecraftClient.getInstance()
- var canUseVanillaTooltipEvents = false
+ var canUseVanillaTooltipEvents = true
+ @OptIn(ExpensiveItemCacheApi::class)
override fun getTooltip(entry: EntryStack<SBItemStack>, tooltipContext: TooltipContext): Tooltip? {
+ if (!entry.value.isWarm() && RepoManager.Config.perfectRenders < RepoManager.PerfectRender.RENDER_AND_TEXT) {
+ val neuItem = entry.value.neuItem
+ if (neuItem != null) {
+ val lore = mutableListOf<Text>()
+ lore.add(Text.literal(neuItem.displayName))
+ neuItem.lore.mapTo(mutableListOf()) { Text.literal(it) }
+ return Tooltip.create(lore)
+ }
+ }
+
val stack = entry.value.asImmutableItemStack()
val lore = mutableListOf(stack.displayNameAccordingToNbt)
@@ -60,16 +92,21 @@ object NEUItemEntryRenderer : EntryRenderer<SBItemStack> {
stack, tooltipContext.vanillaContext(), TooltipType.BASIC, lore
)
} catch (ex: Exception) {
+ canUseVanillaTooltipEvents = false
ErrorUtil.softError("Failed to use vanilla tooltips", ex)
}
} else {
- ItemTooltipEvent.publish(ItemTooltipEvent(
- stack,
- tooltipContext.vanillaContext(),
- TooltipType.BASIC,
- lore
- ))
+ ItemTooltipEvent.publish(
+ ItemTooltipEvent(
+ stack,
+ tooltipContext.vanillaContext(),
+ TooltipType.BASIC,
+ lore
+ )
+ )
}
+ if (entry.value.getStackSize() > 1000 && lore.isNotEmpty())
+ lore.add(1, Text.literal("${entry.value.getStackSize()}x").darkGrey())
// TODO: tags aren't sent as early now so some tooltip components that use tags will crash the game
// stack.getTooltip(
// Item.TooltipContext.create(
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/REIRecipeLayouter.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/REIRecipeLayouter.kt
new file mode 100644
index 0000000..8e39f28
--- /dev/null
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/REIRecipeLayouter.kt
@@ -0,0 +1,62 @@
+package moe.nea.firmament.compat.rei
+
+import io.github.notenoughupdates.moulconfig.gui.GuiComponent
+import me.shedaniel.math.Dimension
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import me.shedaniel.rei.api.client.gui.widgets.Widget
+import me.shedaniel.rei.api.client.gui.widgets.Widgets
+import net.minecraft.text.Text
+import moe.nea.firmament.compat.rei.recipes.wrapWidget
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.repo.recipes.RecipeLayouter
+
+class REIRecipeLayouter : RecipeLayouter {
+ val container: MutableList<Widget> = mutableListOf()
+ fun <T: Widget> add(t: T): T = t.also(container::add)
+
+ override fun createItemSlot(
+ x: Int,
+ y: Int,
+ content: SBItemStack?,
+ slotKind: RecipeLayouter.SlotKind
+ ) {
+ val slot = Widgets.createSlot(Point(x, y))
+ if (content != null)
+ slot.entry(SBItemEntryDefinition.getEntry(content))
+ when (slotKind) {
+ RecipeLayouter.SlotKind.SMALL_INPUT -> slot.markInput()
+ RecipeLayouter.SlotKind.SMALL_OUTPUT -> slot.markOutput()
+ RecipeLayouter.SlotKind.BIG_OUTPUT -> {
+ slot.markOutput().disableBackground()
+ add(Widgets.createResultSlotBackground(Point(x, y)))
+ }
+ }
+ add(slot)
+ }
+
+ override fun createTooltip(rectangle: Rectangle, label: Text) {
+ add(Widgets.createTooltip(rectangle, label))
+ }
+
+ override fun createLabel(x: Int, y: Int, text: Text) {
+ add(Widgets.createLabel(Point(x, y), text))
+ }
+
+ override fun createArrow(x: Int, y: Int) =
+ add(Widgets.createArrow(Point(x, y))).bounds
+
+ override fun createMoulConfig(
+ x: Int,
+ y: Int,
+ w: Int,
+ h: Int,
+ component: GuiComponent
+ ) {
+ add(wrapWidget(Rectangle(Point(x, y), Dimension(w, h)), component))
+ }
+
+ override fun createFire(ingredientsCenter: Point, animationTicks: Int) {
+ add(Widgets.createBurningFire(ingredientsCenter).animationDurationTicks(animationTicks.toDouble()))
+ }
+}
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt
index 9638281..1d0a611 100644
--- a/src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SBItemEntryDefinition.kt
@@ -15,18 +15,17 @@ import net.minecraft.registry.tag.TagKey
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import moe.nea.firmament.compat.rei.FirmamentReiPlugin.Companion.asItemEntry
-import moe.nea.firmament.repo.PetData
+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.petData
-import moe.nea.firmament.util.skyBlockId
object SBItemEntryDefinition : EntryDefinition<SBItemStack> {
override fun equals(o1: SBItemStack, o2: SBItemStack, context: ComparisonContext): Boolean {
return o1.skyblockId == o2.skyblockId && o1.getStackSize() == o2.getStackSize()
}
+ @OptIn(ExpensiveItemCacheApi::class)
override fun cheatsAs(entry: EntryStack<SBItemStack>?, value: SBItemStack): ItemStack {
return value.asCopiedItemStack()
}
@@ -44,8 +43,14 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> {
return Stream.empty()
}
+ @OptIn(ExpensiveItemCacheApi::class)
override fun asFormattedText(entry: EntryStack<SBItemStack>, value: SBItemStack): Text {
- return VanillaEntryTypes.ITEM.definition.asFormattedText(entry.asItemEntry(), value.asImmutableItemStack())
+ val neuItem = entry.value.neuItem
+ return if (RepoManager.Config.perfectRenders < RepoManager.PerfectRender.RENDER_AND_TEXT || entry.value.isWarm() || neuItem == null) {
+ VanillaEntryTypes.ITEM.definition.asFormattedText(entry.asItemEntry(), value.asImmutableItemStack())
+ } else {
+ Text.literal(neuItem.displayName)
+ }
}
override fun hash(entry: EntryStack<SBItemStack>, value: SBItemStack, context: ComparisonContext): Long {
@@ -54,8 +59,10 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> {
}
override fun wildcard(entry: EntryStack<SBItemStack>?, value: SBItemStack): SBItemStack {
- return value.copy(stackSize = 1, petData = RepoManager.getPotentialStubPetData(value.skyblockId),
- stars = 0, extraLore = listOf())
+ return value.copy(
+ stackSize = 1, petData = RepoManager.getPotentialStubPetData(value.skyblockId),
+ stars = 0, extraLore = listOf(), reforge = null
+ )
}
override fun normalize(entry: EntryStack<SBItemStack>?, value: SBItemStack): SBItemStack {
@@ -86,12 +93,5 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> {
fun getPassthrough(item: ItemConvertible) = getEntry(SBItemStack.passthrough(ItemStack(item.asItem())))
fun getEntry(stack: ItemStack): EntryStack<SBItemStack> =
- getEntry(
- SBItemStack(
- stack.skyBlockId ?: SkyblockId.NULL,
- RepoManager.getNEUItem(stack.skyBlockId ?: SkyblockId.NULL),
- stack.count,
- petData = stack.petData?.let { PetData.fromHypixel(it) }
- )
- )
+ getEntry(SBItemStack(stack))
}
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt
index f52f418..900ebab 100644
--- a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt
@@ -1,65 +1,56 @@
-
-
package moe.nea.firmament.compat.rei
-import io.github.moulberry.repo.data.NEUCraftingRecipe
import io.github.moulberry.repo.data.NEUForgeRecipe
import io.github.moulberry.repo.data.NEUKatUpgradeRecipe
import io.github.moulberry.repo.data.NEUMobDropRecipe
+import io.github.moulberry.repo.data.NEUNpcShopRecipe
import io.github.moulberry.repo.data.NEURecipe
import java.util.Optional
import me.shedaniel.rei.api.client.registry.display.DynamicDisplayGenerator
import me.shedaniel.rei.api.client.view.ViewSearchBuilder
import me.shedaniel.rei.api.common.display.Display
import me.shedaniel.rei.api.common.entry.EntryStack
-import moe.nea.firmament.compat.rei.recipes.SBCraftingRecipe
-import moe.nea.firmament.compat.rei.recipes.SBEssenceUpgradeRecipe
-import moe.nea.firmament.compat.rei.recipes.SBForgeRecipe
import moe.nea.firmament.compat.rei.recipes.SBKatRecipe
import moe.nea.firmament.compat.rei.recipes.SBMobDropRecipe
+import moe.nea.firmament.compat.rei.recipes.SBShopRecipe
import moe.nea.firmament.repo.EssenceRecipeProvider
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.repo.SBItemStack
-val SkyblockCraftingRecipeDynamicGenerator =
- neuDisplayGenerator<SBCraftingRecipe, NEUCraftingRecipe> { SBCraftingRecipe(it) }
-
-val SkyblockForgeRecipeDynamicGenerator =
- neuDisplayGenerator<SBForgeRecipe, NEUForgeRecipe> { SBForgeRecipe(it) }
-
val SkyblockMobDropRecipeDynamicGenerator =
- neuDisplayGenerator<SBMobDropRecipe, NEUMobDropRecipe> { SBMobDropRecipe(it) }
-
+ neuDisplayGenerator<SBMobDropRecipe, NEUMobDropRecipe> { SBMobDropRecipe(it) }
+val SkyblockShopRecipeDynamicGenerator =
+ neuDisplayGenerator<SBShopRecipe, NEUNpcShopRecipe> { SBShopRecipe(it) }
val SkyblockKatRecipeDynamicGenerator =
- neuDisplayGenerator<SBKatRecipe, NEUKatUpgradeRecipe> { SBKatRecipe(it) }
-val SkyblockEssenceRecipeDynamicGenerator =
- neuDisplayGenerator<SBEssenceUpgradeRecipe, EssenceRecipeProvider.EssenceUpgradeRecipe> { SBEssenceUpgradeRecipe(it) }
+ neuDisplayGenerator<SBKatRecipe, NEUKatUpgradeRecipe> { SBKatRecipe(it) }
inline fun <D : Display, reified T : NEURecipe> neuDisplayGenerator(crossinline mapper: (T) -> D) =
- object : DynamicDisplayGenerator<D> {
- override fun getRecipeFor(entry: EntryStack<*>): Optional<List<D>> {
- if (entry.type != SBItemEntryDefinition.type) return Optional.empty()
- val item = entry.castValue<SBItemStack>()
- val recipes = RepoManager.getRecipesFor(item.skyblockId)
- val craftingRecipes = recipes.filterIsInstance<T>()
- return Optional.of(craftingRecipes.map(mapper))
- }
-
- override fun generate(builder: ViewSearchBuilder): Optional<List<D>> {
- if (SBCraftingRecipe.Category.catIdentifier !in builder.categories) return Optional.empty()
- return Optional.of(
- RepoManager.getAllRecipes().filterIsInstance<T>().map { mapper(it) }
- .toList()
- )
- }
-
- override fun getUsageFor(entry: EntryStack<*>): Optional<List<D>> {
- if (entry.type != SBItemEntryDefinition.type) return Optional.empty()
- val item = entry.castValue<SBItemStack>()
- val recipes = RepoManager.getUsagesFor(item.skyblockId)
- val craftingRecipes = recipes.filterIsInstance<T>()
- return Optional.of(craftingRecipes.map(mapper))
-
- }
- }
+ neuDisplayGeneratorWithItem<D, T> { _, it -> mapper(it) }
+
+inline fun <D : Display, reified T : NEURecipe> neuDisplayGeneratorWithItem(crossinline mapper: (SBItemStack, T) -> D) =
+ neuDisplayGeneratorWithItem(T::class.java, mapper)
+inline fun <D : Display, T : NEURecipe> neuDisplayGeneratorWithItem(
+ filter: Class<T>,
+ crossinline mapper: (SBItemStack, T) -> D) =
+ object : DynamicDisplayGenerator<D> {
+ override fun getRecipeFor(entry: EntryStack<*>): Optional<List<D>> {
+ if (entry.type != SBItemEntryDefinition.type) return Optional.empty()
+ val item = entry.castValue<SBItemStack>()
+ val recipes = RepoManager.getRecipesFor(item.skyblockId)
+ val craftingRecipes = recipes.filterIsInstance<T>(filter)
+ return Optional.of(craftingRecipes.map { mapper(item, it) })
+ }
+
+ override fun generate(builder: ViewSearchBuilder): Optional<List<D>> {
+ return Optional.empty() // TODO: allows searching without blocking getRecipeFor
+ }
+
+ override fun getUsageFor(entry: EntryStack<*>): Optional<List<D>> {
+ if (entry.type != SBItemEntryDefinition.type) return Optional.empty()
+ val item = entry.castValue<SBItemStack>()
+ val recipes = RepoManager.getUsagesFor(item.skyblockId)
+ val craftingRecipes = recipes.filterIsInstance<T>(filter)
+ return Optional.of(craftingRecipes.map { mapper(item, it) })
+ }
+ }
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt
index cfb6f74..9ccfab4 100644
--- a/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt
@@ -9,7 +9,6 @@ import me.shedaniel.rei.api.common.entry.EntryStack
import net.minecraft.client.gui.screen.Screen
import net.minecraft.client.gui.screen.ingame.HandledScreen
import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
-import moe.nea.firmament.util.skyBlockId
object SkyblockItemIdFocusedStackProvider : FocusedStackProvider {
override fun provide(screen: Screen?, mouse: Point?): CompoundEventResult<EntryStack<*>> {
@@ -17,8 +16,7 @@ object SkyblockItemIdFocusedStackProvider : FocusedStackProvider {
screen as AccessorHandledScreen
val focusedSlot = screen.focusedSlot_Firmament ?: return CompoundEventResult.pass()
val item = focusedSlot.stack ?: return CompoundEventResult.pass()
- val skyblockId = item.skyBlockId ?: return CompoundEventResult.pass()
- return CompoundEventResult.interruptTrue(SBItemEntryDefinition.getEntry(skyblockId))
+ return CompoundEventResult.interruptTrue(SBItemEntryDefinition.getEntry(item))
}
override fun getPriority(): Double = 1_000_000.0
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/GenericREIRecipeCategory.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/GenericREIRecipeCategory.kt
new file mode 100644
index 0000000..15cb818
--- /dev/null
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/GenericREIRecipeCategory.kt
@@ -0,0 +1,67 @@
+package moe.nea.firmament.compat.rei.recipes
+
+import io.github.moulberry.repo.data.NEURecipe
+import me.shedaniel.math.Rectangle
+import me.shedaniel.rei.api.client.gui.Renderer
+import me.shedaniel.rei.api.client.gui.widgets.Widget
+import me.shedaniel.rei.api.client.gui.widgets.Widgets
+import me.shedaniel.rei.api.client.registry.display.DisplayCategory
+import me.shedaniel.rei.api.client.registry.display.DisplayRegistry
+import me.shedaniel.rei.api.common.category.CategoryIdentifier
+import me.shedaniel.rei.api.common.util.EntryStacks
+import net.minecraft.text.Text
+import moe.nea.firmament.compat.rei.REIRecipeLayouter
+import moe.nea.firmament.compat.rei.neuDisplayGeneratorWithItem
+import moe.nea.firmament.repo.SBItemStack
+import moe.nea.firmament.repo.recipes.GenericRecipeRenderer
+
+class GenericREIRecipeCategory<T : NEURecipe>(
+ val renderer: GenericRecipeRenderer<T>,
+) : DisplayCategory<GenericRecipe<T>> {
+ private val dynamicGenerator =
+ neuDisplayGeneratorWithItem<GenericRecipe<T>, T>(renderer.typ) { item, recipe ->
+ GenericRecipe(
+ recipe,
+ item,
+ categoryIdentifier
+ )
+ }
+
+ private val categoryIdentifier = CategoryIdentifier.of<GenericRecipe<T>>(renderer.identifier)
+ override fun getCategoryIdentifier(): CategoryIdentifier<GenericRecipe<T>> {
+ return categoryIdentifier
+ }
+
+ override fun getDisplayHeight(): Int {
+ return renderer.displayHeight
+ }
+
+ override fun getTitle(): Text? {
+ return renderer.title
+ }
+
+ override fun getIcon(): Renderer? {
+ return EntryStacks.of(renderer.icon)
+ }
+
+ override fun setupDisplay(display: GenericRecipe<T>, bounds: Rectangle): List<Widget> {
+ val layouter = REIRecipeLayouter()
+ layouter.container.add(Widgets.createRecipeBase(bounds))
+ renderer.render(display.neuRecipe, bounds, layouter, display.sourceItem)
+ return layouter.container
+ }
+
+ fun registerDynamicGenerator(registry: DisplayRegistry) {
+ registry.registerDisplayGenerator(categoryIdentifier, dynamicGenerator)
+ }
+}
+
+class GenericRecipe<T : NEURecipe>(
+ override val neuRecipe: T,
+ val sourceItem: SBItemStack?,
+ val id: CategoryIdentifier<GenericRecipe<T>>
+) : SBRecipe() {
+ override fun getCategoryIdentifier(): CategoryIdentifier<*>? {
+ return id
+ }
+}
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt
deleted file mode 100644
index 8db3d75..0000000
--- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBCraftingRecipe.kt
+++ /dev/null
@@ -1,56 +0,0 @@
-package moe.nea.firmament.compat.rei.recipes
-
-import io.github.moulberry.repo.data.NEUCraftingRecipe
-import io.github.moulberry.repo.data.NEUIngredient
-import me.shedaniel.math.Point
-import me.shedaniel.math.Rectangle
-import me.shedaniel.rei.api.client.gui.Renderer
-import me.shedaniel.rei.api.client.gui.widgets.Widget
-import me.shedaniel.rei.api.client.gui.widgets.Widgets
-import me.shedaniel.rei.api.client.registry.display.DisplayCategory
-import me.shedaniel.rei.api.common.category.CategoryIdentifier
-import me.shedaniel.rei.api.common.display.Display
-import me.shedaniel.rei.api.common.display.DisplaySerializer
-import me.shedaniel.rei.api.common.util.EntryStacks
-import net.minecraft.block.Blocks
-import net.minecraft.text.Text
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.compat.rei.SBItemEntryDefinition
-import moe.nea.firmament.repo.SBItemStack
-
-class SBCraftingRecipe(override val neuRecipe: NEUCraftingRecipe) : SBRecipe() {
- override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.catIdentifier
-
- object Category : DisplayCategory<SBCraftingRecipe> {
- val catIdentifier = CategoryIdentifier.of<SBCraftingRecipe>(Firmament.MOD_ID, "crafing_recipe")
- override fun getCategoryIdentifier(): CategoryIdentifier<out SBCraftingRecipe> = catIdentifier
-
- override fun getTitle(): Text = Text.literal("SkyBlock Crafting")
-
- override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Blocks.CRAFTING_TABLE)
- override fun setupDisplay(display: SBCraftingRecipe, bounds: Rectangle): List<Widget> {
- val point = Point(bounds.centerX - 58, bounds.centerY - 27)
- return buildList {
- add(Widgets.createRecipeBase(bounds))
- add(Widgets.createArrow(Point(point.x + 60, point.y + 18)))
- add(Widgets.createResultSlotBackground(Point(point.x + 95, point.y + 19)))
- for (i in 0 until 3) {
- for (j in 0 until 3) {
- val slot = Widgets.createSlot(Point(point.x + 1 + i * 18, point.y + 1 + j * 18)).markInput()
- add(slot)
- val item = display.neuRecipe.inputs[i + j * 3]
- if (item == NEUIngredient.SENTINEL_EMPTY) continue
- slot.entry(SBItemEntryDefinition.getEntry(item))
- }
- }
- add(
- Widgets.createSlot(Point(point.x + 95, point.y + 19))
- .entry(SBItemEntryDefinition.getEntry(display.neuRecipe.output))
- .disableBackground().markOutput()
- )
- }
- }
-
- }
-
-}
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt
deleted file mode 100644
index ec71ec8..0000000
--- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt
+++ /dev/null
@@ -1,61 +0,0 @@
-package moe.nea.firmament.compat.rei.recipes
-
-import me.shedaniel.math.Point
-import me.shedaniel.math.Rectangle
-import me.shedaniel.rei.api.client.gui.Renderer
-import me.shedaniel.rei.api.client.gui.widgets.Widget
-import me.shedaniel.rei.api.client.gui.widgets.Widgets
-import me.shedaniel.rei.api.client.registry.display.DisplayCategory
-import me.shedaniel.rei.api.common.category.CategoryIdentifier
-import net.minecraft.text.Text
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.compat.rei.SBItemEntryDefinition
-import moe.nea.firmament.repo.EssenceRecipeProvider
-import moe.nea.firmament.repo.SBItemStack
-import moe.nea.firmament.util.SkyblockId
-
-class SBEssenceUpgradeRecipe(override val neuRecipe: EssenceRecipeProvider.EssenceUpgradeRecipe) : SBRecipe() {
- object Category : DisplayCategory<SBEssenceUpgradeRecipe> {
- override fun getCategoryIdentifier(): CategoryIdentifier<SBEssenceUpgradeRecipe> =
- CategoryIdentifier.of(Firmament.MOD_ID, "essence_upgrade")
-
- override fun getTitle(): Text {
- return Text.literal("Essence Upgrades")
- }
-
- override fun getIcon(): Renderer {
- return SBItemEntryDefinition.getEntry(SkyblockId("ESSENCE_WITHER"))
- }
-
- override fun setupDisplay(display: SBEssenceUpgradeRecipe, bounds: Rectangle): List<Widget> {
- val recipe = display.neuRecipe
- val list = mutableListOf<Widget>()
- list.add(Widgets.createRecipeBase(bounds))
- list.add(Widgets.createSlot(Point(bounds.minX + 12, bounds.centerY - 8 - 18 / 2))
- .markInput()
- .entry(SBItemEntryDefinition.getEntry(SBItemStack(recipe.itemId).copy(stars = recipe.starCountAfter - 1))))
- list.add(Widgets.createSlot(Point(bounds.minX + 12, bounds.centerY - 8 + 18 / 2))
- .markInput()
- .entry(SBItemEntryDefinition.getEntry(recipe.essenceIngredient)))
- list.add(Widgets.createSlot(Point(bounds.maxX - 12 - 16, bounds.centerY - 8))
- .markOutput()
- .entry(SBItemEntryDefinition.getEntry(SBItemStack(recipe.itemId).copy(stars = recipe.starCountAfter))))
- val extraItems = recipe.extraItems
- list.add(Widgets.createArrow(Point(bounds.centerX - 24 / 2,
- if (extraItems.isEmpty()) bounds.centerY - 17 / 2
- else bounds.centerY + 18 / 2)))
- for ((index, item) in extraItems.withIndex()) {
- list.add(Widgets.createSlot(
- Point(bounds.centerX - extraItems.size * 16 / 2 - 2 / 2 + index * 18,
- bounds.centerY - 18 / 2))
- .markInput()
- .entry(SBItemEntryDefinition.getEntry(item)))
- }
- return list
- }
- }
-
- override fun getCategoryIdentifier(): CategoryIdentifier<*> {
- return Category.categoryIdentifier
- }
-}
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt
deleted file mode 100644
index 92b2f3f..0000000
--- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBForgeRecipe.kt
+++ /dev/null
@@ -1,71 +0,0 @@
-package moe.nea.firmament.compat.rei.recipes
-
-import io.github.moulberry.repo.data.NEUForgeRecipe
-import me.shedaniel.math.Point
-import me.shedaniel.math.Rectangle
-import me.shedaniel.rei.api.client.gui.Renderer
-import me.shedaniel.rei.api.client.gui.widgets.Widget
-import me.shedaniel.rei.api.client.gui.widgets.Widgets
-import me.shedaniel.rei.api.client.registry.display.DisplayCategory
-import me.shedaniel.rei.api.common.category.CategoryIdentifier
-import me.shedaniel.rei.api.common.util.EntryStacks
-import kotlin.math.cos
-import kotlin.math.sin
-import kotlin.time.Duration.Companion.seconds
-import net.minecraft.block.Blocks
-import net.minecraft.text.Text
-import moe.nea.firmament.Firmament
-import moe.nea.firmament.compat.rei.SBItemEntryDefinition
-import moe.nea.firmament.compat.rei.plus
-
-class SBForgeRecipe(override val neuRecipe: NEUForgeRecipe) : SBRecipe() {
- override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.categoryIdentifier
-
- object Category : DisplayCategory<SBForgeRecipe> {
- override fun getCategoryIdentifier(): CategoryIdentifier<SBForgeRecipe> =
- CategoryIdentifier.of(Firmament.MOD_ID, "forge_recipe")
-
- override fun getTitle(): Text = Text.literal("Forge Recipes")
- override fun getDisplayHeight(): Int {
- return 104
- }
-
- override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Blocks.ANVIL)
- override fun setupDisplay(display: SBForgeRecipe, bounds: Rectangle): List<Widget> {
- return buildList {
- add(Widgets.createRecipeBase(bounds))
- add(Widgets.createResultSlotBackground(Point(bounds.minX + 124, bounds.minY + 46)))
- val arrow = Widgets.createArrow(Point(bounds.minX + 90, bounds.minY + 54 - 18 / 2))
- add(arrow)
- add(Widgets.createTooltip(arrow.bounds,
- Text.stringifiedTranslatable("firmament.recipe.forge.time",
- display.neuRecipe.duration.seconds)))
- val ingredientsCenter = Point(bounds.minX + 49 - 8, bounds.minY + 54 - 8)
- val count = display.neuRecipe.inputs.size
- if (count == 1) {
- add(
- Widgets.createSlot(Point(ingredientsCenter.x, ingredientsCenter.y)).markInput()
- .entry(SBItemEntryDefinition.getEntry(display.neuRecipe.inputs.single()))
- )
- } else {
- display.neuRecipe.inputs.forEachIndexed { idx, ingredient ->
- val rad = Math.PI * 2 * idx / count
- add(
- Widgets.createSlot(
- Point(
- cos(rad) * 30,
- sin(rad) * 30,
- ) + ingredientsCenter
- ).markInput().entry(SBItemEntryDefinition.getEntry(ingredient))
- )
- }
- }
- add(
- Widgets.createSlot(Point(bounds.minX + 124, bounds.minY + 46)).markOutput().disableBackground()
- .entry(SBItemEntryDefinition.getEntry(display.neuRecipe.outputStack))
- )
- }
- }
- }
-
-}
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt
index 4d00a4f..fca3edf 100644
--- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBReforgeRecipe.kt
@@ -1,3 +1,5 @@
+@file:OptIn(ExpensiveItemCacheApi::class)
+
package moe.nea.firmament.compat.rei.recipes
import java.util.Optional
@@ -19,6 +21,7 @@ import me.shedaniel.rei.api.common.entry.EntryIngredient
import me.shedaniel.rei.api.common.entry.EntryStack
import net.minecraft.entity.EntityType
import net.minecraft.entity.SpawnReason
+import net.minecraft.registry.entry.RegistryEntry
import net.minecraft.text.Text
import net.minecraft.util.Identifier
import net.minecraft.village.VillagerProfession
@@ -26,6 +29,7 @@ import moe.nea.firmament.Firmament
import moe.nea.firmament.compat.rei.EntityWidget
import moe.nea.firmament.compat.rei.SBItemEntryDefinition
import moe.nea.firmament.gui.entity.EntityRenderer
+import moe.nea.firmament.repo.ExpensiveItemCacheApi
import moe.nea.firmament.repo.Reforge
import moe.nea.firmament.repo.ReforgeStore
import moe.nea.firmament.repo.RepoItemTypeCache
@@ -33,6 +37,7 @@ import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.repo.SBItemStack
import moe.nea.firmament.util.AprilFoolsUtil
import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.MC
import moe.nea.firmament.util.SkyblockId
import moe.nea.firmament.util.gold
import moe.nea.firmament.util.grey
@@ -44,7 +49,7 @@ import moe.nea.firmament.util.tr
class SBReforgeRecipe(
val reforge: Reforge,
- val limitToItem: SkyblockId?,
+ val limitToItem: SBItemStack?,
) : Display {
companion object {
val catIdentifier = CategoryIdentifier.of<SBReforgeRecipe>(Firmament.MOD_ID, "reforge_recipe")
@@ -92,7 +97,7 @@ class SBReforgeRecipe(
list.add(Widgets.withTooltip(
EntityWidget(
EntityType.VILLAGER.create(EntityRenderer.fakeWorld, SpawnReason.COMMAND)
- ?.also { it.villagerData = it.villagerData.withProfession(VillagerProfession.WEAPONSMITH) },
+ ?.also { it.villagerData = it.villagerData.withProfession(MC.currentOrDefaultRegistries.getEntryOrThrow(VillagerProfession.WEAPONSMITH)) },
Point(bounds.minX + 10 + 24 + 8 - dimension.width / 2, bounds.centerY - dimension.height / 2),
dimension
),
@@ -132,10 +137,10 @@ class SBReforgeRecipe(
fun getRecipesForSBItemStack(item: SBItemStack): Optional<List<SBReforgeRecipe>> {
val reforgeRecipes = mutableListOf<SBReforgeRecipe>()
for (reforge in ReforgeStore.findEligibleForInternalName(item.skyblockId)) {
- reforgeRecipes.add(SBReforgeRecipe(reforge, item.skyblockId))
+ reforgeRecipes.add(SBReforgeRecipe(reforge, item))
}
for (reforge in ReforgeStore.findEligibleForItem(item.itemType ?: ItemType.NIL)) {
- reforgeRecipes.add(SBReforgeRecipe(reforge, item.skyblockId))
+ reforgeRecipes.add(SBReforgeRecipe(reforge, item))
}
if (reforgeRecipes.isEmpty()) return Optional.empty()
return Optional.of(reforgeRecipes)
@@ -162,26 +167,27 @@ class SBReforgeRecipe(
}
}
- private val eligibleItems =
- if (limitToItem != null) listOfNotNull(RepoManager.getNEUItem(limitToItem))
- else reforge.eligibleItems.flatMap {
+ private val inputItems = run {
+ if (limitToItem != null) return@run listOf(SBItemEntryDefinition.getEntry(limitToItem))
+ val eligibleItems = reforge.eligibleItems.flatMap {
when (it) {
- is Reforge.ReforgeEligibilityFilter.AllowsInternalName ->
- listOfNotNull(RepoManager.getNEUItem(it.internalName))
-
- is Reforge.ReforgeEligibilityFilter.AllowsItemType ->
- ReforgeStore.resolveItemType(it.itemType)
- .flatMapTo(mutableSetOf()) {
- (RepoItemTypeCache.byItemType[it] ?: listOf()) +
- (RepoItemTypeCache.byItemType[it.dungeonVariant] ?: listOf())
- }.toList()
-
- is Reforge.ReforgeEligibilityFilter.AllowsVanillaItemType -> {
- listOf() // TODO: add filter support for this and potentially rework this to search for the declared item type in repo, instead of remapped item type
+ is Reforge.ReforgeEligibilityFilter.AllowsInternalName ->
+ listOfNotNull(RepoManager.getNEUItem(it.internalName))
+
+ is Reforge.ReforgeEligibilityFilter.AllowsItemType ->
+ ReforgeStore.resolveItemType(it.itemType)
+ .flatMapTo(mutableSetOf()) {
+ (RepoItemTypeCache.byItemType[it] ?: listOf()) +
+ (RepoItemTypeCache.byItemType[it.dungeonVariant] ?: listOf())
+ }.toList()
+
+ is Reforge.ReforgeEligibilityFilter.AllowsVanillaItemType -> {
+ listOf() // TODO: add filter support for this and potentially rework this to search for the declared item type in repo, instead of remapped item type
+ }
}
- }
}
- private val inputItems = eligibleItems.map { SBItemEntryDefinition.getEntry(it.skyblockId) }
+ eligibleItems.map { SBItemEntryDefinition.getEntry(it.skyblockId) }
+ }
private val outputItems =
inputItems.map { SBItemEntryDefinition.getEntry(it.value.copy(reforge = reforge.reforgeId)) }
private val reforgeStone = reforge.reforgeStone?.let(SBItemEntryDefinition::getEntry)
diff --git a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBShopRecipe.kt b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBShopRecipe.kt
new file mode 100644
index 0000000..a252802
--- /dev/null
+++ b/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBShopRecipe.kt
@@ -0,0 +1,61 @@
+package moe.nea.firmament.compat.rei.recipes
+
+import io.github.moulberry.repo.data.NEUNpcShopRecipe
+import me.shedaniel.math.Point
+import me.shedaniel.math.Rectangle
+import me.shedaniel.rei.api.client.gui.Renderer
+import me.shedaniel.rei.api.client.gui.widgets.Widget
+import me.shedaniel.rei.api.client.gui.widgets.Widgets
+import me.shedaniel.rei.api.client.registry.display.DisplayCategory
+import me.shedaniel.rei.api.common.category.CategoryIdentifier
+import me.shedaniel.rei.api.common.entry.EntryIngredient
+import net.minecraft.item.Items
+import net.minecraft.text.Text
+import moe.nea.firmament.Firmament
+import moe.nea.firmament.compat.rei.SBItemEntryDefinition
+import moe.nea.firmament.util.skyblockId
+
+class SBShopRecipe(override val neuRecipe: NEUNpcShopRecipe) : SBRecipe() {
+ override fun getCategoryIdentifier(): CategoryIdentifier<*> = Category.catIdentifier
+ val merchant = SBItemEntryDefinition.getEntry(neuRecipe.isSoldBy.skyblockId)
+ override fun getInputEntries(): List<EntryIngredient> {
+ return listOf(EntryIngredient.of(merchant)) + super.getInputEntries()
+ }
+
+ object Category : DisplayCategory<SBShopRecipe> {
+ val catIdentifier = CategoryIdentifier.of<SBShopRecipe>(Firmament.MOD_ID, "npc_shopping")
+ override fun getCategoryIdentifier(): CategoryIdentifier<SBShopRecipe> = catIdentifier
+
+ override fun getTitle(): Text = Text.literal("SkyBlock NPC Shopping")
+
+ override fun getIcon(): Renderer = SBItemEntryDefinition.getPassthrough(Items.EMERALD)
+ override fun setupDisplay(display: SBShopRecipe, bounds: Rectangle): List<Widget> {
+ val point = Point(bounds.centerX, bounds.centerY)
+ return buildList {
+ add(Widgets.createRecipeBase(bounds))
+ add(Widgets.createSlot(Point(point.x - 2 - 18 / 2, point.y - 18 - 6))
+ .unmarkInputOrOutput()
+ .entry(display.merchant)
+ .disableBackground())
+ add(Widgets.createArrow(Point(point.x - 2 - 24 / 2, point.y - 6)))
+ val cost = display.neuRecipe.cost
+ for ((i, item) in cost.withIndex()) {
+ add(Widgets.createSlot(Point(
+ point.x - 14 - 18,
+ point.y + i * 18 - 18 * cost.size / 2))
+ .entry(SBItemEntryDefinition.getEntry(item))
+ .markInput())
+ // TODO: fix frame clipping
+ }
+ add(Widgets.createResultSlotBackground(Point(point.x + 18, point.y - 18 / 2)))
+ add(
+ Widgets.createSlot(Point(point.x + 18, point.y - 18 / 2))
+ .entry(SBItemEntryDefinition.getEntry(display.neuRecipe.result))
+ .disableBackground().markOutput()
+ )
+ }
+ }
+
+ }
+
+}
diff --git a/src/compat/wildfireGender/java/moe/nea/firmament/compat/gender/Compat.kt b/src/compat/wildfireGender/java/moe/nea/firmament/compat/gender/Compat.kt
new file mode 100644
index 0000000..347dd5d
--- /dev/null
+++ b/src/compat/wildfireGender/java/moe/nea/firmament/compat/gender/Compat.kt
@@ -0,0 +1,13 @@
+package moe.nea.firmament.compat.gender
+
+import net.fabricmc.loader.api.FabricLoader
+import moe.nea.firmament.util.compatloader.CompatMeta
+import moe.nea.firmament.util.compatloader.ICompatMeta
+
+@CompatMeta
+object Compat : ICompatMeta {
+ override fun shouldLoad(): Boolean {
+ return FabricLoader.getInstance().isModLoaded("wildfire_gender")
+ }
+
+}
diff --git a/src/compat/yacl/java/YaclIntegration.kt b/src/compat/yacl/java/YaclIntegration.kt
index 45a0d02..a022ffd 100644
--- a/src/compat/yacl/java/YaclIntegration.kt
+++ b/src/compat/yacl/java/YaclIntegration.kt
@@ -154,7 +154,7 @@ class YaclIntegration : FirmamentConfigScreenProvider {
override val key: String
get() = "yacl"
- override fun open(parent: Screen?): Screen {
+ override fun open(search: String?, parent: Screen?): Screen {
return object : YACLScreen(buildConfig(), parent) {
override fun setFocused(focused: Element?) {
if (this.focused is KeybindingWidget &&
diff --git a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java
index 0713068..a9db7f9 100644
--- a/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java
+++ b/src/main/java/moe/nea/firmament/init/AutoDiscoveryPlugin.java
@@ -1,6 +1,9 @@
package moe.nea.firmament.init;
+import moe.nea.firmament.util.ErrorUtil;
+import moe.nea.firmament.util.compatloader.ICompatMeta;
+
import java.io.File;
import java.io.IOException;
import java.net.MalformedURLException;
@@ -94,7 +97,7 @@ public class AutoDiscoveryPlugin {
String norm = (className.substring(0, className.length() - ".class".length()))
.replace("\\", "/")
.replace("/", ".");
- if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".")) {
+ if (norm.startsWith(getMixinPackage() + ".") && !norm.endsWith(".") && ICompatMeta.Companion.shouldLoad(norm)) {
mixins.add(norm.substring(getMixinPackage().length() + 1));
}
}
@@ -125,24 +128,25 @@ public class AutoDiscoveryPlugin {
*/
public List<String> getMixins() {
if (mixins != null) return mixins;
- System.out.println("Trying to discover mixins");
- mixins = new ArrayList<>();
- URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation();
- System.out.println("Found classes at " + classUrl);
- tryDiscoverFromContentFile(classUrl);
- var classRoots = System.getProperty("firmament.classroots");
- if (classRoots != null && !classRoots.isBlank()) {
- System.out.println("Found firmament class roots: " + classRoots);
- for (String s : classRoots.split(File.pathSeparator)) {
- if (s.isBlank()) {
- continue;
- }
- try {
+ try {
+ System.out.println("Trying to discover mixins");
+ mixins = new ArrayList<>();
+ URL classUrl = getClass().getProtectionDomain().getCodeSource().getLocation();
+ System.out.println("Found classes at " + classUrl);
+ tryDiscoverFromContentFile(classUrl);
+ var classRoots = System.getProperty("firmament.classroots");
+ if (classRoots != null && !classRoots.isBlank()) {
+ System.out.println("Found firmament class roots: " + classRoots);
+ for (String s : classRoots.split(File.pathSeparator)) {
+ if (s.isBlank()) {
+ continue;
+ }
tryDiscoverFromContentFile(new File(s).toURI().toURL());
- } catch (MalformedURLException e) {
- throw new RuntimeException(e);
}
}
+ } catch (Exception e) {
+ e.printStackTrace();
+ System.exit(1);
}
return mixins;
}
diff --git a/src/main/java/moe/nea/firmament/init/MixinPlugin.java b/src/main/java/moe/nea/firmament/init/MixinPlugin.java
index 61e8f14..d48139b 100644
--- a/src/main/java/moe/nea/firmament/init/MixinPlugin.java
+++ b/src/main/java/moe/nea/firmament/init/MixinPlugin.java
@@ -8,54 +8,69 @@ import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin;
import org.spongepowered.asm.mixin.extensibility.IMixinInfo;
import java.util.ArrayList;
+import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
public class MixinPlugin implements IMixinConfigPlugin {
- AutoDiscoveryPlugin autoDiscoveryPlugin = new AutoDiscoveryPlugin();
- public static String mixinPackage;
- @Override
- public void onLoad(String mixinPackage) {
- MixinExtrasBootstrap.init();
- MixinPlugin.mixinPackage = mixinPackage;
- autoDiscoveryPlugin.setMixinPackage(mixinPackage);
- }
-
- @Override
- public String getRefMapperConfig() {
- return null;
- }
-
- @Override
- public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
- if (!Boolean.getBoolean("firmament.debug") && mixinClassName.contains("devenv.")) {
- return false;
- }
- return true;
- }
-
- @Override
- public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
-
- }
-
- @Override
- public List<String> getMixins() {
- return autoDiscoveryPlugin.getMixins().stream().filter(it -> this.shouldApplyMixin(null, it))
- .toList();
- }
-
- @Override
- public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
-
- }
-
- public static List<String> appliedMixins = new ArrayList<>();
-
- @Override
- public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
- appliedMixins.add(mixinClassName);
- }
+ AutoDiscoveryPlugin autoDiscoveryPlugin = new AutoDiscoveryPlugin();
+ public static List<MixinPlugin> instances = new ArrayList<>();
+ public String mixinPackage;
+
+ @Override
+ public void onLoad(String mixinPackage) {
+ MixinExtrasBootstrap.init();
+ instances.add(this);
+ this.mixinPackage = mixinPackage;
+ autoDiscoveryPlugin.setMixinPackage(mixinPackage);
+ }
+
+ @Override
+ public String getRefMapperConfig() {
+ return null;
+ }
+
+ @Override
+ public boolean shouldApplyMixin(String targetClassName, String mixinClassName) {
+ if (!Boolean.getBoolean("firmament.debug") && mixinClassName.contains("devenv.")) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public void acceptTargets(Set<String> myTargets, Set<String> otherTargets) {
+
+ }
+
+ @Override
+ public List<String> getMixins() {
+ return autoDiscoveryPlugin.getMixins().stream().filter(it -> this.shouldApplyMixin(null, it))
+ .toList();
+ }
+
+ @Override
+ public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
+
+ }
+
+ public Set<String> getAppliedFullPathMixins() {
+ return new HashSet<>(appliedMixins);
+ }
+
+ public Set<String> getExpectedFullPathMixins() {
+ return getMixins()
+ .stream()
+ .map(it -> mixinPackage + "." + it)
+ .collect(Collectors.toSet());
+ }
+
+ public List<String> appliedMixins = new ArrayList<>();
+
+ @Override
+ public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) {
+ appliedMixins.add(mixinClassName);
+ }
}
diff --git a/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java b/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java
index f2c6c53..8b65946 100644
--- a/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java
+++ b/src/main/java/moe/nea/firmament/init/SectionBuilderRiser.java
@@ -3,10 +3,9 @@ package moe.nea.firmament.init;
import me.shedaniel.mm.api.ClassTinkerers;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.block.BlockState;
-import net.minecraft.client.render.block.BlockModels;
import net.minecraft.client.render.block.BlockRenderManager;
import net.minecraft.client.render.chunk.SectionBuilder;
-import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.BlockStateModel;
import net.minecraft.util.math.BlockPos;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
@@ -20,98 +19,100 @@ import org.objectweb.asm.tree.VarInsnNode;
public class SectionBuilderRiser extends RiserUtils {
- @IntermediaryName(SectionBuilder.class)
- String SectionBuilder;
- @IntermediaryName(BlockPos.class)
- String BlockPos;
- @IntermediaryName(BlockRenderManager.class)
- String BlockRenderManager;
- @IntermediaryName(BlockState.class)
- String BlockState;
- @IntermediaryName(BakedModel.class)
- String BakedModel;
- String CustomBlockTextures = "moe.nea.firmament.features.texturepack.CustomBlockTextures";
+ @IntermediaryName(SectionBuilder.class)
+ String SectionBuilder;
+ @IntermediaryName(BlockPos.class)
+ String BlockPos;
+ @IntermediaryName(BlockRenderManager.class)
+ String BlockRenderManager;
+ @IntermediaryName(BlockState.class)
+ String BlockState;
+ @IntermediaryName(BlockStateModel.class)
+ String BlockStateModel;
+ String CustomBlockTextures = "moe.nea.firmament.features.texturepack.CustomBlockTextures";
- Type getModelDesc = Type.getMethodType(
- getTypeForClassName(BlockRenderManager),
- getTypeForClassName(BlockState)
- );
- String getModel = remapper.mapMethodName(
- "intermediary",
- Intermediary.<BlockRenderManager>className(),
- Intermediary.methodName(net.minecraft.client.render.block.BlockRenderManager::getModel),
- Type.getMethodDescriptor(
- getTypeForClassName(Intermediary.<BakedModel>className()),
- getTypeForClassName(Intermediary.<BlockState>className())
- )
- );
+ Type getModelDesc = Type.getMethodType(
+ getTypeForClassName(BlockRenderManager),
+ getTypeForClassName(BlockState)
+ );
+ String getModel = remapper.mapMethodName(
+ "intermediary",
+ Intermediary.<BlockRenderManager>className(),
+ Intermediary.methodName(net.minecraft.client.render.block.BlockRenderManager::getModel),
+ Type.getMethodDescriptor(
+ getTypeForClassName(Intermediary.<BlockStateModel>className()),
+ getTypeForClassName(Intermediary.<BlockState>className())
+ )
+ );
- @Override
- public void addTinkerers() {
- if (FabricLoader.getInstance().isModLoaded("fabric-renderer-indigo"))
- ClassTinkerers.addTransformation(SectionBuilder, this::handle, true);
- }
+ @Override
+ public void addTinkerers() {
+ if (FabricLoader.getInstance().isModLoaded("fabric-renderer-indigo"))
+ ClassTinkerers.addTransformation(SectionBuilder, this::handle, true);
+ }
- private void handle(ClassNode classNode) {
- for (MethodNode method : classNode.methods) {
- if ((method.name.endsWith("$fabric-renderer-indigo$hookBuildRenderBlock")
- || method.name.endsWith("$fabric-renderer-indigo$hookChunkBuildTessellate")) &&
- method.name.startsWith("redirect$")) {
- handleIndigo(method);
- return;
- }
- }
- System.err.println("Could not inject indigo rendering hook. Is a custom renderer installed (e.g. sodium)?");
- }
+ private void handle(ClassNode classNode) {
+ System.out.println("AVAST! "+ getModel);
+ for (MethodNode method : classNode.methods) {
+ if ((method.name.endsWith("$fabric-renderer-indigo$hookBuildRenderBlock")
+ || method.name.endsWith("$fabric-renderer-indigo$hookChunkBuildTessellate")) &&
+ method.name.startsWith("redirect$")) {
+ handleIndigo(method);
+ return;
+ }
+ }
+ System.err.println("Could not inject indigo rendering hook. Is a custom renderer installed (e.g. sodium)?");
+ }
- private void handleIndigo(MethodNode method) {
- LocalVariableNode blockPosVar = null, blockStateVar = null;
- for (LocalVariableNode localVariable : method.localVariables) {
- if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockPos))) {
- blockPosVar = localVariable;
- }
- if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockState))) {
- blockStateVar = localVariable;
- }
- }
- if (blockPosVar == null || blockStateVar == null) {
- System.err.println("Firmament could inject into indigo: missing either block pos or blockstate");
- return;
- }
- for (AbstractInsnNode instruction : method.instructions) {
- if (instruction.getOpcode() != Opcodes.INVOKEVIRTUAL) continue;
- var methodInsn = (MethodInsnNode) instruction;
- if (!(methodInsn.name.equals(getModel) && Type.getObjectType(methodInsn.owner).equals(getTypeForClassName(BlockRenderManager))))
- continue;
- method.instructions.insertBefore(
- methodInsn,
- new MethodInsnNode(
- Opcodes.INVOKESTATIC,
- getTypeForClassName(CustomBlockTextures).getInternalName(),
- "enterFallbackCall",
- Type.getMethodDescriptor(Type.VOID_TYPE)
- ));
+ private void handleIndigo(MethodNode method) {
+ LocalVariableNode blockPosVar = null, blockStateVar = null;
+ for (LocalVariableNode localVariable : method.localVariables) {
+ if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockPos))) {
+ blockPosVar = localVariable;
+ }
+ if (Type.getType(localVariable.desc).equals(getTypeForClassName(BlockState))) {
+ blockStateVar = localVariable;
+ }
+ }
+ if (blockPosVar == null || blockStateVar == null) {
+ System.err.println("Firmament could inject into indigo: missing either block pos or blockstate");
+ return;
+ }
+ for (AbstractInsnNode instruction : method.instructions) {
+ if (instruction.getOpcode() != Opcodes.INVOKEVIRTUAL) continue;
+ var methodInsn = (MethodInsnNode) instruction;
+ if (!(methodInsn.name.equals(getModel) && Type.getObjectType(methodInsn.owner).equals(getTypeForClassName(BlockRenderManager))))
+ continue;
+ method.instructions.insertBefore(
+ methodInsn,
+ new MethodInsnNode(
+ Opcodes.INVOKESTATIC,
+ getTypeForClassName(CustomBlockTextures).getInternalName(),
+ "enterFallbackCall",
+ Type.getMethodDescriptor(Type.VOID_TYPE)
+ ));
- var insnList = new InsnList();
- insnList.add(new MethodInsnNode(
- Opcodes.INVOKESTATIC,
- getTypeForClassName(CustomBlockTextures).getInternalName(),
- "exitFallbackCall",
- Type.getMethodDescriptor(Type.VOID_TYPE)
- ));
- insnList.add(new VarInsnNode(Opcodes.ALOAD, blockPosVar.index));
- insnList.add(new VarInsnNode(Opcodes.ALOAD, blockStateVar.index));
- insnList.add(new MethodInsnNode(
- Opcodes.INVOKESTATIC,
- getTypeForClassName(CustomBlockTextures).getInternalName(),
- "patchIndigo",
- Type.getMethodDescriptor(getTypeForClassName(BakedModel),
- getTypeForClassName(BakedModel),
- getTypeForClassName(BlockPos),
- getTypeForClassName(BlockState)),
- false
- ));
- method.instructions.insert(methodInsn, insnList);
- }
- }
+ var insnList = new InsnList();
+ insnList.add(new MethodInsnNode(
+ Opcodes.INVOKESTATIC,
+ getTypeForClassName(CustomBlockTextures).getInternalName(),
+ "exitFallbackCall",
+ Type.getMethodDescriptor(Type.VOID_TYPE)
+ ));
+ insnList.add(new VarInsnNode(Opcodes.ALOAD, blockPosVar.index));
+ insnList.add(new VarInsnNode(Opcodes.ALOAD, blockStateVar.index));
+ insnList.add(new MethodInsnNode(
+ Opcodes.INVOKESTATIC,
+ getTypeForClassName(CustomBlockTextures).getInternalName(),
+ "patchIndigo",
+ Type.getMethodDescriptor(
+ getTypeForClassName(BlockStateModel),
+ getTypeForClassName(BlockStateModel),
+ getTypeForClassName(BlockPos),
+ getTypeForClassName(BlockState)),
+ false
+ ));
+ method.instructions.insert(methodInsn, insnList);
+ }
+ }
}
diff --git a/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java b/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java
index 22ce991..d8e35d7 100644
--- a/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java
+++ b/src/main/java/moe/nea/firmament/mixins/AppendRepoAsResourcePack.java
@@ -1,28 +1,34 @@
package moe.nea.firmament.mixins;
+import com.llamalad7.mixinextras.sugar.Local;
import moe.nea.firmament.repo.RepoModResourcePack;
import net.fabricmc.fabric.api.resource.ModResourcePack;
+import net.fabricmc.fabric.impl.resource.loader.ModResourcePackSorter;
import net.fabricmc.fabric.impl.resource.loader.ModResourcePackUtil;
+import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.resource.ResourceType;
import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
import java.util.List;
@Mixin(ModResourcePackUtil.class)
public class AppendRepoAsResourcePack {
- @Inject(method = "appendModResourcePacks", at = @At("TAIL"))
- private static void onAppendModResourcePack(
- List<ModResourcePack> packs,
- ResourceType type,
- @Nullable String subPath,
- CallbackInfo ci
- ) {
- RepoModResourcePack.Companion.append(packs);
- }
+ @Inject(
+ method = "getModResourcePacks",
+ at = @At(value = "INVOKE", target = "Lnet/fabricmc/fabric/impl/resource/loader/ModResourcePackSorter;getPacks()Ljava/util/List;"),
+ require = 0
+ )
+ private static void onAppendModResourcePack(
+ FabricLoader fabricLoader, ResourceType type, @Nullable String subPath, CallbackInfoReturnable<List<ModResourcePack>> cir,
+ @Local ModResourcePackSorter sorter
+ ) {
+ RepoModResourcePack.Companion.append(sorter);
+ }
}
diff --git a/src/main/java/moe/nea/firmament/mixins/CopyChatPatch.java b/src/main/java/moe/nea/firmament/mixins/CopyChatPatch.java
new file mode 100644
index 0000000..6996818
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/CopyChatPatch.java
@@ -0,0 +1,44 @@
+package moe.nea.firmament.mixins;
+
+import moe.nea.firmament.features.chat.CopyChat;
+import moe.nea.firmament.mixins.accessor.AccessorChatHud;
+import moe.nea.firmament.util.ClipboardUtils;
+import net.minecraft.client.MinecraftClient;
+import net.minecraft.client.gui.hud.ChatHud;
+import net.minecraft.client.gui.hud.ChatHudLine;
+import net.minecraft.client.gui.screen.ChatScreen;
+import net.minecraft.text.Text;
+import net.minecraft.util.Formatting;
+import net.minecraft.util.math.MathHelper;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+import java.util.List;
+
+@Mixin(ChatScreen.class)
+public class CopyChatPatch {
+ @Inject(method = "mouseClicked", at = @At("HEAD"), cancellable = true)
+ private void onRightClick(double mouseX, double mouseY, int button, CallbackInfoReturnable<Boolean> cir) throws NoSuchFieldException, IllegalAccessException {
+ if (button != 1 || !CopyChat.TConfig.INSTANCE.getCopyChat()) return;
+ MinecraftClient client = MinecraftClient.getInstance();
+ ChatHud chatHud = client.inGameHud.getChatHud();
+ int lineIndex = getChatLineIndex(chatHud, mouseY);
+ if (lineIndex < 0) return;
+ List<ChatHudLine.Visible> visible = ((AccessorChatHud) chatHud).getVisibleMessages_firmament();
+ if (lineIndex >= visible.size()) return;
+ ChatHudLine.Visible line = visible.get(lineIndex);
+ String text = CopyChat.INSTANCE.orderedTextToString(line.content());
+ ClipboardUtils.INSTANCE.setTextContent(text);
+ chatHud.addMessage(Text.literal("Copied: ").append(text).formatted(Formatting.GRAY));
+ cir.setReturnValue(true);
+ cir.cancel();
+ }
+
+ @Unique
+ private int getChatLineIndex(ChatHud chatHud, double mouseY) {
+ double chatLineY = ((AccessorChatHud) chatHud).toChatLineY_firmament(mouseY);
+ return MathHelper.floor(chatLineY + ((AccessorChatHud) chatHud).getScrolledLines_firmament());
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/DisableHurtCam.java b/src/main/java/moe/nea/firmament/mixins/DisableHurtCam.java
new file mode 100644
index 0000000..ed7a2d4
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/DisableHurtCam.java
@@ -0,0 +1,18 @@
+package moe.nea.firmament.mixins;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import moe.nea.firmament.features.fixes.Fixes;
+import net.minecraft.client.render.GameRenderer;
+import org.objectweb.asm.Opcodes;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(GameRenderer.class)
+public class DisableHurtCam {
+ @ModifyExpressionValue(method = "tiltViewWhenHurt", at = @At(value = "FIELD", target = "Lnet/minecraft/entity/LivingEntity;hurtTime:I", opcode = Opcodes.GETFIELD))
+ private int replaceHurtTime(int original) {
+ if (Fixes.TConfig.INSTANCE.getNoHurtCam())
+ return 0;
+ return original;
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java b/src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java
new file mode 100644
index 0000000..f1b07bb
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/DispatchMouseInputEventsPatch.java
@@ -0,0 +1,17 @@
+package moe.nea.firmament.mixins;
+
+import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
+import moe.nea.firmament.events.WorldMouseMoveEvent;
+import net.minecraft.client.Mouse;
+import net.minecraft.client.network.ClientPlayerEntity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(Mouse.class)
+public class DispatchMouseInputEventsPatch {
+ @WrapWithCondition(method = "updateMouse", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/network/ClientPlayerEntity;changeLookDirection(DD)V"))
+ public boolean onRotatePlayer(ClientPlayerEntity instance, double deltaX, double deltaY) {
+ var event = WorldMouseMoveEvent.Companion.publish(new WorldMouseMoveEvent(deltaX, deltaY));
+ return !event.getCancelled();
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java b/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java
index c2d6e46..d956da9 100644
--- a/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java
+++ b/src/main/java/moe/nea/firmament/mixins/EntityUpdateEventListener.java
@@ -12,6 +12,7 @@ import net.minecraft.entity.Entity;
import net.minecraft.entity.LivingEntity;
import net.minecraft.network.ClientConnection;
import net.minecraft.network.packet.s2c.play.EntityAttributesS2CPacket;
+import net.minecraft.network.packet.s2c.play.EntityEquipmentUpdateS2CPacket;
import net.minecraft.network.packet.s2c.play.EntityTrackerUpdateS2CPacket;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
@@ -22,21 +23,26 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ClientPlayNetworkHandler.class)
public abstract class EntityUpdateEventListener extends ClientCommonNetworkHandler {
- @Shadow
- private ClientWorld world;
+ @Shadow
+ private ClientWorld world;
- protected EntityUpdateEventListener(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) {
- super(client, connection, connectionState);
- }
+ protected EntityUpdateEventListener(MinecraftClient client, ClientConnection connection, ClientConnectionState connectionState) {
+ super(client, connection, connectionState);
+ }
- @Inject(method = "onEntityAttributes", at = @At("TAIL"))
- private void onAttributeUpdate(EntityAttributesS2CPacket packet, CallbackInfo ci) {
- EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.AttributeUpdate(
- (LivingEntity) world.getEntityById(packet.getEntityId()), packet.getEntries()));
- }
+ @Inject(method = "onEntityEquipmentUpdate", at = @At(value = "INVOKE", target = "Ljava/util/List;forEach(Ljava/util/function/Consumer;)V", shift = At.Shift.AFTER))
+ private void onEquipmentUpdate(EntityEquipmentUpdateS2CPacket packet, CallbackInfo ci, @Local LivingEntity entity) {
+ EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.EquipmentUpdate(entity, packet.getEquipmentList()));
+ }
- @Inject(method = "onEntityTrackerUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/data/DataTracker;writeUpdatedEntries(Ljava/util/List;)V", shift = At.Shift.AFTER))
- private void onEntityTracker(EntityTrackerUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) {
- EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.TrackedDataUpdate(entity, packet.trackedValues()));
- }
+ @Inject(method = "onEntityAttributes", at = @At("TAIL"))
+ private void onAttributeUpdate(EntityAttributesS2CPacket packet, CallbackInfo ci) {
+ EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.AttributeUpdate(
+ (LivingEntity) world.getEntityById(packet.getEntityId()), packet.getEntries()));
+ }
+
+ @Inject(method = "onEntityTrackerUpdate", at = @At(value = "INVOKE", target = "Lnet/minecraft/entity/data/DataTracker;writeUpdatedEntries(Ljava/util/List;)V", shift = At.Shift.AFTER))
+ private void onEntityTracker(EntityTrackerUpdateS2CPacket packet, CallbackInfo ci, @Local Entity entity) {
+ EntityUpdateEvent.Companion.publish(new EntityUpdateEvent.TrackedDataUpdate(entity, packet.trackedValues()));
+ }
}
diff --git a/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java b/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java
index 85c0462..49e86fb 100644
--- a/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java
+++ b/src/main/java/moe/nea/firmament/mixins/HudRenderEventsPatch.java
@@ -4,6 +4,7 @@ package moe.nea.firmament.mixins;
import moe.nea.firmament.events.HotbarItemRenderEvent;
import moe.nea.firmament.events.HudRenderEvent;
+import moe.nea.firmament.features.fixes.Fixes;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.hud.InGameHud;
import net.minecraft.client.render.RenderTickCounter;
@@ -26,4 +27,10 @@ public class HudRenderEventsPatch {
if (stack != null && !stack.isEmpty())
HotbarItemRenderEvent.Companion.publish(new HotbarItemRenderEvent(stack, context, x, y, tickCounter));
}
+
+ @Inject(method = "renderStatusEffectOverlay", at = @At("HEAD"), cancellable = true)
+ public void hideStatusEffects(CallbackInfo ci) {
+ if (Fixes.TConfig.INSTANCE.getHidePotionEffectsHud()) ci.cancel();
+ }
+
}
diff --git a/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java b/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java
index 48f3c23..d2b3f91 100644
--- a/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java
+++ b/src/main/java/moe/nea/firmament/mixins/KeyPressInWorldEventPatch.java
@@ -2,18 +2,19 @@
package moe.nea.firmament.mixins;
+import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import moe.nea.firmament.events.WorldKeyboardEvent;
import net.minecraft.client.Keyboard;
+import net.minecraft.client.util.InputUtil;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(Keyboard.class)
public class KeyPressInWorldEventPatch {
- @Inject(method = "onKey", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/option/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V"))
- public void onKeyBoardInWorld(long window, int key, int scancode, int action, int modifiers, CallbackInfo ci) {
- WorldKeyboardEvent.Companion.publish(new WorldKeyboardEvent(key, scancode, modifiers));
- }
+ @WrapWithCondition(method = "onKey", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/option/KeyBinding;onKeyPressed(Lnet/minecraft/client/util/InputUtil$Key;)V"))
+ public boolean onKeyBoardInWorld(InputUtil.Key key, long window, int _key, int scancode, int action, int modifiers) {
+ var event = WorldKeyboardEvent.Companion.publish(new WorldKeyboardEvent(_key, scancode, modifiers));
+ return !event.getCancelled();
+ }
}
diff --git a/src/main/java/moe/nea/firmament/mixins/LenientProfileComponentPatch.java b/src/main/java/moe/nea/firmament/mixins/LenientProfileComponentPatch.java
deleted file mode 100644
index 76b34ba..0000000
--- a/src/main/java/moe/nea/firmament/mixins/LenientProfileComponentPatch.java
+++ /dev/null
@@ -1,25 +0,0 @@
-
-package moe.nea.firmament.mixins;
-
-import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
-import com.mojang.serialization.Codec;
-import com.mojang.serialization.DataResult;
-import com.mojang.serialization.Lifecycle;
-import com.mojang.util.UndashedUuid;
-import moe.nea.firmament.util.json.FirmCodecs;
-import net.minecraft.component.type.ProfileComponent;
-import net.minecraft.util.Uuids;
-import org.objectweb.asm.Opcodes;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.injection.At;
-
-import java.util.UUID;
-
-@Mixin(ProfileComponent.class)
-public class LenientProfileComponentPatch {
- // lambda in RecordCodecBuilder.create for BASE_CODEC
- @ModifyExpressionValue(method = "method_57508", at = @At(value = "FIELD", opcode = Opcodes.GETSTATIC, target = "Lnet/minecraft/util/Uuids;INT_STREAM_CODEC:Lcom/mojang/serialization/Codec;"))
- private static Codec<UUID> onStaticInit(Codec<UUID> original) {
- return FirmCodecs.UUID_LENIENT_PREFER_INT_STREAM;
- }
-}
diff --git a/src/main/java/moe/nea/firmament/mixins/MinecraftInitLevelListener.java b/src/main/java/moe/nea/firmament/mixins/MinecraftInitLevelListener.java
new file mode 100644
index 0000000..1673987
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/MinecraftInitLevelListener.java
@@ -0,0 +1,26 @@
+package moe.nea.firmament.mixins;
+
+import moe.nea.firmament.util.mc.InitLevel;
+import net.minecraft.client.MinecraftClient;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(MinecraftClient.class)
+public class MinecraftInitLevelListener {
+ @Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;initBackendSystem()Lnet/minecraft/util/TimeSupplier$Nanoseconds;"))
+ private void onInitRenderBackend(CallbackInfo ci) {
+ InitLevel.bump(InitLevel.RENDER_INIT);
+ }
+
+ @Inject(method = "<init>", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;initRenderer(JIZLjava/util/function/BiFunction;Z)V"))
+ private void onInitRender(CallbackInfo ci) {
+ InitLevel.bump(InitLevel.RENDER);
+ }
+
+ @Inject(method = "<init>", at = @At(value = "TAIL"))
+ private void onFinishedLoading(CallbackInfo ci) {
+ InitLevel.bump(InitLevel.MAIN_MENU);
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java b/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java
index e607ba3..43aec40 100644
--- a/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java
+++ b/src/main/java/moe/nea/firmament/mixins/MixinHandledScreen.java
@@ -4,8 +4,11 @@ package moe.nea.firmament.mixins;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
-import com.llamalad7.mixinextras.sugar.Local;
-import moe.nea.firmament.events.*;
+import moe.nea.firmament.events.HandledScreenClickEvent;
+import moe.nea.firmament.events.HandledScreenForegroundEvent;
+import moe.nea.firmament.events.HandledScreenKeyPressedEvent;
+import moe.nea.firmament.events.IsSlotProtectedEvent;
+import moe.nea.firmament.events.SlotRenderEvents;
import net.minecraft.client.gui.DrawContext;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
import net.minecraft.entity.player.PlayerInventory;
@@ -22,9 +25,6 @@ import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
-import org.spongepowered.asm.mixin.injection.callback.LocalCapture;
-
-import java.util.Iterator;
@Mixin(value = HandledScreen.class, priority = 990)
public abstract class MixinHandledScreen<T extends ScreenHandler> {
@@ -74,17 +74,17 @@ public abstract class MixinHandledScreen<T extends ScreenHandler> {
public void onMouseClickedSlot(Slot slot, int slotId, int button, SlotActionType actionType, CallbackInfo ci) {
if (slotId == -999 && getScreenHandler() != null && actionType == SlotActionType.PICKUP) { // -999 is code for "clicked outside the main window"
ItemStack cursorStack = getScreenHandler().getCursorStack();
- if (cursorStack != null && IsSlotProtectedEvent.shouldBlockInteraction(slot, SlotActionType.THROW, cursorStack)) {
+ if (cursorStack != null && IsSlotProtectedEvent.shouldBlockInteraction(slot, SlotActionType.THROW, IsSlotProtectedEvent.MoveOrigin.INVENTORY_MOVE, cursorStack)) {
ci.cancel();
return;
}
}
- if (IsSlotProtectedEvent.shouldBlockInteraction(slot, actionType)) {
+ if (IsSlotProtectedEvent.shouldBlockInteraction(slot, actionType, IsSlotProtectedEvent.MoveOrigin.INVENTORY_MOVE)) {
ci.cancel();
return;
}
if (actionType == SlotActionType.SWAP && 0 <= button && button < 9) {
- if (IsSlotProtectedEvent.shouldBlockInteraction(new Slot(playerInventory, button, 0, 0), actionType)) {
+ if (IsSlotProtectedEvent.shouldBlockInteraction(new Slot(playerInventory, button, 0, 0), actionType, IsSlotProtectedEvent.MoveOrigin.INVENTORY_MOVE)) {
ci.cancel();
}
}
diff --git a/src/main/java/moe/nea/firmament/mixins/MixinRecipeBookScreen.java b/src/main/java/moe/nea/firmament/mixins/MixinRecipeBookScreen.java
new file mode 100644
index 0000000..2dbe738
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/MixinRecipeBookScreen.java
@@ -0,0 +1,16 @@
+package moe.nea.firmament.mixins;
+
+import moe.nea.firmament.features.fixes.Fixes;
+import net.minecraft.client.gui.screen.ingame.RecipeBookScreen;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(value = RecipeBookScreen.class, priority = 999)
+public class MixinRecipeBookScreen {
+ @Inject(method = "addRecipeBook", at = @At("HEAD"), cancellable = true)
+ public void addRecipeBook(CallbackInfo ci) {
+ if (Fixes.TConfig.INSTANCE.getHideRecipeBook()) ci.cancel();
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java b/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java
index 9a4626f..f07604e 100644
--- a/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java
+++ b/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java
@@ -14,15 +14,15 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
@Mixin(ClientPlayerEntity.class)
public abstract class PlayerDropEventPatch extends PlayerEntity {
- public PlayerDropEventPatch() {
- super(null, null, 0, null);
- }
+ public PlayerDropEventPatch() {
+ super(null, null, 0, null);
+ }
- @Inject(method = "dropSelectedItem", at = @At("HEAD"), cancellable = true)
- public void onDropSelectedItem(boolean entireStack, CallbackInfoReturnable<Boolean> cir) {
- Slot fakeSlot = new Slot(getInventory(), getInventory().selectedSlot, 0, 0);
- if (IsSlotProtectedEvent.shouldBlockInteraction(fakeSlot, SlotActionType.THROW)) {
- cir.setReturnValue(false);
- }
- }
+ @Inject(method = "dropSelectedItem", at = @At("HEAD"), cancellable = true)
+ public void onDropSelectedItem(boolean entireStack, CallbackInfoReturnable<Boolean> cir) {
+ Slot fakeSlot = new Slot(getInventory(), getInventory().getSelectedSlot(), 0, 0);
+ if (IsSlotProtectedEvent.shouldBlockInteraction(fakeSlot, SlotActionType.THROW, IsSlotProtectedEvent.MoveOrigin.DROP_FROM_HOTBAR)) {
+ cir.setReturnValue(false);
+ }
+ }
}
diff --git a/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatch.java b/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatch.java
deleted file mode 100644
index e7331c5..0000000
--- a/src/main/java/moe/nea/firmament/mixins/PropertySignatureIgnorePatch.java
+++ /dev/null
@@ -1,36 +0,0 @@
-
-
-package moe.nea.firmament.mixins;
-
-import com.mojang.authlib.properties.Property;
-import moe.nea.firmament.features.fixes.Fixes;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
-
-import java.security.PublicKey;
-
-@Mixin(value = Property.class, remap = false)
-public class PropertySignatureIgnorePatch {
- @Inject(method = "isSignatureValid", cancellable = true, at = @At("HEAD"), remap = false)
- public void onValidateSignature(PublicKey publicKey, CallbackInfoReturnable<Boolean> cir) {
- if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) {
- cir.setReturnValue(true);
- }
- }
-
- @Inject(method = "signature", cancellable = true, at = @At("HEAD"), remap = false)
- public void returnEmptySignatureInsteadOfNull(CallbackInfoReturnable<String> cir) {
- if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) {
- cir.setReturnValue("");
- }
- }
-
- @Inject(method = "hasSignature", cancellable = true, at = @At("HEAD"), remap = false)
- public void onHasSignature(CallbackInfoReturnable<Boolean> cir) {
- if (Fixes.TConfig.INSTANCE.getFixUnsignedPlayerSkins()) {
- cir.setReturnValue(true);
- }
- }
-}
diff --git a/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java b/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java
index 06ecbd4..a4ae931 100644
--- a/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java
+++ b/src/main/java/moe/nea/firmament/mixins/SlotUpdateListener.java
@@ -43,11 +43,11 @@ public abstract class SlotUpdateListener extends ClientCommonNetworkHandler {
private void onMultiSlotUpdate(InventoryS2CPacket packet, CallbackInfo ci) {
var player = this.client.player;
assert player != null;
- if (packet.getSyncId() == 0) {
- PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Multi(packet.getContents()));
- } else if (packet.getSyncId() == player.currentScreenHandler.syncId) {
+ if (packet.syncId() == 0) {
+ PlayerInventoryUpdate.Companion.publish(new PlayerInventoryUpdate.Multi(packet.contents()));
+ } else if (packet.syncId() == player.currentScreenHandler.syncId) {
ChestInventoryUpdateEvent.Companion.publish(
- new ChestInventoryUpdateEvent.Multi(packet.getContents())
+ new ChestInventoryUpdateEvent.Multi(packet.contents())
);
}
}
diff --git a/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java b/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java
index 5c52d70..b8cba80 100644
--- a/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java
+++ b/src/main/java/moe/nea/firmament/mixins/SoundReceiveEventPatch.java
@@ -1,30 +1,32 @@
package moe.nea.firmament.mixins;
+import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
import moe.nea.firmament.events.SoundReceiveEvent;
import net.minecraft.client.network.ClientPlayNetworkHandler;
-import net.minecraft.network.packet.s2c.play.PlaySoundS2CPacket;
+import net.minecraft.client.world.ClientWorld;
+import net.minecraft.entity.Entity;
+import net.minecraft.registry.entry.RegistryEntry;
+import net.minecraft.sound.SoundCategory;
+import net.minecraft.sound.SoundEvent;
import net.minecraft.util.math.Vec3d;
+import org.jetbrains.annotations.Nullable;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
@Mixin(ClientPlayNetworkHandler.class)
public class SoundReceiveEventPatch {
- @Inject(method = "onPlaySound", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientWorld;playSound(Lnet/minecraft/entity/player/PlayerEntity;DDDLnet/minecraft/registry/entry/RegistryEntry;Lnet/minecraft/sound/SoundCategory;FFJ)V"), cancellable = true)
- private void postEventWhenSoundIsPlayed(PlaySoundS2CPacket packet, CallbackInfo ci) {
+ @WrapWithCondition(method = "onPlaySound", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/world/ClientWorld;playSound(Lnet/minecraft/entity/Entity;DDDLnet/minecraft/registry/entry/RegistryEntry;Lnet/minecraft/sound/SoundCategory;FFJ)V"))
+ private boolean postEventWhenSoundIsPlayed(ClientWorld instance, @Nullable Entity source, double x, double y, double z, RegistryEntry<SoundEvent> sound, SoundCategory category, float volume, float pitch, long seed) {
var event = new SoundReceiveEvent(
- packet.getSound(),
- packet.getCategory(),
- new Vec3d(packet.getX(), packet.getY(), packet.getZ()),
- packet.getPitch(),
- packet.getVolume(),
- packet.getSeed()
+ sound,
+ category,
+ new Vec3d(x,y,z),
+ pitch,
+ volume,
+ seed
);
SoundReceiveEvent.Companion.publish(event);
- if (event.getCancelled()) {
- ci.cancel();
- }
+ return !event.getCancelled();
}
}
diff --git a/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java b/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java
index 847fb4d..3ed8c1b 100644
--- a/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java
+++ b/src/main/java/moe/nea/firmament/mixins/WorldRenderLastEventPatch.java
@@ -2,11 +2,9 @@
package moe.nea.firmament.mixins;
-import com.llamalad7.mixinextras.sugar.Local;
import moe.nea.firmament.events.WorldRenderLastEvent;
import net.minecraft.client.render.*;
import net.minecraft.client.util.Handle;
-import net.minecraft.client.util.ObjectAllocator;
import net.minecraft.client.util.math.MatrixStack;
import net.minecraft.util.profiler.Profiler;
import org.joml.Matrix4f;
@@ -31,12 +29,12 @@ public abstract class WorldRenderLastEventPatch {
protected abstract void checkEmpty(MatrixStack matrices);
@Inject(method = "method_62214", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/profiler/Profiler;pop()V", shift = At.Shift.AFTER))
- public void onWorldRenderLast(Fog fog, RenderTickCounter tickCounter, Camera camera, Profiler profiler, Matrix4f matrix4f, Matrix4f matrix4f2, Handle handle, Handle handle2, Handle handle3, Handle handle4, boolean bl, Frustum frustum, Handle handle5, CallbackInfo ci) {
+ public void onWorldRenderLast(Fog fog, RenderTickCounter renderTickCounter, Camera camera, Profiler profiler, Matrix4f matrix4f, Matrix4f matrix4f2, Handle handle, Handle handle2, boolean bl, Frustum frustum, Handle handle3, Handle handle4, CallbackInfo ci) {
var imm = this.bufferBuilders.getEntityVertexConsumers();
var stack = new MatrixStack();
// TODO: pre-cancel this event if F1 is active
var event = new WorldRenderLastEvent(
- stack, tickCounter,
+ stack, renderTickCounter,
camera,
imm
);
diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java
index 72a72f0..d164aac 100644
--- a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java
+++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorChatHud.java
@@ -4,6 +4,7 @@ import net.minecraft.client.gui.hud.ChatHud;
import net.minecraft.client.gui.hud.ChatHudLine;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.gen.Accessor;
+import org.spongepowered.asm.mixin.gen.Invoker;
import java.util.List;
@@ -11,4 +12,13 @@ import java.util.List;
public interface AccessorChatHud {
@Accessor("messages")
List<ChatHudLine> getMessages_firmament();
+
+ @Accessor("visibleMessages")
+ List<ChatHudLine.Visible> getVisibleMessages_firmament();
+
+ @Accessor("scrolledLines")
+ int getScrolledLines_firmament();
+
+ @Invoker("toChatLineY")
+ double toChatLineY_firmament(double y);
}
diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java
index 7ed04b1..f55ef4f 100644
--- a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java
+++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorHandledScreen.java
@@ -1,5 +1,3 @@
-
-
package moe.nea.firmament.mixins.accessor;
import net.minecraft.client.gui.screen.ingame.HandledScreen;
diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java
new file mode 100644
index 0000000..81ea0fd
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorPlayerListHud.java
@@ -0,0 +1,31 @@
+package moe.nea.firmament.mixins.accessor;
+
+import net.minecraft.client.gui.hud.PlayerListHud;
+import net.minecraft.client.network.PlayerListEntry;
+import net.minecraft.text.Text;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+import org.spongepowered.asm.mixin.gen.Invoker;
+
+import java.util.Comparator;
+import java.util.List;
+
+@Mixin(PlayerListHud.class)
+public interface AccessorPlayerListHud {
+
+ @Accessor("ENTRY_ORDERING")
+ static Comparator<PlayerListEntry> getEntryOrdering() {
+ throw new AssertionError();
+ }
+
+ @Invoker("collectPlayerEntries")
+ List<PlayerListEntry> collectPlayerEntries_firmament();
+
+ @Accessor("footer")
+ @Nullable Text getFooter_firmament();
+
+ @Accessor("header")
+ @Nullable Text getHeader_firmament();
+
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/accessor/AccessorWorldRenderer.java b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorWorldRenderer.java
new file mode 100644
index 0000000..8b25562
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/accessor/AccessorWorldRenderer.java
@@ -0,0 +1,17 @@
+package moe.nea.firmament.mixins.accessor;
+
+import it.unimi.dsi.fastutil.longs.Long2ObjectMap;
+import net.minecraft.client.render.WorldRenderer;
+import net.minecraft.entity.player.BlockBreakingInfo;
+import org.jetbrains.annotations.NotNull;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.gen.Accessor;
+
+import java.util.SortedSet;
+
+@Mixin(WorldRenderer.class)
+public interface AccessorWorldRenderer {
+ @Accessor("blockBreakingProgressions")
+ @NotNull
+ Long2ObjectMap<SortedSet<BlockBreakingInfo>> getBlockBreakingProgressions_firmament();
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java b/src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java
new file mode 100644
index 0000000..0abed22
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/feature/DisableSlotHighlights.java
@@ -0,0 +1,25 @@
+package moe.nea.firmament.mixins.feature;
+
+import moe.nea.firmament.features.fixes.Fixes;
+import net.minecraft.component.DataComponentTypes;
+import net.minecraft.item.ItemStack;
+import net.minecraft.screen.slot.Slot;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+@Mixin(Slot.class)
+public abstract class DisableSlotHighlights {
+ @Shadow
+ public abstract ItemStack getStack();
+
+ @Inject(method = "canBeHighlighted", at = @At("HEAD"), cancellable = true)
+ private void dontHighlight(CallbackInfoReturnable<Boolean> cir) {
+ if (!Fixes.TConfig.INSTANCE.getHideSlotHighlights()) return;
+ var display = getStack().get(DataComponentTypes.TOOLTIP_DISPLAY);
+ if (display != null && display.hideTooltip())
+ cir.setReturnValue(false);
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeFeatureRenderer.java b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeFeatureRenderer.java
new file mode 100644
index 0000000..5a92f89
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeFeatureRenderer.java
@@ -0,0 +1,43 @@
+package moe.nea.firmament.mixins.feature.devcosmetics;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import com.llamalad7.mixinextras.sugar.Local;
+import kotlin.Unit;
+import moe.nea.firmament.features.misc.CustomCapes;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.client.render.VertexConsumer;
+import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.render.entity.feature.CapeFeatureRenderer;
+import net.minecraft.client.render.entity.feature.FeatureRenderer;
+import net.minecraft.client.render.entity.feature.FeatureRendererContext;
+import net.minecraft.client.render.entity.model.BipedEntityModel;
+import net.minecraft.client.render.entity.model.PlayerEntityModel;
+import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
+import net.minecraft.client.util.SkinTextures;
+import net.minecraft.client.util.math.MatrixStack;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+@Mixin(CapeFeatureRenderer.class)
+public abstract class CustomCapeFeatureRenderer extends FeatureRenderer<PlayerEntityRenderState, PlayerEntityModel> {
+ public CustomCapeFeatureRenderer(FeatureRendererContext<PlayerEntityRenderState, PlayerEntityModel> context) {
+ super(context);
+ }
+
+ @WrapOperation(
+ method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/PlayerEntityRenderState;FF)V",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/model/BipedEntityModel;render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumer;II)V")
+ )
+ private void onRender(BipedEntityModel instance, MatrixStack matrixStack, VertexConsumer vertexConsumer, int light, int overlay, Operation<Void> original, @Local PlayerEntityRenderState playerEntityRenderState, @Local SkinTextures skinTextures, @Local VertexConsumerProvider vertexConsumerProvider) {
+ CustomCapes.render(
+ playerEntityRenderState,
+ vertexConsumer,
+ RenderLayer.getEntitySolid(skinTextures.capeTexture()),
+ vertexConsumerProvider,
+ updatedConsumer -> {
+ original.call(instance, matrixStack, updatedConsumer, light, overlay);
+ return Unit.INSTANCE;
+ });
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeStorage.java b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeStorage.java
new file mode 100644
index 0000000..428d7ec
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/CustomCapeStorage.java
@@ -0,0 +1,23 @@
+package moe.nea.firmament.mixins.feature.devcosmetics;
+
+import moe.nea.firmament.features.misc.CustomCapes;
+import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+
+@Mixin(PlayerEntityRenderState.class)
+public class CustomCapeStorage implements CustomCapes.CapeStorage {
+ @Unique
+ CustomCapes.CustomCape customCape;
+
+ @Override
+ public CustomCapes.@Nullable CustomCape getCape_firmament() {
+ return customCape;
+ }
+
+ @Override
+ public void setCape_firmament(CustomCapes.@Nullable CustomCape customCape) {
+ this.customCape = customCape;
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/SaveCapeToPlayerEntityRenderState.java b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/SaveCapeToPlayerEntityRenderState.java
new file mode 100644
index 0000000..ae9c743
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/feature/devcosmetics/SaveCapeToPlayerEntityRenderState.java
@@ -0,0 +1,19 @@
+package moe.nea.firmament.mixins.feature.devcosmetics;
+
+import moe.nea.firmament.features.misc.CustomCapes;
+import net.minecraft.client.network.AbstractClientPlayerEntity;
+import net.minecraft.client.render.entity.PlayerEntityRenderer;
+import net.minecraft.client.render.entity.state.PlayerEntityRenderState;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(PlayerEntityRenderer.class)
+public class SaveCapeToPlayerEntityRenderState {
+ @Inject(method = "updateRenderState(Lnet/minecraft/client/network/AbstractClientPlayerEntity;Lnet/minecraft/client/render/entity/state/PlayerEntityRenderState;F)V",
+ at = @At("TAIL"))
+ private void addCustomCape(AbstractClientPlayerEntity abstractClientPlayerEntity, PlayerEntityRenderState playerEntityRenderState, float f, CallbackInfo ci) {
+ CustomCapes.addCapeData(abstractClientPlayerEntity, playerEntityRenderState);
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java
new file mode 100644
index 0000000..2b96e5c
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ChangeColorOfLivingEntities.java
@@ -0,0 +1,62 @@
+package moe.nea.firmament.mixins.render.entitytints;
+
+import com.llamalad7.mixinextras.injector.ModifyReturnValue;
+import com.llamalad7.mixinextras.sugar.Local;
+import moe.nea.firmament.events.EntityRenderTintEvent;
+import net.minecraft.client.render.VertexConsumerProvider;
+import net.minecraft.client.render.entity.LivingEntityRenderer;
+import net.minecraft.client.render.entity.model.EntityModel;
+import net.minecraft.client.render.entity.state.LivingEntityRenderState;
+import net.minecraft.client.util.math.MatrixStack;
+import net.minecraft.entity.LivingEntity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.ModifyArg;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+/**
+ * Applies various rendering modifications from {@link EntityRenderTintEvent}
+ */
+@Mixin(LivingEntityRenderer.class)
+public class ChangeColorOfLivingEntities<T extends LivingEntity, S extends LivingEntityRenderState, M extends EntityModel<? super S>> {
+ @ModifyReturnValue(method = "getMixColor", at = @At("RETURN"))
+ private int changeColor(int original, @Local(argsOnly = true) S state) {
+ var tintState = EntityRenderTintEvent.HasTintRenderState.cast(state);
+ if (tintState.getHasTintOverride_firmament())
+ return tintState.getTint_firmament();
+ return original;
+ }
+
+ @ModifyArg(
+ method = "getOverlay",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/OverlayTexture;getU(F)I"),
+ allow = 1
+ )
+ private static float modifyLightOverlay(float originalWhiteOffset, @Local(argsOnly = true) LivingEntityRenderState state) {
+ var tintState = EntityRenderTintEvent.HasTintRenderState.cast(state);
+ if (tintState.getHasTintOverride_firmament() || tintState.getOverlayTexture_firmament() != null) {
+ return 1F; // TODO: add interpolation percentage to render state extension
+ }
+ return originalWhiteOffset;
+ }
+
+ @Inject(method = "render(Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/math/MatrixStack;pop()V"))
+ private void afterRender(S livingEntityRenderState, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) {
+ var tintState = EntityRenderTintEvent.HasTintRenderState.cast(livingEntityRenderState);
+ var overlayTexture = tintState.getOverlayTexture_firmament();
+ if (overlayTexture != null && vertexConsumerProvider instanceof VertexConsumerProvider.Immediate imm) {
+ imm.drawCurrentLayer();
+ }
+ EntityRenderTintEvent.overlayOverride = null;
+ }
+
+ @Inject(method = "render(Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;I)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/util/math/MatrixStack;push()V"))
+ private void beforeRender(S livingEntityRenderState, MatrixStack matrixStack, VertexConsumerProvider vertexConsumerProvider, int i, CallbackInfo ci) {
+ var tintState = EntityRenderTintEvent.HasTintRenderState.cast(livingEntityRenderState);
+ var overlayTexture = tintState.getOverlayTexture_firmament();
+ if (overlayTexture != null) {
+ EntityRenderTintEvent.overlayOverride = overlayTexture;
+ }
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/EntityRenderStateTint.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/EntityRenderStateTint.java
new file mode 100644
index 0000000..1019027
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/EntityRenderStateTint.java
@@ -0,0 +1,55 @@
+package moe.nea.firmament.mixins.render.entitytints;
+
+import moe.nea.firmament.events.EntityRenderTintEvent;
+import moe.nea.firmament.util.render.TintedOverlayTexture;
+import net.minecraft.client.render.entity.state.EntityRenderState;
+import org.jetbrains.annotations.Nullable;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Unique;
+
+@Mixin(EntityRenderState.class)
+public class EntityRenderStateTint implements EntityRenderTintEvent.HasTintRenderState {
+ @Unique
+ int tint = -1;
+ @Unique
+ TintedOverlayTexture overlayTexture;
+ @Unique
+ boolean hasTintOverride = false;
+
+ @Override
+ public int getTint_firmament() {
+ return tint;
+ }
+
+ @Override
+ public void setTint_firmament(int i) {
+ tint = i;
+ hasTintOverride = true;
+ }
+
+ @Override
+ public boolean getHasTintOverride_firmament() {
+ return hasTintOverride;
+ }
+
+ @Override
+ public void setHasTintOverride_firmament(boolean b) {
+ hasTintOverride = b;
+ }
+
+ @Override
+ public void reset_firmament() {
+ hasTintOverride = false;
+ overlayTexture = null;
+ }
+
+ @Override
+ public @Nullable TintedOverlayTexture getOverlayTexture_firmament() {
+ return overlayTexture;
+ }
+
+ @Override
+ public void setOverlayTexture_firmament(@Nullable TintedOverlayTexture tintedOverlayTexture) {
+ this.overlayTexture = tintedOverlayTexture;
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/InjectIntoRenderState.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/InjectIntoRenderState.java
new file mode 100644
index 0000000..7938340
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/InjectIntoRenderState.java
@@ -0,0 +1,30 @@
+package moe.nea.firmament.mixins.render.entitytints;
+
+import moe.nea.firmament.events.EntityRenderTintEvent;
+import net.minecraft.client.render.entity.EntityRenderer;
+import net.minecraft.client.render.entity.state.EntityRenderState;
+import net.minecraft.entity.Entity;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+/**
+ * Dispatches {@link EntityRenderTintEvent} to collect additional render state used by {@link ChangeColorOfLivingEntities}
+ */
+@Mixin(EntityRenderer.class)
+public class InjectIntoRenderState<T extends Entity, S extends EntityRenderState> {
+
+ @Inject(
+ method = "updateRenderState",
+ at = @At("RETURN"))
+ private void onUpdateRenderState(T entity, S state, float tickDelta, CallbackInfo ci) {
+ var renderState = EntityRenderTintEvent.HasTintRenderState.cast(state);
+ renderState.reset_firmament();
+ var tintEvent = new EntityRenderTintEvent(
+ entity,
+ renderState
+ );
+ EntityRenderTintEvent.Companion.publish(tintEvent);
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/ReplaceOverlayTexture.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ReplaceOverlayTexture.java
new file mode 100644
index 0000000..61e5c65
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/ReplaceOverlayTexture.java
@@ -0,0 +1,24 @@
+package moe.nea.firmament.mixins.render.entitytints;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import moe.nea.firmament.events.EntityRenderTintEvent;
+import net.minecraft.client.render.OverlayTexture;
+import net.minecraft.client.render.RenderLayer;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+/**
+ * Replaces the overlay texture used by rendering with the override specified in {@link EntityRenderTintEvent#overlayOverride}
+ */
+@Mixin(RenderLayer.Overlay.class)
+public class ReplaceOverlayTexture {
+ @ModifyExpressionValue(
+ method = {"method_23555", "method_23556"},
+ expect = 2,
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/GameRenderer;getOverlayTexture()Lnet/minecraft/client/render/OverlayTexture;"))
+ private static OverlayTexture replaceOverlayTexture(OverlayTexture original) {
+ if (EntityRenderTintEvent.overlayOverride != null)
+ return EntityRenderTintEvent.overlayOverride;
+ return original;
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableEquipmentRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableEquipmentRenderer.java
new file mode 100644
index 0000000..d9c174c
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableEquipmentRenderer.java
@@ -0,0 +1,34 @@
+package moe.nea.firmament.mixins.render.entitytints;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import moe.nea.firmament.events.EntityRenderTintEvent;
+import net.minecraft.client.render.OverlayTexture;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.client.render.entity.equipment.EquipmentRenderer;
+import net.minecraft.util.Identifier;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+/**
+ * Patch to make {@link EquipmentRenderer} use a {@link RenderLayer} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified.
+ */
+@Mixin(EquipmentRenderer.class)
+public class UseOverlayableEquipmentRenderer {
+ @WrapOperation(method = "render(Lnet/minecraft/client/render/entity/equipment/EquipmentModel$LayerType;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/client/model/Model;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/util/Identifier;)V",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/RenderLayer;getArmorCutoutNoCull(Lnet/minecraft/util/Identifier;)Lnet/minecraft/client/render/RenderLayer;"))
+ private RenderLayer replace(Identifier texture, Operation<RenderLayer> original) {
+ if (EntityRenderTintEvent.overlayOverride != null)
+ return RenderLayer.getEntityTranslucent(texture);
+ return original.call(texture);
+ }
+
+ @ModifyExpressionValue(method = "render(Lnet/minecraft/client/render/entity/equipment/EquipmentModel$LayerType;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/client/model/Model;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/util/Identifier;)V",
+ at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/OverlayTexture;DEFAULT_UV:I"))
+ private int replaceUvIndex(int original) {
+ if (EntityRenderTintEvent.overlayOverride != null)
+ return OverlayTexture.packUv(15, 10); // TODO: store this info in a global alongside overlayOverride
+ return original;
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java
new file mode 100644
index 0000000..07bc5cf
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableHeadFeatureRenderer.java
@@ -0,0 +1,25 @@
+package moe.nea.firmament.mixins.render.entitytints;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import moe.nea.firmament.events.EntityRenderTintEvent;
+import net.minecraft.client.render.OverlayTexture;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.client.render.entity.feature.HeadFeatureRenderer;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+/**
+ * Patch to make {@link HeadFeatureRenderer} use a {@link RenderLayer} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified.
+ * @see UseOverlayableItemRenderer
+ */
+@Mixin(HeadFeatureRenderer.class)
+public class UseOverlayableHeadFeatureRenderer {
+
+ @ModifyExpressionValue(method = "render(Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/client/render/entity/state/LivingEntityRenderState;FF)V",
+ at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/OverlayTexture;DEFAULT_UV:I"))
+ private int replaceUvIndex(int original) {
+ if (EntityRenderTintEvent.overlayOverride != null)
+ return OverlayTexture.packUv(15, 10); // TODO: store this info in a global alongside overlayOverride
+ return original;
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java
new file mode 100644
index 0000000..620ab2c
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableItemRenderer.java
@@ -0,0 +1,25 @@
+package moe.nea.firmament.mixins.render.entitytints;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import moe.nea.firmament.events.EntityRenderTintEvent;
+import net.minecraft.client.render.OverlayTexture;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.client.render.RenderPhase;
+import net.minecraft.client.render.item.ItemRenderState;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+/**
+ * Patch to make {@link ItemRenderState} use a {@link RenderLayer} that allows uses Minecraft's overlay texture.
+ *
+ * @see UseOverlayableHeadFeatureRenderer
+ */
+@Mixin(ItemRenderState.LayerRenderState.class)
+public class UseOverlayableItemRenderer {
+ @ModifyExpressionValue(method = "render", at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/item/ItemRenderState$LayerRenderState;renderLayer:Lnet/minecraft/client/render/RenderLayer;"))
+ private RenderLayer replace(RenderLayer original) {
+ if (EntityRenderTintEvent.overlayOverride != null && original instanceof RenderLayer.MultiPhase multiPhase && multiPhase.phases.texture instanceof RenderPhase.Texture texture && texture.getId().isPresent())
+ return RenderLayer.getEntityTranslucent(texture.getId().get());
+ return original;
+ }
+}
diff --git a/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableSkullBlockEntityRenderer.java b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableSkullBlockEntityRenderer.java
new file mode 100644
index 0000000..9905af1
--- /dev/null
+++ b/src/main/java/moe/nea/firmament/mixins/render/entitytints/UseOverlayableSkullBlockEntityRenderer.java
@@ -0,0 +1,25 @@
+package moe.nea.firmament.mixins.render.entitytints;
+
+import com.llamalad7.mixinextras.injector.ModifyExpressionValue;
+import moe.nea.firmament.events.EntityRenderTintEvent;
+import net.minecraft.client.render.OverlayTexture;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.client.render.block.entity.SkullBlockEntityRenderer;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+/**
+ * Patch to make {@link SkullBlockEntityRenderer} use a {@link RenderLayer} that allows uses Minecraft's overlay texture, if a {@link EntityRenderTintEvent#overlayOverride} is specified.
+ */
+
+@Mixin(SkullBlockEntityRenderer.class)
+public class UseOverlayableSkullBlockEntityRenderer {
+ @ModifyExpressionValue(method = "renderSkull",
+ at = @At(value = "FIELD", target = "Lnet/minecraft/client/render/OverlayTexture;DEFAULT_UV:I"))
+ private static int replaceUvIndex(int original) {
+ if (EntityRenderTintEvent.overlayOverride != null)
+ return OverlayTexture.packUv(15, 10); // TODO: store this info in a global alongside overlayOverride
+ return original;
+ }
+
+}
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 01905c7..b00546a 100644
--- a/src/main/kotlin/Firmament.kt
+++ b/src/main/kotlin/Firmament.kt
@@ -19,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
@@ -49,6 +51,8 @@ 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() }
@@ -63,6 +67,8 @@ object Firmament {
}
val version: Version by lazy { metadata.version }
+ private val DEFAULT_JSON_INDENT = " "
+
@OptIn(ExperimentalSerializationApi::class)
val json = Json {
prettyPrint = DEBUG
@@ -70,10 +76,23 @@ object Firmament {
allowTrailingComma = 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
+ encodeDefaults = false
+ explicitNulls = false
}
@@ -116,10 +135,10 @@ object Firmament {
@JvmStatic
fun onClientInitialize() {
+ InitLevel.bump(InitLevel.MC_INIT)
FeatureManager.subscribeEvents()
- var tick = 0
ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { instance ->
- TickEvent.publish(TickEvent(tick++))
+ TickEvent.publish(TickEvent(MC.currentTick++))
})
IDataHolder.registerEvents()
RepoManager.initialize()
@@ -145,6 +164,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..156de89 100644
--- a/src/main/kotlin/apis/Profiles.kt
+++ b/src/main/kotlin/apis/Profiles.kt
@@ -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..5e29402 100644
--- a/src/main/kotlin/apis/Routes.kt
+++ b/src/main/kotlin/apis/Routes.kt
@@ -28,13 +28,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 +44,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/commands/rome.kt b/src/main/kotlin/commands/rome.kt
index 13acb3c..f808231 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,10 +12,12 @@ 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
@@ -32,6 +35,7 @@ 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.mc.SNbtFormatter
@@ -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/CustomItemModelEvent.kt b/src/main/kotlin/events/CustomItemModelEvent.kt
index 11528fd..7b86980 100644
--- a/src/main/kotlin/events/CustomItemModelEvent.kt
+++ b/src/main/kotlin/events/CustomItemModelEvent.kt
@@ -1,23 +1,75 @@
package moe.nea.firmament.events
+import java.util.Objects
+import java.util.Optional
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.component.DataComponentTypes
import net.minecraft.item.ItemStack
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,
+ val itemModelManager: IntrospectableItemModelManager,
var overrideModel: Identifier? = null,
) : FirmamentEvent() {
companion object : FirmamentEventBus<CustomItemModelEvent>() {
+ 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?): Identifier? {
- // TODO: Re-add memoization and add an error / warning if the model does not exist
+ fun getModelIdentifier(itemStack: ItemStack?, itemModelManager: IntrospectableItemModelManager): Identifier? {
if (itemStack == null) return null
- return publish(CustomItemModelEvent(itemStack)).overrideModel
+ return cache.invoke(itemStack, itemModelManager).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) {
- this.overrideModel = overrideModel
+ 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..29b888b
--- /dev/null
+++ b/src/main/kotlin/events/EntityRenderTintEvent.kt
@@ -0,0 +1,66 @@
+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.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/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/IsSlotProtectedEvent.kt b/src/main/kotlin/events/IsSlotProtectedEvent.kt
index cd2b676..8fe0a96 100644
--- a/src/main/kotlin/events/IsSlotProtectedEvent.kt
+++ b/src/main/kotlin/events/IsSlotProtectedEvent.kt
@@ -1,5 +1,3 @@
-
-
package moe.nea.firmament.events
import net.minecraft.item.ItemStack
@@ -8,39 +6,60 @@ 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/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/WorldKeyboardEvent.kt b/src/main/kotlin/events/WorldKeyboardEvent.kt
index e8566fd..1d6a758 100644
--- a/src/main/kotlin/events/WorldKeyboardEvent.kt
+++ b/src/main/kotlin/events/WorldKeyboardEvent.kt
@@ -1,18 +1,17 @@
-
-
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>()
+ companion object : FirmamentEventBus<WorldKeyboardEvent>()
- fun matches(keyBinding: KeyBinding): Boolean {
- return matches(IKeyBinding.minecraft(keyBinding))
- }
+ fun matches(keyBinding: KeyBinding): Boolean {
+ return matches(IKeyBinding.minecraft(keyBinding))
+ }
- fun matches(keyBinding: IKeyBinding): Boolean {
- return keyBinding.matches(keyCode, scanCode, modifiers)
- }
+ fun matches(keyBinding: IKeyBinding, atLeast: Boolean = false): Boolean {
+ return if (atLeast) keyBinding.matchesAtLeast(keyCode, scanCode, modifiers) else
+ keyBinding.matches(keyCode, scanCode, modifiers)
+ }
}
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/features/FeatureManager.kt b/src/main/kotlin/features/FeatureManager.kt
index 0f5ebf8..e0799c4 100644
--- a/src/main/kotlin/features/FeatureManager.kt
+++ b/src/main/kotlin/features/FeatureManager.kt
@@ -25,99 +25,109 @@ 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.WardrobeKeybinds
import moe.nea.firmament.features.inventory.buttons.InventoryButtons
import moe.nea.firmament.features.inventory.storageoverlay.StorageOverlay
+import moe.nea.firmament.features.items.EtherwarpOverlay
import moe.nea.firmament.features.mining.PickaxeAbility
import moe.nea.firmament.features.mining.PristineProfitTracker
+import moe.nea.firmament.features.misc.CustomCapes
+import moe.nea.firmament.features.misc.Hud
import moe.nea.firmament.features.world.FairySouls
import moe.nea.firmament.features.world.Waypoints
+import moe.nea.firmament.util.compatloader.ICompatMeta
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()
- )
+ @Serializable
+ data class Config(
+ val enabledFeatures: MutableMap<String, Boolean> = mutableMapOf()
+ )
- private val features = mutableMapOf<String, FirmamentFeature>()
+ private val features = mutableMapOf<String, FirmamentFeature>()
- val allFeatures: Collection<FirmamentFeature> get() = features.values
+ val allFeatures: Collection<FirmamentFeature> get() = features.values
- private var hasAutoloaded = false
+ 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(CompatibliltyFeatures)
+ loadFeature(AnniversaryFeatures)
+ loadFeature(QuickCommands)
+ loadFeature(PetFeatures)
+ loadFeature(SaveCursorPosition)
+ loadFeature(PriceData)
+ loadFeature(Fixes)
+ loadFeature(CustomCapes)
+ loadFeature(Hud)
+ loadFeature(EtherwarpOverlay)
+ loadFeature(WardrobeKeybinds)
+ 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 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(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) }
+ fun subscribeEvents() {
+ SubscriptionList.allLists.forEach { list ->
+ if (ICompatMeta.shouldLoad(list.javaClass.name))
+ runCatching {
+ list.provideSubscriptions {
+ it.owner.javaClass.classes.forEach {
+ runCatching { it.getDeclaredField("INSTANCE").get(null) }
+ }
+ subscribeSingleEvent(it)
+ }
+ }.getOrElse {
+ // TODO: allow annotating source sets to specifically opt out of loading for mods, maybe automatically
+ Firmament.logger.info("Ignoring events from $list, likely due to a missing compat mod.", it)
}
- subscribeSingleEvent(it)
- }
- }
- }
+ }
+ }
- private fun <T : FirmamentEvent> subscribeSingleEvent(it: Subscription<T>) {
- it.eventBus.subscribe(false, "${it.owner.javaClass.simpleName}:${it.methodName}", it.invoke)
- }
+ 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 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 isEnabled(identifier: String): Boolean? =
+ data.enabledFeatures[identifier]
- fun setEnabled(identifier: String, value: Boolean) {
- data.enabledFeatures[identifier] = value
- markDirty()
- }
+ fun setEnabled(identifier: String, value: Boolean) {
+ data.enabledFeatures[identifier] = value
+ markDirty()
+ }
}
diff --git a/src/main/kotlin/features/chat/ChatLinks.kt b/src/main/kotlin/features/chat/ChatLinks.kt
index f85825b..1fb12e1 100644
--- a/src/main/kotlin/features/chat/ChatLinks.kt
+++ b/src/main/kotlin/features/chat/ChatLinks.kt
@@ -3,6 +3,7 @@ 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 java.util.concurrent.atomic.AtomicInteger
@@ -50,7 +51,7 @@ object ChatLinks : FirmamentFeature {
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(
@@ -78,7 +79,7 @@ object ChatLinks : FirmamentFeature {
val texId = Firmament.identifier("dynamic_image_preview${nextTexId.getAndIncrement()}")
MC.textureManager.registerTexture(
texId,
- NativeImageBackedTexture(image)
+ NativeImageBackedTexture({ texId.path }, image)
)
Image(texId, image.width, image.height)
} else
@@ -102,8 +103,8 @@ object ChatLinks : FirmamentFeature {
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 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
@@ -138,19 +139,20 @@ object ChatLinks : FirmamentFeature {
var index = 0
while (index < text.length) {
val nextMatch = urlRegex.find(text, index)
- if (nextMatch == null) {
+ 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
- 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))
+ ).withHoverEvent(HoverEvent.ShowText(Text.literal(url)))
+ .withClickEvent(ClickEvent.OpenUrl(uri))
)
)
if (isImageUrl(url))
diff --git a/src/main/kotlin/features/chat/CopyChat.kt b/src/main/kotlin/features/chat/CopyChat.kt
new file mode 100644
index 0000000..64f8734
--- /dev/null
+++ b/src/main/kotlin/features/chat/CopyChat.kt
@@ -0,0 +1,31 @@
+package moe.nea.firmament.features.chat
+
+import net.minecraft.text.OrderedText
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.ClientStartedEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.reconstitute
+
+
+object CopyChat : FirmamentFeature {
+ override val identifier: String
+ get() = "copy-chat"
+
+ object TConfig : ManagedConfig(identifier, Category.CHAT) {
+ val copyChat by toggle("copy-chat") { false }
+ }
+
+ @Subscribe
+ fun onInit(event: ClientStartedEvent) {
+ }
+
+ override val config: ManagedConfig?
+ get() = TConfig
+
+ fun orderedTextToString(orderedText: OrderedText): String {
+ return orderedText.reconstitute().string
+ }
+
+
+}
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..9115956 100644
--- a/src/main/kotlin/features/debug/DebugLogger.kt
+++ b/src/main/kotlin/features/debug/DebugLogger.kt
@@ -10,6 +10,7 @@ class DebugLogger(val tag: String) {
companion object {
val allInstances = InstanceList<DebugLogger>("DebugLogger")
}
+
object EnabledLogs : DataHolder<MutableSet<String>>(serializer(), "DebugLogs", ::mutableSetOf)
init {
@@ -17,6 +18,7 @@ class DebugLogger(val tag: String) {
}
fun isEnabled() = DeveloperFeatures.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/DeveloperFeatures.kt b/src/main/kotlin/features/debug/DeveloperFeatures.kt
index 8f0c25c..fd236f9 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,14 +14,18 @@ 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.iterate
object DeveloperFeatures : FirmamentFeature {
+ val DEVELOPER_SUBCOMMAND: String = "dev"
override val identifier: String
get() = "developer"
override val config: TConfig
@@ -42,6 +50,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
@@ -60,9 +104,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..f0250dc
--- /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().saveVersion.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/PowerUserTools.kt b/src/main/kotlin/features/debug/PowerUserTools.kt
index 225bc13..7c1df3f 100644
--- a/src/main/kotlin/features/debug/PowerUserTools.kt
+++ b/src/main/kotlin/features/debug/PowerUserTools.kt
@@ -10,10 +10,13 @@ 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
@@ -30,10 +33,14 @@ import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
import moe.nea.firmament.util.ClipboardUtils
import moe.nea.firmament.util.MC
import moe.nea.firmament.util.focusedItemStack
+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
@@ -48,6 +55,10 @@ 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")
}
override val config
@@ -56,14 +67,13 @@ object PowerUserTools : FirmamentFeature {
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
@@ -87,12 +97,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)))
}
}
@@ -119,7 +134,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) // TODO: remove global texture overrides, maybe
+ 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
@@ -159,11 +178,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"))
}
}
@@ -203,7 +234,7 @@ object PowerUserTools : FirmamentFeature {
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..4f9acd8
--- /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("(", "").replace(")", ""))
+ }
+
+ 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..d7d17aa
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/ItemExporter.kt
@@ -0,0 +1,184 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import com.mojang.brigadier.arguments.StringArgumentType
+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.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.events.HandledScreenKeyPressedEvent
+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.setSkyBlockId
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.tr
+
+object ItemExporter {
+
+ fun exportItem(itemStack: ItemStack): Text {
+ val exporter = LegacyItemExporter.createExporter(itemStack)
+ val json = exporter.exportJson()
+ val jsonFormatted = Firmament.twoSpaceJson.encodeToString(json)
+ val fileName = json.jsonObject["internalname"]!!.jsonPrimitive.content
+ val itemFile = RepoDownloadManager.repoSavedLocation.resolve("items").resolve("${fileName}.json")
+ itemFile.createParentDirectories()
+ 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", StringArgumentType.string()) { itemid ->
+ suggestsList { RepoManager.neuRepo.items.items.keys }
+ thenExecute {
+ val itemid = SkyblockId(get(itemid))
+ 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))
+ }
+ }
+
+ 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..c0f48ca
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemData.kt
@@ -0,0 +1,75 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+import kotlinx.serialization.Serializable
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.item.ItemStack
+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.MC
+
+/**
+ * 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)
+ })!!
+ val stack = ItemStack.fromNbt(MC.defaultRegistries, nbt).getOrNull()
+ ?: error("Could not transform ${legacyItemType}")
+ stack.item to legacyItemType
+ }
+ }.toMap()
+
+}
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..3cd1ce8
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/LegacyItemExporter.kt
@@ -0,0 +1,270 @@
+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 net.minecraft.component.DataComponentTypes
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtElement
+import net.minecraft.nbt.NbtInt
+import net.minecraft.nbt.NbtOps
+import net.minecraft.nbt.NbtString
+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.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)
+ }
+ var lore = itemStack.loreAccordingToNbt
+ 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()
+ copyExtraAttributes()
+ copyLegacySkullNbt()
+ copyDisplay()
+ copyEnchantments()
+ copyEnchantGlint()
+ // TODO: copyDisplay
+ }
+
+ private fun copyItemModel() {
+ val itemModel = itemStack.get(DataComponentTypes.ITEM_MODEL) ?: return
+ legacyNbt.put("ItemModel", NbtString.of(itemModel.toString()))
+ }
+
+ private fun copyDisplay() {
+ legacyNbt.put("display", NbtCompound().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)
+ .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.id.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/debug/itemeditor/PromptScreen.kt b/src/main/kotlin/features/debug/itemeditor/PromptScreen.kt
new file mode 100644
index 0000000..187b70b
--- /dev/null
+++ b/src/main/kotlin/features/debug/itemeditor/PromptScreen.kt
@@ -0,0 +1,15 @@
+package moe.nea.firmament.features.debug.itemeditor
+
+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.TextComponent
+import io.github.notenoughupdates.moulconfig.gui.component.TextFieldComponent
+import io.github.notenoughupdates.moulconfig.observer.GetSetter
+import kotlin.reflect.KMutableProperty0
+import moe.nea.firmament.gui.FirmButtonComponent
+import moe.nea.firmament.util.MoulConfigUtils
+
diff --git a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
index 5151862..0cfaeba 100644
--- a/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
+++ b/src/main/kotlin/features/events/anniversity/AnniversaryFeatures.kt
@@ -15,6 +15,7 @@ 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
@@ -202,7 +203,8 @@ object AnniversaryFeatures : FirmamentFeature {
SBItemStack(SkyblockId.NULL)
}
- @Bind
+ @OptIn(ExpensiveItemCacheApi::class)
+ @Bind
fun name(): String {
return when (backedBy) {
is Reward.Coins -> "Coins"
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..9935051
--- /dev/null
+++ b/src/main/kotlin/features/events/anniversity/CenturyRaffleFeatures.kt
@@ -0,0 +1,63 @@
+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.gui.config.ManagedConfig
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.SkyblockId
+import moe.nea.firmament.util.render.TintedOverlayTexture
+import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.skyblock.SkyBlockItems
+
+object CenturyRaffleFeatures {
+ 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/MinesweeperHelper.kt b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
index 1824225..cfc05cc 100644
--- a/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
+++ b/src/main/kotlin/features/events/carnival/MinesweeperHelper.kt
@@ -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/Fixes.kt b/src/main/kotlin/features/fixes/Fixes.kt
index 7030319..d490cc4 100644
--- a/src/main/kotlin/features/fixes/Fixes.kt
+++ b/src/main/kotlin/features/fixes/Fixes.kt
@@ -11,6 +11,7 @@ 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.tr
object Fixes : FirmamentFeature {
override val identifier: String
@@ -20,9 +21,14 @@ object Fixes : FirmamentFeature {
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) { Point(0.0, 1.0) }
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 }
}
override val config: ManagedConfig
@@ -32,8 +38,12 @@ object Fixes : FirmamentFeature {
keyBinding: KeyBinding,
cir: CallbackInfoReturnable<Boolean>
) {
- if (keyBinding === MinecraftClient.getInstance().options.sprintKey && TConfig.autoSprint && MC.player?.isSprinting != true)
- cir.returnValue = true
+ 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
@@ -42,14 +52,18 @@ object Fixes : FirmamentFeature {
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
+ 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.pop()
}
diff --git a/src/main/kotlin/features/garden/HideComposterNoises.kt b/src/main/kotlin/features/garden/HideComposterNoises.kt
new file mode 100644
index 0000000..69207a9
--- /dev/null
+++ b/src/main/kotlin/features/garden/HideComposterNoises.kt
@@ -0,0 +1,32 @@
+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.gui.config.ManagedConfig
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.SkyBlockIsland
+
+object HideComposterNoises {
+ 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..f823086 100644
--- a/src/main/kotlin/features/inventory/CraftingOverlay.kt
+++ b/src/main/kotlin/features/inventory/CraftingOverlay.kt
@@ -8,6 +8,7 @@ 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
@@ -45,6 +46,7 @@ object CraftingOverlay : FirmamentFeature {
override 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..e826b31 100644
--- a/src/main/kotlin/features/inventory/ItemHotkeys.kt
+++ b/src/main/kotlin/features/inventory/ItemHotkeys.kt
@@ -3,12 +3,14 @@ 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.focusedItemStack
import moe.nea.firmament.util.skyBlockId
import moe.nea.firmament.util.skyblock.SBItemUtil.getSearchName
@@ -18,6 +20,7 @@ object ItemHotkeys {
val openGlobalTradeInterface by keyBindingWithDefaultUnbound("global-trade-interface")
}
+ @OptIn(ExpensiveItemCacheApi::class)
@Subscribe
fun onHandledInventoryPress(event: HandledScreenKeyPressedEvent) {
if (!event.matches(TConfig.openGlobalTradeInterface)) {
@@ -26,7 +29,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/PetFeatures.kt b/src/main/kotlin/features/inventory/PetFeatures.kt
index 5ca10f7..9393b03 100644
--- a/src/main/kotlin/features/inventory/PetFeatures.kt
+++ b/src/main/kotlin/features/inventory/PetFeatures.kt
@@ -1,14 +1,25 @@
package moe.nea.firmament.features.inventory
-import net.minecraft.util.Identifier
+import moe.nea.jarvis.api.Point
+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.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.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
@@ -19,9 +30,12 @@ object PetFeatures : FirmamentFeature {
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) { Point(0.5, 1.0) }
}
val petMenuTitle = "Pets(?: \\([0-9]+/[0-9]+\\))?".toPattern()
+ var petItemStack: ItemStack? = null
@Subscribe
fun onSlotRender(event: SlotRenderEvents.Before) {
@@ -29,12 +43,44 @@ object PetFeatures : FirmamentFeature {
val stack = event.slot.stack
if (stack.petData?.active == true)
petMenuTitle.useMatch(MC.screenName ?: return) {
- event.context.drawGuiTexture(
- event.slot.x, event.slot.y, 0, 16, 16,
- Identifier.of("firmament:selected_pet_background")
- )
- }
+ petItemStack = stack
+ event.context.drawGuiTexture(
+ 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.push()
+ TConfig.petOverlayHud.applyTransformations(it.context.matrices)
+
+ val lines = mutableListOf<Text>()
+ it.context.matrices.push()
+ it.context.matrices.translate(-0.5, -0.5, 0.0)
+ it.context.matrices.scale(2f, 2f, 1f)
+ it.context.drawItem(itemStack, 0, 0)
+ it.context.matrices.pop()
+ 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.pop()
+ }
}
diff --git a/src/main/kotlin/features/inventory/PriceData.kt b/src/main/kotlin/features/inventory/PriceData.kt
index 4477203..92bfc58 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.gold
import moe.nea.firmament.util.skyBlockId
+import moe.nea.firmament.util.tr
+import moe.nea.firmament.util.yellow
object PriceData : FirmamentFeature {
- override val identifier: String
- get() = "price-data"
-
- object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
- val tooltipEnabled by toggle("enable-always") { true }
- val enableKeybinding by keyBindingWithDefaultUnbound("enable-keybind")
- }
-
- override val config get() = TConfig
-
- @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))
- )
- }
- }
+ override val identifier: String
+ get() = "price-data"
+
+ 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
+ }
+ }
+
+ enum class AvgLowestBin : StringIdentifiable {
+ OFF,
+ ONEDAYAVGLOWESTBIN,
+ THREEDAYAVGLOWESTBIN,
+ SEVENDAYAVGLOWESTBIN;
+
+ 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) return
+ if (TConfig.enableKeybinding.isBound && !TConfig.enableKeybinding.isPressed()) return
+ val sbId = it.stack.skyBlockId
+ val stackSize = it.stack.count
+ 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.sell-order", "Bazaar Sell Order"),
+ bazaarData.quickStatus.sellPrice * multiplier
+ )
+ )
+ it.lines.add(
+ formatPrice(
+ tr("firmament.tooltip.bazaar.buy-order", "Bazaar Buy 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..476759a 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
@@ -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.Config.warnForMissingItemListMod) return
if (hasREI) return
if (sentWarning) return
sentWarning = true
diff --git a/src/main/kotlin/features/inventory/SlotLocking.kt b/src/main/kotlin/features/inventory/SlotLocking.kt
index 99130d5..d3348a2 100644
--- a/src/main/kotlin/features/inventory/SlotLocking.kt
+++ b/src/main/kotlin/features/inventory/SlotLocking.kt
@@ -4,10 +4,23 @@ package moe.nea.firmament.features.inventory
import java.util.UUID
import org.lwjgl.glfw.GLFW
+import util.render.CustomRenderLayers
+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.gui.screen.ingame.HandledScreen
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.client.render.RenderLayers
+import net.minecraft.client.render.TexturedRenderLayers
import net.minecraft.entity.player.PlayerInventory
import net.minecraft.screen.GenericContainerScreenHandler
import net.minecraft.screen.slot.Slot
@@ -15,6 +28,7 @@ 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.FeaturesInitializedEvent
import moe.nea.firmament.events.HandledScreenForegroundEvent
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
import moe.nea.firmament.events.HandledScreenKeyReleasedEvent
@@ -35,8 +49,8 @@ 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.render.drawLine
+import moe.nea.firmament.util.skyblock.DungeonUtil
import moe.nea.firmament.util.skyblockUUID
import moe.nea.firmament.util.unformattedString
@@ -49,9 +63,66 @@ object SlotLocking : FirmamentFeature {
val lockedSlots: MutableSet<Int> = mutableSetOf(),
val lockedSlotsRift: MutableSet<Int> = mutableSetOf(),
val lockedUUIDs: MutableSet<UUID> = mutableSetOf(),
- val boundSlots: MutableMap<Int, Int> = mutableMapOf()
+ val boundSlots: BoundSlots = BoundSlots()
)
+ @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))
+
+ }
+ }
+ }
+
+
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
val lockSlot by keyBinding("lock") { GLFW.GLFW_KEY_L }
val lockUUID by keyBindingWithOutDefaultModifiers("lock-uuid") {
@@ -60,6 +131,8 @@ object SlotLocking : FirmamentFeature {
val slotBind by keyBinding("bind") { GLFW.GLFW_KEY_L }
val slotBindRequireShift by toggle("require-quick-move") { true }
val slotRenderLines by choice("bind-render") { SlotRenderLinesMode.ONLY_BOXES }
+ val allowMultiBinding by toggle("multi-bind") { true } // TODO: filter based on this option
+ val allowDroppingInDungeons by toggle("drop-in-dungeons") { true }
}
enum class SlotRenderLinesMode : StringIdentifiable {
@@ -120,7 +193,11 @@ object SlotLocking : FirmamentFeature {
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) {
@@ -156,20 +233,33 @@ object SlotLocking : FirmamentFeature {
}
@Subscribe
+ fun onEvent(event: FeaturesInitializedEvent) {
+ 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 = DConfig.data?.boundSlots ?: BoundSlots()
val isValidAction =
it.actionType == SlotActionType.QUICK_MOVE || (it.actionType == SlotActionType.PICKUP && !TConfig.slotBindRequireShift)
if (!isValidAction) return
val handler = MC.handledScreen?.screenHandler ?: 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)
}
}
@@ -208,10 +298,8 @@ object SlotLocking : FirmamentFeature {
val boundSlots = DConfig.data?.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
@@ -225,9 +313,7 @@ object SlotLocking : FirmamentFeature {
storedLockingSlot = null
val boundSlots = DConfig.data?.boundSlots ?: return
if (slot != null)
- boundSlots.entries.removeIf {
- it.value == slot.index || it.key == slot.index
- }
+ boundSlots.removeAllInvolving(slot.index)
}
}
@@ -238,32 +324,38 @@ object SlotLocking : FirmamentFeature {
val accScreen = event.screen as AccessorHandledScreen
val sx = accScreen.x_Firmament
val sy = accScreen.y_Firmament
- for (it in boundSlots.entries) {
- val hotbarSlot = findByIndex(it.key) ?: continue
- val inventorySlot = findByIndex(it.value) ?: continue
+ 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()
val anyHovered = accScreen.focusedSlot_Firmament === hotbarSlot
- || accScreen.focusedSlot_Firmament === inventorySlot
+ || accScreen.focusedSlot_Firmament === inventorySlot
if (!anyHovered && TConfig.slotRenderLines == SlotRenderLinesMode.NOTHING)
continue
- val color = if (anyHovered)
- me.shedaniel.math.Color.ofOpaque(0x00FF00)
- else
- me.shedaniel.math.Color.ofTransparent(0xc0a0f000.toInt())
+ if (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
+ color(anyHovered)
)
event.context.drawBorder(hotbarSlot.x + sx,
hotbarSlot.y + sy,
- 16, 16, color.color)
+ 16, 16, color(hotbarSlot in highlitSlots).color)
event.context.drawBorder(inventorySlot.x + sx,
inventorySlot.y + sy,
- 16, 16, color.color)
+ 16, 16, color(inventorySlot in highlitSlots).color)
}
}
@@ -319,11 +411,9 @@ object SlotLocking : FirmamentFeature {
fun toggleSlotLock(slot: Slot) {
val lockedSlots = lockedSlots ?: return
- val boundSlots = DConfig.data?.boundSlots ?: mutableMapOf()
+ val boundSlots = DConfig.data?.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)
@@ -358,7 +448,7 @@ object SlotLocking : FirmamentFeature {
val isUUIDLocked = (it.slot.stack?.skyblockUUID) in (lockedUUIDs ?: setOf())
if (isSlotLocked || isUUIDLocked) {
it.context.drawGuiTexture(
- GuiRenderLayers.GUI_TEXTURED_NO_DEPTH,
+ RenderLayer::getGuiTexturedOverlay,
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
index f1b77c6..cc1df9a 100644
--- a/src/main/kotlin/features/inventory/TimerInLore.kt
+++ b/src/main/kotlin/features/inventory/TimerInLore.kt
@@ -16,12 +16,14 @@ import moe.nea.firmament.util.SBData
import moe.nea.firmament.util.aqua
import moe.nea.firmament.util.grey
import moe.nea.firmament.util.mc.displayNameAccordingToNbt
+import moe.nea.firmament.util.timestamp
import moe.nea.firmament.util.tr
import moe.nea.firmament.util.unformattedString
object TimerInLore {
object TConfig : ManagedConfig("lore-timers", Category.INVENTORY) {
val showTimers by toggle("show") { true }
+ val showCreationTimestamp by toggle("show-creation") { true }
val timerFormat by choice("format") { TimerFormat.SOCIALIST }
}
@@ -80,13 +82,25 @@ object TimerInLore {
COMMUNITYPROJECTS("Contribute again", "Come back at"),
CHOCOLATEFACTORY("Next Charge", "Available at"),
STONKSAUCTION("Auction ends in", "Ends at"),
- LIZSTONKREDEMPTION("Resets in:", "Resets 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
@@ -107,9 +121,13 @@ object TimerInLore {
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())
+ event.lines.add(
+ i + 1,
+ tr(
+ "firmament.loretimer.missingrelative",
+ "Found a relative countdown with no baseline (Firmament)"
+ ).grey()
+ )
continue
}
baseLine = lastTimer
@@ -119,10 +137,11 @@ object TimerInLore {
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())
+ 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..6e2b4a9
--- /dev/null
+++ b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt
@@ -0,0 +1,80 @@
+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.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.mc.SlotUtils.clickLeftMouseButton
+
+object WardrobeKeybinds : FirmamentFeature {
+ override val identifier: String
+ get() = "wardrobe-keybinds"
+
+ object TConfig : ManagedConfig(identifier, 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 }
+ }
+ }
+
+ override val config: ManagedConfig?
+ get() = TConfig
+
+ 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
+ if (itemStack.item != Items.PINK_DYE && itemStack.item != Items.LIME_DYE) 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..eecbd17 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.gui.component.PanelComponent
import io.github.notenoughupdates.moulconfig.platform.ModernItemStack
+import io.github.notenoughupdates.moulconfig.platform.ModernRenderContext
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
@@ -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,30 @@ 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 +149,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 +171,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)
@@ -114,11 +188,23 @@ class InventoryButtonEditor(
return newButtons
}
+ override fun renderBackground(context: DrawContext, mouseX: Int, mouseY: Int, delta: Float) {
+ context.matrices.push()
+ context.matrices.translate(0F, 0F, -15F)
+ super.renderBackground(context, mouseX, mouseY, delta)
+ context.matrices.pop()
+ }
+
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)
+ PanelComponent.DefaultBackgroundRenderer.VANILLA
+ .render(
+ ModernRenderContext(context),
+ lastGuiRect.minX, lastGuiRect.minY,
+ lastGuiRect.width, lastGuiRect.height,
+ )
context.matrices.pop()
for (button in buttons) {
val buttonPosition = button.getBounds(lastGuiRect)
@@ -142,7 +228,8 @@ 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 +267,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 +275,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/InventoryButtons.kt b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
index d5b5417..ab80d97 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
@@ -5,44 +5,50 @@ 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.TimeMark
import moe.nea.firmament.util.data.DataHolder
import moe.nea.firmament.util.accessors.getRectangle
+import moe.nea.firmament.util.gold
-object InventoryButtons : FirmamentFeature {
- override val identifier: String
- get() = "inventory-buttons"
+object InventoryButtons {
- object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
+ 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 DConfig : DataHolder<Data>(serializer(), identifier, ::Data)
+ object DConfig : DataHolder<Data>(serializer(), "inventory-buttons", ::Data)
@Serializable
data class Data(
var buttons: MutableList<InventoryButton> = mutableListOf()
)
+ fun getValidButtons(screen: HandledScreen<*>): Sequence<InventoryButton> {
+ return DConfig.data.buttons.asSequence().filter { button ->
+ button.isValid() && (!TConfig.onlyInv || screen is InventoryScreen)
+ }
+ }
- override val config: ManagedConfig
- get() = TConfig
-
- fun getValidButtons() = DConfig.data.buttons.asSequence().filter { it.isValid() }
@Subscribe
fun onRectangles(it: HandledScreenPushREIEvent) {
val bounds = it.screen.getRectangle()
- for (button in getValidButtons()) {
+ for (button in getValidButtons(it.screen)) {
val buttonBounds = button.getBounds(bounds)
it.block(buttonBounds)
}
@@ -51,7 +57,7 @@ object InventoryButtons : FirmamentFeature {
@Subscribe
fun onClickScreen(it: HandledScreenClickEvent) {
val bounds = it.screen.getRectangle()
- for (button in getValidButtons()) {
+ 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 */)
@@ -60,16 +66,36 @@ object InventoryButtons : FirmamentFeature {
}
}
+ var lastHoveredComponent: InventoryButton? = null
+ var lastMouseMove = TimeMark.farPast()
+
@Subscribe
fun onRenderForeground(it: HandledScreenForegroundEvent) {
- val bounds = it.screen.getRectangle()
- for (button in getValidButtons()) {
+ val bounds = it.screen.getRectangle()
+
+ var hoveredComponent: InventoryButton? = null
+ for (button in getValidButtons(it.screen)) {
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()
+
+ 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
}
@@ -78,9 +104,9 @@ object InventoryButtons : FirmamentFeature {
ScreenUtil.setScreenLater(
InventoryButtonEditor(
lastRectangle ?: Rectangle(
- MC.window.scaledWidth / 2 - 100,
- MC.window.scaledHeight / 2 - 100,
- 200, 200,
+ 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..ec62aa6 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlay.kt
@@ -27,12 +27,14 @@ object StorageOverlay : FirmamentFeature {
object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
val alwaysReplace by toggle("always-replace") { true }
+ val outlineActiveStoragePage by toggle("outline-active-page") { false }
val columns by integer("rows", 1, 10) { 3 }
val height by integer("height", 80, 3000) { 3 * 18 * 6 }
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 }
}
fun adjustScrollSpeed(amount: Double): Double {
@@ -100,7 +102,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..81f058e 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,
@@ -113,6 +114,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..84d0f2b 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,15 +47,18 @@ 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 isExiting: Boolean = false
@@ -67,13 +71,14 @@ 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
}
@@ -99,6 +104,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
coerceScroll(StorageOverlay.adjustScrollSpeed(verticalAmount).toFloat())
return true
}
+
fun coerceScroll(offset: Float) {
scroll = (scroll + offset)
.coerceAtMost(getMaxScroll())
@@ -148,21 +154,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 +199,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 +237,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 +246,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 +280,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 +306,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 +329,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 +348,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
}
@@ -355,6 +385,10 @@ 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,
@@ -414,7 +448,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,22 +480,41 @@ 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 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,
+ 0xFFFF00FF.toInt()
+ )
+ 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 slotX = (index % 9) * SLOT_SIZE + x + 3
+ val slotY = (index / 9) * SLOT_SIZE + y + 5 + textRenderer.fontHeight + 1
val fakeSlot = FakeSlot(stack, slotX, slotY)
if (slots == null) {
SlotRenderEvents.Before.publish(SlotRenderEvents.Before(context, fakeSlot))
@@ -474,22 +527,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/VirtualInventory.kt b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
index 3b86184..d99acd7 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt
@@ -11,6 +11,7 @@ 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
@@ -42,15 +43,15 @@ data class VirtualInventory(
override fun deserialize(decoder: Decoder): VirtualInventory {
val s = decoder.decodeString()
val n = NbtIo.readCompressed(ByteArrayInputStream(s.decodeBase64Bytes()), NbtSizeTracker.of(100_000_000))
- val items = n.getList(INVENTORY, NbtCompound.COMPOUND_TYPE.toInt())
+ val items = n.getList(INVENTORY).getOrNull()
val ops = getOps()
- return VirtualInventory(items.map {
+ 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())
}
fun getOps() = TolerantRegistriesOps(NbtOps.INSTANCE, MC.currentOrDefaultRegistries)
diff --git a/src/main/kotlin/features/items/BonemerangOverlay.kt b/src/main/kotlin/features/items/BonemerangOverlay.kt
new file mode 100644
index 0000000..ffdffe3
--- /dev/null
+++ b/src/main/kotlin/features/items/BonemerangOverlay.kt
@@ -0,0 +1,101 @@
+package moe.nea.firmament.features.items
+
+import me.shedaniel.math.Color
+import moe.nea.jarvis.api.Point
+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.ClientStartedEvent
+import moe.nea.firmament.events.EntityRenderTintEvent
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.MC
+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 : FirmamentFeature {
+ override val identifier: String
+ get() = "bonemerang-overlay"
+
+ object TConfig : ManagedConfig(identifier, Category.ITEMS) {
+ var bonemerangOverlay by toggle("bonemerang-overlay") { false }
+ val bonemerangOverlayHud by position("bonemerang-overlay-hud", 80, 10) { Point(0.1, 1.0) }
+ var highlightHitEntities by toggle("highlight-hit-entities") { false }
+ }
+
+ @Subscribe
+ fun onInit(event: ClientStartedEvent) {
+ }
+
+ override val config: ManagedConfig
+ get() = TConfig
+
+ 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.push()
+ 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.pop()
+ }
+}
diff --git a/src/main/kotlin/features/items/EtherwarpOverlay.kt b/src/main/kotlin/features/items/EtherwarpOverlay.kt
new file mode 100644
index 0000000..2de2159
--- /dev/null
+++ b/src/main/kotlin/features/items/EtherwarpOverlay.kt
@@ -0,0 +1,51 @@
+package moe.nea.firmament.features.items
+
+import me.shedaniel.math.Color
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.MC
+import net.minecraft.util.hit.BlockHitResult
+import moe.nea.firmament.events.WorldRenderLastEvent
+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
+
+object EtherwarpOverlay : FirmamentFeature {
+ override val identifier: String
+ get() = "etherwarp-overlay"
+
+ object TConfig : ManagedConfig(identifier, Category.ITEMS) {
+ var etherwarpOverlay by toggle("etherwarp-overlay") { false }
+ var cube by toggle("cube") { true }
+ var wireframe by toggle("wireframe") { false }
+ }
+
+ override val config: ManagedConfig
+ get() = TConfig
+
+
+ @Subscribe
+ fun renderEtherwarpOverlay(event: WorldRenderLastEvent) {
+ if (!TConfig.etherwarpOverlay) return
+ val player = MC.player ?: return
+ if (!player.isSneaking) return
+ val world = player.world
+ val camera = MC.camera ?: return
+ val heldItem = MC.stackInHand
+ if (heldItem.skyBlockId !in listOf(SkyBlockItems.ASPECT_OF_THE_VOID, SkyBlockItems.ASPECT_OF_THE_END)) return
+ if (!heldItem.extraAttributes.contains("ethermerge")) return
+
+ val hitResult = camera.raycast(61.0, 0.0f, false)
+ if (hitResult !is BlockHitResult) return
+ val blockPos = hitResult.blockPos
+ if (camera.squaredDistanceTo(blockPos.toCenterPos()) > 61 * 61) return
+ if (!world.getBlockState(blockPos.up()).isAir) return
+ if (!world.getBlockState(blockPos.up(2)).isAir) return
+ RenderInWorldContext.renderInWorld(event) {
+ if (TConfig.cube) block(blockPos, Color.ofRGBA(172, 0, 255, 60).color)
+ if (TConfig.wireframe) wireframeCube(blockPos, 10f)
+ }
+ }
+}
diff --git a/src/main/kotlin/features/macros/ComboProcessor.kt b/src/main/kotlin/features/macros/ComboProcessor.kt
new file mode 100644
index 0000000..5c5ac0e
--- /dev/null
+++ b/src/main/kotlin/features/macros/ComboProcessor.kt
@@ -0,0 +1,114 @@
+package moe.nea.firmament.features.macros
+
+import kotlin.time.Duration.Companion.seconds
+import net.minecraft.client.util.InputUtil
+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>()
+
+ init {
+ val f = SavedKeyBinding(InputUtil.GLFW_KEY_F)
+ val one = SavedKeyBinding(InputUtil.GLFW_KEY_1)
+ val two = SavedKeyBinding(InputUtil.GLFW_KEY_2)
+ setActions(
+ MacroData.DConfig.data.comboActions
+ )
+ }
+
+ 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.push()
+ val width = 120
+ event.context.matrices.translate(
+ (MC.window.scaledWidth - width) / 2F,
+ (MC.window.scaledHeight) / 2F + 8,
+ 0F
+ )
+ 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, 0F)
+ 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, 0F)
+ }
+ event.context.matrices.pop()
+ }
+
+ @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..452bc56
--- /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.keys.isEmpty()) {
+ ErrorUtil.softUserError("Key Combo for ${combo.action.label.string} is empty")
+ continue
+ }
+ for ((index, key) in combo.keys.withIndex()) {
+ val m = (p.nodes as MutableMap)
+ if (index == combo.keys.lastIndex) {
+ if (key in m) {
+ ErrorUtil.softUserError("Overlapping actions found for ${combo.keys.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.keys} (final node exists at index $index) through another action already")
+ break
+ } else {
+ p = c
+ }
+ }
+ }
+ }
+ return root
+ }
+ }
+}
+
+@Serializable
+data class MacroWheel(
+ val key: SavedKeyBinding,
+ val options: List<HotkeyAction>
+)
+
+@Serializable
+data class ComboKeyAction(
+ val action: HotkeyAction,
+ val keys: List<SavedKeyBinding>,
+)
+
+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..91de423
--- /dev/null
+++ b/src/main/kotlin/features/macros/MacroData.kt
@@ -0,0 +1,12 @@
+package moe.nea.firmament.features.macros
+
+import kotlinx.serialization.Serializable
+import moe.nea.firmament.util.data.DataHolder
+
+@Serializable
+data class MacroData(
+ var comboActions: List<ComboKeyAction> = listOf(),
+ var wheels: List<MacroWheel> = listOf(),
+) {
+ object DConfig : DataHolder<MacroData>(kotlinx.serialization.serializer(), "macros", ::MacroData)
+}
diff --git a/src/main/kotlin/features/macros/MacroUI.kt b/src/main/kotlin/features/macros/MacroUI.kt
new file mode 100644
index 0000000..8c22c5c
--- /dev/null
+++ b/src/main/kotlin/features/macros/MacroUI.kt
@@ -0,0 +1,285 @@
+package moe.nea.firmament.features.macros
+
+import io.github.notenoughupdates.moulconfig.gui.CloseEventListener
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+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 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() = binding.format().string
+
+ @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.key, 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
+
+ @field:Bind("combo")
+ val combo = action.keys.map { KeyBindingEditor(it, this) }.toObservableList()
+
+ @Bind
+ fun formattedCombo() =
+ combo.joinToString(" > ") { it.binding.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..2e09c44
--- /dev/null
+++ b/src/main/kotlin/features/macros/RadialMenu.kt
@@ -0,0 +1,149 @@
+package moe.nea.firmament.features.macros
+
+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) {
+ 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.push()
+ mat.translate(
+ (MC.window.scaledWidth) / 2F,
+ (MC.window.scaledHeight) / 2F,
+ 0F
+ )
+ 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.push()
+ mat.scale(64F, 64F, 1F)
+ 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.pop()
+ mat.push()
+ val centreAngle = lerpAngle(range.start, range.endInclusive, 0.5F)
+ val vec = Vector2f(cos(centreAngle), sin(centreAngle)).mul(40F)
+ mat.translate(vec.x, vec.y, 0F)
+ option.renderSlice(event.context)
+ mat.pop()
+ }
+ event.context.drawLine(1, 1, delta.x.toInt(), delta.y.toInt(), me.shedaniel.math.Color.ofOpaque(0x00FF00))
+ mat.pop()
+ }
+
+ @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 {
+ var wheels = MacroData.DConfig.data.wheels
+ 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.key, 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.key
+ override val options: List<RadialMenuOption> =
+ wheel.options.map { R(it) }
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/features/mining/MiningBlockInfoUi.kt b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt
new file mode 100644
index 0000000..e8ea4f4
--- /dev/null
+++ b/src/main/kotlin/features/mining/MiningBlockInfoUi.kt
@@ -0,0 +1,54 @@
+package moe.nea.firmament.features.mining
+
+import io.github.notenoughupdates.moulconfig.observer.ObservableList
+import io.github.notenoughupdates.moulconfig.observer.Property
+import io.github.notenoughupdates.moulconfig.platform.ModernItemStack
+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 = ModernItemStack.of(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 94b49f9..430bae0 100644
--- a/src/main/kotlin/features/mining/PickaxeAbility.kt
+++ b/src/main/kotlin/features/mining/PickaxeAbility.kt
@@ -1,9 +1,13 @@
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.text.Text
import net.minecraft.util.DyeColor
import net.minecraft.util.Hand
import net.minecraft.util.Identifier
@@ -47,6 +51,7 @@ object PickaxeAbility : FirmamentFeature {
object TConfig : ManagedConfig(identifier, Category.MINING) {
val cooldownEnabled by toggle("ability-cooldown") { false }
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",
@@ -140,9 +145,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(),
@@ -156,10 +161,26 @@ 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
@@ -179,6 +200,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,
diff --git a/src/main/kotlin/features/misc/CustomCapes.kt b/src/main/kotlin/features/misc/CustomCapes.kt
new file mode 100644
index 0000000..dc5187a
--- /dev/null
+++ b/src/main/kotlin/features/misc/CustomCapes.kt
@@ -0,0 +1,192 @@
+package moe.nea.firmament.features.misc
+
+import com.mojang.blaze3d.systems.RenderSystem
+import java.util.OptionalDouble
+import java.util.OptionalInt
+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.BufferBuilder
+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.BufferAllocator
+import net.minecraft.client.util.SkinTextures
+import net.minecraft.util.Identifier
+import moe.nea.firmament.Firmament
+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
+
+object CustomCapes : FirmamentFeature {
+ override val identifier: String
+ get() = "developer-capes"
+
+ object TConfig : ManagedConfig(identifier, Category.DEV) {
+ val showCapes by toggle("show-cape") { true }
+ }
+
+ override val config: ManagedConfig
+ get() = TConfig
+
+ interface CustomCapeRenderer {
+ fun replaceRender(
+ renderLayer: RenderLayer,
+ vertexConsumerProvider: VertexConsumerProvider,
+ model: (VertexConsumer) -> Unit
+ )
+ }
+
+ data class TexturedCapeRenderer(
+ val location: Identifier
+ ) : CustomCapeRenderer {
+ override fun replaceRender(
+ renderLayer: RenderLayer,
+ vertexConsumerProvider: VertexConsumerProvider,
+ 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,
+ model: (VertexConsumer) -> Unit
+ ) {
+ BufferAllocator(2048).use { allocator ->
+ val bufferBuilder = BufferBuilder(allocator, renderLayer.drawMode, renderLayer.vertexFormat)
+ model(bufferBuilder)
+ bufferBuilder.end().use { buffer ->
+ val commandEncoder = RenderSystem.getDevice().createCommandEncoder()
+ val vertexBuffer = renderLayer.vertexFormat.uploadImmediateVertexBuffer(buffer.buffer)
+ val indexBufferConstructor = RenderSystem.getSequentialBuffer(renderLayer.drawMode)
+ val indexBuffer = indexBufferConstructor.getIndexBuffer(buffer.drawParameters.indexCount)
+ val templateTexture = MC.textureManager.getTexture(template)
+ val backgroundTexture = MC.textureManager.getTexture(background)
+ val foregroundTexture = MC.textureManager.getTexture(overlay)
+ commandEncoder.createRenderPass(
+ MC.instance.framebuffer.colorAttachment,
+ OptionalInt.empty(),
+ MC.instance.framebuffer.depthAttachment,
+ OptionalDouble.empty(),
+ ).use { renderPass ->
+ // TODO: account for lighting
+ renderPass.setPipeline(CustomRenderPipelines.PARALLAX_CAPE_SHADER)
+ renderPass.bindSampler("Sampler0", templateTexture.glTexture)
+ renderPass.bindSampler("Sampler1", backgroundTexture.glTexture)
+ renderPass.bindSampler("Sampler3", foregroundTexture.glTexture)
+ val animationValue = (startTime.passedTime() / animationSpeed).mod(1F)
+ renderPass.setUniform("Animation", animationValue.toFloat())
+ renderPass.setIndexBuffer(indexBuffer, indexBufferConstructor.indexType)
+ renderPass.setVertexBuffer(0, vertexBuffer)
+ renderPass.drawIndexed(0, buffer.drawParameters.indexCount)
+ }
+ }
+ }
+ }
+ }
+
+ 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
+ )
+ ),
+
+ FURFSKY_STATIC(
+ "FurfSky",
+ TexturedCapeRenderer(Firmament.identifier("textures/cape/fsr_static.png"))
+ ),
+
+ FIRMAMENT_STATIC(
+ "Firmament",
+ TexturedCapeRenderer(Firmament.identifier("textures/cape/firm_static.png"))
+ )
+ ;
+
+ val cape = CustomCape(name, label, render)
+ }
+
+ val byId = AllCapes.entries.associateBy { it.cape.id }
+ val byUuid =
+ listOf(
+ listOf(
+ Devs.nea to AllCapes.FIRMAMENT_ANIMATED,
+ Devs.kath to AllCapes.FIRMAMENT_STATIC,
+ Devs.jani to AllCapes.FIRMAMENT_ANIMATED,
+ ),
+ 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,
+ model: (VertexConsumer) -> Unit
+ ) {
+ val capeStorage = CapeStorage.cast(playerEntityRenderState)
+ val firmCape = capeStorage.cape_firmament
+ if (firmCape != null) {
+ firmCape.render.replaceRender(renderLayer, vertexConsumerProvider, 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..1f16400
--- /dev/null
+++ b/src/main/kotlin/features/misc/Devs.kt
@@ -0,0 +1,38 @@
+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
+ )
+ }
+
+}
diff --git a/src/main/kotlin/features/misc/Hud.kt b/src/main/kotlin/features/misc/Hud.kt
new file mode 100644
index 0000000..9661fc5
--- /dev/null
+++ b/src/main/kotlin/features/misc/Hud.kt
@@ -0,0 +1,77 @@
+package moe.nea.firmament.features.misc
+
+import moe.nea.firmament.annotations.Subscribe
+import moe.nea.firmament.events.HudRenderEvent
+import moe.nea.firmament.features.FirmamentFeature
+import moe.nea.firmament.gui.config.ManagedConfig
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.tr
+import moe.nea.jarvis.api.Point
+import net.minecraft.client.network.PlayerListEntry
+import net.minecraft.text.Text
+
+object Hud : FirmamentFeature {
+ override val identifier: String
+ get() = "hud"
+
+ object TConfig : ManagedConfig(identifier, Category.MISC) {
+ var dayCount by toggle("day-count") { false }
+ val dayCountHud by position("day-count-hud", 80, 10) { Point(0.5, 0.8) }
+ var fpsCount by toggle("fps-count") { false }
+ val fpsCountHud by position("fps-count-hud", 80, 10) { Point(0.5, 0.9) }
+ var pingCount by toggle("ping-count") { false }
+ val pingCountHud by position("ping-count-hud", 80, 10) { Point(0.5, 1.0) }
+ }
+
+ override val config: ManagedConfig
+ get() = TConfig
+
+ @Subscribe
+ fun onRenderHud(it: HudRenderEvent) {
+ if (TConfig.dayCount) {
+ it.context.matrices.push()
+ 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.pop()
+ }
+
+ if (TConfig.fpsCount) {
+ it.context.matrices.push()
+ 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.pop()
+ }
+
+ if (TConfig.pingCount) {
+ it.context.matrices.push()
+ 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.pop()
+ }
+ }
+}
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/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..d4bf560 100644
--- a/src/main/kotlin/features/world/FairySouls.kt
+++ b/src/main/kotlin/features/world/FairySouls.kt
@@ -3,6 +3,7 @@
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
@@ -100,7 +101,7 @@ object FairySouls : FirmamentFeature {
if (!TConfig.displaySouls) return
renderInWorld(it) {
currentMissingSouls.forEach {
- block(it.blockPos, 0x80FFFF00.toInt())
+ block(it.blockPos, Color.ofRGBA(176, 0, 255, 128).color)
}
color(1f, 0f, 1f, 1f)
currentLocationSouls.forEach {
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..3c8e895
--- /dev/null
+++ b/src/main/kotlin/features/world/TemporaryWaypoints.kt
@@ -0,0 +1,74 @@
+package moe.nea.firmament.features.world
+
+import me.shedaniel.math.Color
+import kotlin.compareTo
+import kotlin.text.clear
+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..b4f91b0 100644
--- a/src/main/kotlin/features/world/Waypoints.kt
+++ b/src/main/kotlin/features/world/Waypoints.kt
@@ -2,36 +2,26 @@ 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.mc.asFakeServer
import moe.nea.firmament.util.render.RenderInWorldContext
+import moe.nea.firmament.util.tr
object Waypoints : FirmamentFeature {
override val identifier: String
@@ -41,99 +31,85 @@ object Waypoints : FirmamentFeature {
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),
- )
- )
- .reversed()
- .forEach { (waypoint, col) ->
- val (index, pos) = waypoint
- block(pos, col.color)
- if (TConfig.showIndex)
- withFacingThePlayer(pos.toCenterPos()) {
- text(Text.literal(index.toString()))
- }
+ tracer(w.waypoints[orderedIndex].blockPos.toCenterPos(), 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.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,41 +118,77 @@ object Waypoints : FirmamentFeature {
event.subcommand("waypoint") {
thenArgument("pos", BlockPosArgumentType.blockPos()) { pos ->
thenExecute {
+ source
val position = pos.get(this).toAbsoluteBlockPos(source.asFakeServer())
- waypoints.add(position)
- source.sendFeedback(
- Text.stringifiedTranslatable(
- "firmament.command.waypoint.added",
- position.x,
- position.y,
- position.z
- )
- )
+ val w = useEditableWaypoints()
+ w.waypoints.add(FirmWaypoints.Waypoint.from(position))
+ source.sendFeedback(Text.stringifiedTranslatable("firmament.command.waypoint.added",
+ position.x,
+ position.y,
+ position.z))
}
}
}
- 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 +199,27 @@ 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,
- )
- }
- }
- }
- }
- }
+ fun textInvalidIndex(index: Int) =
+ tr("firmament.command.waypoint.invalid-index",
+ "Invalid index $index provided.")
- @Subscribe
- fun onWorldReady(event: WorldReadyEvent) {
- temporaryPlayerWaypointList.clear()
- }
+ 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 +232,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/CheckboxComponent.kt b/src/main/kotlin/gui/CheckboxComponent.kt
index 761c086..fc48661 100644
--- a/src/main/kotlin/gui/CheckboxComponent.kt
+++ b/src/main/kotlin/gui/CheckboxComponent.kt
@@ -28,8 +28,8 @@ class CheckboxComponent<T>(
val ctx = (context.renderContext as ModernRenderContext).drawContext
ctx.drawGuiTexture(
RenderLayer::getGuiTextured,
- if (isEnabled()) Firmament.identifier("firmament:widget/checkbox_checked")
- else Firmament.identifier("firmament:widget/checkbox_unchecked"),
+ if (isEnabled()) Firmament.identifier("widget/checkbox_checked")
+ else Firmament.identifier("widget/checkbox_unchecked"),
0, 0,
16, 16
)
@@ -43,6 +43,7 @@ class CheckboxComponent<T>(
isClicking = false
if (context.isHovered)
state.set(value)
+ blur()
return true
}
if (mouseEvent.mouseState && mouseEvent.mouseButton == 0 && context.isHovered) {
diff --git a/src/main/kotlin/gui/config/AllConfigsGui.kt b/src/main/kotlin/gui/config/AllConfigsGui.kt
index 73ff444..f9ffd2d 100644
--- a/src/main/kotlin/gui/config/AllConfigsGui.kt
+++ b/src/main/kotlin/gui/config/AllConfigsGui.kt
@@ -4,6 +4,12 @@ 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
@@ -18,6 +24,7 @@ object AllConfigsGui {
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 +73,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 +81,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/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/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/KeyBindingHandler.kt b/src/main/kotlin/gui/config/KeyBindingHandler.kt
index d7d0b47..14a4b32 100644
--- a/src/main/kotlin/gui/config/KeyBindingHandler.kt
+++ b/src/main/kotlin/gui/config/KeyBindingHandler.kt
@@ -40,34 +40,7 @@ class KeyBindingHandler(val name: String, val managedConfig: ManagedConfig) :
{ 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..1528ac4 100644
--- a/src/main/kotlin/gui/config/KeyBindingStateManager.kt
+++ b/src/main/kotlin/gui/config/KeyBindingStateManager.kt
@@ -1,8 +1,15 @@
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 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.SavedKeyBinding
class KeyBindingStateManager(
@@ -51,9 +58,11 @@ class KeyBindingStateManager(
) {
lastPressed = ch
} else {
- setValue(SavedKeyBinding(
- ch, modifiers
- ))
+ setValue(
+ SavedKeyBinding(
+ ch, modifiers
+ )
+ )
editing = false
blur()
lastPressed = 0
@@ -104,5 +113,34 @@ class KeyBindingStateManager(
label = stroke
}
+ fun createButton(): FirmButtonComponent {
+ return object : FirmButtonComponent(
+ TextComponent(
+ IMinecraft.instance.defaultFontRenderer,
+ { this@KeyBindingStateManager.label.string },
+ 130,
+ TextComponent.TextAlignment.LEFT,
+ false,
+ false
+ ), action = {
+ this@KeyBindingStateManager.onClick()
+ }) {
+ override fun keyboardEvent(event: KeyboardEvent, context: GuiImmediateContext): Boolean {
+ if (event is KeyboardEvent.KeyPressed) {
+ return this@KeyBindingStateManager.keyboardEvent(event.keycode, 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
index 7ddda9e..ba6792d 100644
--- a/src/main/kotlin/gui/config/ManagedConfig.kt
+++ b/src/main/kotlin/gui/config/ManagedConfig.kt
@@ -38,7 +38,9 @@ abstract class ManagedConfig(
MISC,
CHAT,
INVENTORY,
+ ITEMS,
MINING,
+ GARDEN,
EVENTS,
INTEGRATIONS,
META,
@@ -69,6 +71,7 @@ abstract class ManagedConfig(
category.configs.add(this)
}
+ // TODO: warn if two files use the same config file name :(
val file = Firmament.CONFIG_DIR.resolve("$name.json")
val data: JsonObject by lazy {
try {
diff --git a/src/main/kotlin/gui/config/ManagedOption.kt b/src/main/kotlin/gui/config/ManagedOption.kt
index 383f392..830086c 100644
--- a/src/main/kotlin/gui/config/ManagedOption.kt
+++ b/src/main/kotlin/gui/config/ManagedOption.kt
@@ -49,7 +49,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/entity/EntityRenderer.kt b/src/main/kotlin/gui/entity/EntityRenderer.kt
index fd7a0c4..a1b2577 100644
--- a/src/main/kotlin/gui/entity/EntityRenderer.kt
+++ b/src/main/kotlin/gui/entity/EntityRenderer.kt
@@ -27,41 +27,79 @@ 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),
+ "BREEZE" to t(EntityType.BREEZE),
+ "Bat" to t(EntityType.BAT),
+ "Bee" to t(EntityType.BEE),
+ "Blaze" to t(EntityType.BLAZE),
+ "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),
"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,
@@ -169,13 +207,13 @@ object EntityRenderer {
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
+ entity.lastHeadYaw = entity.yaw
val vector3f = Vector3f(0.0f, (entity.height / 2.0f + bottomOffset).toFloat(), 0.0f)
InventoryScreen.drawEntity(
context,
@@ -190,7 +228,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 ccf6b60..0000000
--- a/src/main/kotlin/gui/entity/FakeWorld.kt
+++ /dev/null
@@ -1,343 +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.boss.dragon.EnderDragonPart
-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 getEnderDragonParts(): MutableCollection<EnderDragonPart> {
- return mutableListOf()
- }
-
- 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/ModifyEquipment.kt b/src/main/kotlin/gui/entity/ModifyEquipment.kt
index a558936..2ef5007 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.setEncodedSkullOwner
-import moe.nea.firmament.util.mc.zeroUUID
+import moe.nea.firmament.util.mc.arbitraryUUID
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..7c8baa7 100644
--- a/src/main/kotlin/gui/entity/ModifyHorse.kt
+++ b/src/main/kotlin/gui/entity/ModifyHorse.kt
@@ -1,4 +1,3 @@
-
package moe.nea.firmament.gui.entity
import com.google.gson.JsonNull
@@ -7,6 +6,7 @@ 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 +15,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/keybindings/IKeyBinding.kt b/src/main/kotlin/keybindings/IKeyBinding.kt
index 1975361..9d9b106 100644
--- a/src/main/kotlin/keybindings/IKeyBinding.kt
+++ b/src/main/kotlin/keybindings/IKeyBinding.kt
@@ -6,24 +6,45 @@ import net.minecraft.client.option.KeyBinding
interface IKeyBinding {
fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean
+ fun matchesAtLeast(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
+ return old.matchesAtLeast(keyCode, scanCode, modifiers) && (modifiers and wantedModifiers) == wantedModifiers
}
- }
+
+ override fun matchesAtLeast(
+ keyCode: Int,
+ scanCode: Int,
+ modifiers: Int
+ ): Boolean {
+ return old.matchesAtLeast(keyCode, scanCode, modifiers) && (modifiers.inv() and wantedModifiers) == 0
+ }
+ }
}
companion object {
fun minecraft(keyBinding: KeyBinding) = object : IKeyBinding {
override fun matches(keyCode: Int, scanCode: Int, modifiers: Int) =
keyBinding.matchesKey(keyCode, scanCode)
- }
+
+ override fun matchesAtLeast(
+ keyCode: Int,
+ scanCode: Int,
+ modifiers: Int
+ ): Boolean =
+ keyBinding.matchesKey(keyCode, scanCode)
+ }
fun ofKeyCode(wantedKeyCode: Int) = object : IKeyBinding {
- override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean = keyCode == wantedKeyCode
- }
+ override fun matches(keyCode: Int, scanCode: Int, modifiers: Int): Boolean = keyCode == wantedKeyCode && modifiers == 0
+ override fun matchesAtLeast(
+ 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..01baa8f 100644
--- a/src/main/kotlin/keybindings/SavedKeyBinding.kt
+++ b/src/main/kotlin/keybindings/SavedKeyBinding.kt
@@ -1,5 +1,3 @@
-
-
package moe.nea.firmament.keybindings
import org.lwjgl.glfw.GLFW
@@ -8,99 +6,120 @@ 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
+// TODO: add support for mouse keybindings
@Serializable
data class SavedKeyBinding(
- val keyCode: Int,
- val shift: Boolean = false,
- val ctrl: Boolean = false,
- val alt: Boolean = false,
+ 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 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() = shiftKeys.any { InputUtil.isKeyPressed(h, it) }
+
+ fun unbound(): SavedKeyBinding =
+ SavedKeyBinding(GLFW.GLFW_KEY_UNKNOWN)
+
+ 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 isPressed(atLeast: Boolean = false): Boolean {
+ if (!isBound) return false
+ val h = MC.window.handle
+ if (!InputUtil.isKeyPressed(h, keyCode)) return false
+
+ // These are modifiers, so if the searched keyCode is a modifier key, then that key does not count as the modifier
+ val ctrl = keyCode !in controlKeys && controlKeys.any { InputUtil.isKeyPressed(h, it) }
+ val shift = keyCode !in shiftKeys && isShiftDown()
+ val alt = keyCode !in altKeys && altKeys.any { InputUtil.isKeyPressed(h, it) }
+ if (atLeast)
+ return (ctrl >= this.ctrl) &&
+ (alt >= this.alt) &&
+ (shift >= this.shift)
+
+ return (ctrl == this.ctrl) &&
+ (alt == this.alt) &&
+ (shift == this.shift)
+ }
+
+ override fun matchesAtLeast(keyCode: Int, scanCode: Int, modifiers: Int): Boolean {
+ if (this.keyCode == GLFW.GLFW_KEY_UNKNOWN) return false
+ val (shift, ctrl, alt) = getMods(modifiers)
+ return keyCode == this.keyCode && this.shift <= shift && this.ctrl <= ctrl && this.alt <= alt
+ }
+
+ 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)
+ }
+
+ override fun toString(): String {
+ return format().string
+ }
+
+ fun format(): Text {
+ val stroke = Text.literal("")
+ if (ctrl) {
+ stroke.append("CTRL + ")
+ }
+ if (alt) {
+ stroke.append("ALT + ")
+ }
+ if (shift) {
+ stroke.append("SHIFT + ") // TODO: translations?
+ }
+ if (InitLevel.isAtLeast(InitLevel.RENDER_INIT)) {
+ stroke.append(InputUtil.Type.KEYSYM.createFromCode(keyCode).localizedText)
+ } else {
+ stroke.append(keyCode.toString())
+ }
+ return stroke
+ }
}
diff --git a/src/main/kotlin/repo/BetterRepoRecipeCache.kt b/src/main/kotlin/repo/BetterRepoRecipeCache.kt
index 4b32e57..6d18223 100644
--- a/src/main/kotlin/repo/BetterRepoRecipeCache.kt
+++ b/src/main/kotlin/repo/BetterRepoRecipeCache.kt
@@ -2,8 +2,10 @@ 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(vararg val extraProviders: ExtraRecipeProvider) : IReloadable {
var usages: Map<SkyblockId, Set<NEURecipe>> = mapOf()
@@ -17,6 +19,9 @@ class BetterRepoRecipeCache(vararg val extraProviders: ExtraRecipeProvider) : IR
.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) }
}
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/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 e140dd8..14decd8 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,16 +29,18 @@ 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
@@ -44,6 +51,7 @@ 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 {
@@ -60,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().saveVersion.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
@@ -100,7 +113,7 @@ object ItemCache : IReloadable {
}
}
- fun un189Lore(lore: String): Text {
+ fun un189Lore(lore: String): MutableText {
val base = Text.literal("")
base.setStyle(Style.EMPTY.withItalic(false))
var lastColorCode = Style.EMPTY
@@ -129,18 +142,48 @@ object ItemCache : IReloadable {
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)
+ 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)
- val extraAttributes = oldItemTag.getCompound("tag").getCompound("ExtraAttributes")
- if (extraAttributes != null)
- itemInstance.set(DataComponentTypes.CUSTOM_DATA, NbtComponent.of(extraAttributes))
return itemInstance
} catch (e: Exception) {
e.printStackTrace()
@@ -148,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]
@@ -181,57 +229,49 @@ object ItemCache : IReloadable {
}
}
- 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
- }
+ var itemRecacheScope: CoroutineScope? = null
- @get:Bind("current")
- var current: Double = 0.0
+ private var recacheSoonSubmitted = mutableSetOf<SkyblockId>()
- @get:Bind("label")
- var label: String = ""
-
- @get:Bind("max")
- var max: Double = 0.0
-
- 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..e96a241
--- /dev/null
+++ b/src/main/kotlin/repo/MiningRepoData.kt
@@ -0,0 +1,133 @@
+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 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.jvm.optionals.getOrNull
+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.MC
+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.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 = ItemStack.fromNbt(MC.defaultRegistries, newCompound).getOrNull() ?: 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/RepoDownloadManager.kt b/src/main/kotlin/repo/RepoDownloadManager.kt
index 3efd83b..888248d 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
@@ -28,101 +26,102 @@ 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.Config.branch == "prerelease") {
+ RepoManager.Config.branch = "master"
+ }
+ val response =
+ Firmament.httpClient.get("https://api.github.com/repos/${RepoManager.Config.username}/${RepoManager.Config.reponame}/commits/${branchOverride ?: 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, 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.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) }
+ }
+ }
+ }
}
diff --git a/src/main/kotlin/repo/RepoManager.kt b/src/main/kotlin/repo/RepoManager.kt
index 6d9ba14..df89092 100644
--- a/src/main/kotlin/repo/RepoManager.kt
+++ b/src/main/kotlin/repo/RepoManager.kt
@@ -11,6 +11,7 @@ import kotlinx.coroutines.launch
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
@@ -46,6 +47,16 @@ object RepoManager {
}
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"),
+ ;
+
+ override fun asString(): String? = label
}
val currentDownloadedSha by RepoDownloadManager::latestSavedVersionHash
@@ -54,15 +65,20 @@ object RepoManager {
val essenceRecipeProvider = 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
@@ -73,7 +89,6 @@ object RepoManager {
}
}
}
- registerReloadListener(recipeCache)
}
}
@@ -100,16 +115,16 @@ 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()
}
}
@@ -127,20 +142,17 @@ object RepoManager {
return
}
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
}
}
diff --git a/src/main/kotlin/repo/RepoModResourcePack.kt b/src/main/kotlin/repo/RepoModResourcePack.kt
index 617efec..4fec14a 100644
--- a/src/main/kotlin/repo/RepoModResourcePack.kt
+++ b/src/main/kotlin/repo/RepoModResourcePack.kt
@@ -5,6 +5,7 @@ import java.nio.file.Files
import java.nio.file.Path
import java.util.*
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
@@ -23,14 +24,14 @@ import net.minecraft.resource.metadata.ResourceMetadata
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>) {
+ fun append(packs: ModResourcePackSorter) {
Firmament.logger.info("Registering mod resource pack")
- packs.add(RepoModResourcePack(RepoDownloadManager.repoSavedLocation))
+ packs.addPack(RepoModResourcePack(RepoDownloadManager.repoSavedLocation))
}
fun createResourceDirectly(identifier: Identifier): Optional<Resource> {
diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt
index 4d07801..01d1c4d 100644
--- a/src/main/kotlin/repo/SBItemStack.kt
+++ b/src/main/kotlin/repo/SBItemStack.kt
@@ -17,8 +17,10 @@ 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
@@ -27,12 +29,17 @@ 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
@@ -75,6 +82,7 @@ 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(
@@ -99,6 +107,13 @@ data class SBItemStack constructor(
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>,
@@ -135,6 +150,7 @@ data class SBItemStack constructor(
data class StatFormatting(
val postFix: String,
val color: Formatting,
+ val isStarAffected: Boolean = true,
)
val formattingOverrides = mapOf(
@@ -142,7 +158,7 @@ data class SBItemStack constructor(
"Strength" to StatFormatting("", Formatting.RED),
"Damage" to StatFormatting("", Formatting.RED),
"Bonus Attack Speed" to StatFormatting("%", Formatting.RED),
- "Shot Cooldown" to StatFormatting("s", 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),
@@ -184,9 +200,11 @@ data class SBItemStack constructor(
val color: Formatting,
val prefix: String,
val postFix: String,
+ val isHidden: Boolean,
) {
- REFORGE(Formatting.BLUE, "(", ")"),
-
+ REFORGE(Formatting.BLUE, "(", ")", false),
+ STAR_BUFF(Formatting.RESET, "", "", true),
+ CATA_STAR_BUFF(Formatting.DARK_GRAY, "(", ")", false),
;
}
@@ -194,7 +212,7 @@ data class SBItemStack constructor(
val statName: String,
val value: Text?,
val rest: List<Text> = listOf(),
- val valueNum: Double? = value?.directLiteralStringContent?.trim(' ', '%', '+')?.toDoubleOrNull()
+ val valueNum: Double? = value?.directLiteralStringContent?.trim(' ', 's', '%', '+')?.toDoubleOrNull()
) {
fun addStat(amount: Double, buffKind: BuffKind): StatLine {
val formattedAmount = FirmFormatters.formatCommas(amount, 1, includeSign = true)
@@ -202,18 +220,26 @@ data class SBItemStack constructor(
valueNum = (valueNum ?: 0.0) + amount,
value = null,
rest = rest +
- listOf(
+ if (buffKind.isHidden) emptyList()
+ else listOf(
Text.literal(
buffKind.prefix + formattedAmount +
statFormatting.postFix +
- buffKind.postFix + " ")
- .withColor(buffKind.color)))
+ buffKind.postFix + " "
+ )
+ .withColor(buffKind.color)
+ )
+ )
}
fun formatValue() =
- Text.literal(FirmFormatters.formatCommas(valueNum ?: 0.0,
- 1,
- includeSign = true) + statFormatting.postFix + " ")
+ 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)
@@ -237,7 +263,7 @@ data class SBItemStack constructor(
return segments.joinToString(" ") { it.replaceFirstChar { it.uppercaseChar() } }
}
- private fun parseStatLine(line: Text): StatLine? {
+ 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
@@ -308,14 +334,39 @@ data class SBItemStack constructor(
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 {
@@ -325,12 +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 { appendReforgeInfo(it) }
- .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
@@ -340,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,
@@ -359,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..e38380c 100644
--- a/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt
+++ b/src/main/kotlin/repo/recipes/SBCraftingRecipeRenderer.kt
@@ -12,17 +12,24 @@ 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)
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 +39,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 +55,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..d358e6a
--- /dev/null
+++ b/src/main/kotlin/repo/recipes/SBEssenceUpgradeRecipeRenderer.kt
@@ -0,0 +1,76 @@
+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 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..9fdb756
--- /dev/null
+++ b/src/main/kotlin/repo/recipes/SBForgeRecipeRenderer.kt
@@ -0,0 +1,83 @@
+package moe.nea.firmament.repo.recipes
+
+import io.github.moulberry.repo.NEURepository
+import io.github.moulberry.repo.data.NEUCraftingRecipe
+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)
+ layouter.createTooltip(
+ arrow,
+ Text.stringifiedTranslatable(
+ "firmament.recipe.forge.time",
+ recipe.duration.seconds
+ )
+ )
+
+ 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/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/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 4b32c2a..03dafc5 100644
--- a/src/main/kotlin/util/FirmFormatters.kt
+++ b/src/main/kotlin/util/FirmFormatters.kt
@@ -9,11 +9,40 @@ 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, includeSign: Boolean = false): String {
if (long < 0 && long != Long.MIN_VALUE) {
@@ -103,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/HoveredItemStack.kt b/src/main/kotlin/util/HoveredItemStack.kt
index a2e4ad2..526820a 100644
--- a/src/main/kotlin/util/HoveredItemStack.kt
+++ b/src/main/kotlin/util/HoveredItemStack.kt
@@ -24,4 +24,4 @@ class VanillaScreenProvider : HoveredItemStackProvider {
val HandledScreen<*>.focusedItemStack: ItemStack?
get() =
HoveredItemStackProvider.allValidInstances
- .firstNotNullOfOrNull { it.provideHoveredItemStack(this) }
+ .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/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 215d2a8..e85b119 100644
--- a/src/main/kotlin/util/MC.kt
+++ b/src/main/kotlin/util/MC.kt
@@ -1,23 +1,31 @@
package moe.nea.firmament.util
import io.github.moulberry.repo.data.Coordinate
+import io.github.notenoughupdates.moulconfig.gui.GuiComponentWrapper
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.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.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
@@ -85,6 +93,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
@@ -96,6 +105,7 @@ object MC {
inline val soundManager get() = instance.soundManager
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() = TestUtil.unlessTesting { instance.world }
inline val playerName: String? get() = player?.name?.unformattedString
@@ -109,12 +119,32 @@ object MC {
val defaultRegistries: RegistryWrapper.WrapperLookup by lazy { BuiltinRegistries.createWrapperLookup() }
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? GuiComponentWrapper)?.context
+
+ 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/MoulConfigUtils.kt b/src/main/kotlin/util/MoulConfigUtils.kt
index 62bf3dd..51ff340 100644
--- a/src/main/kotlin/util/MoulConfigUtils.kt
+++ b/src/main/kotlin/util/MoulConfigUtils.kt
@@ -25,6 +25,7 @@ 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 moe.nea.firmament.gui.BarComponent
import moe.nea.firmament.gui.FirmButtonComponent
import moe.nea.firmament.gui.FirmHoverComponent
@@ -34,6 +35,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 +96,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 +196,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 +211,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 +225,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 : GuiComponentWrapper(guiContext) {
override fun close() {
if (context.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 })
@@ -257,7 +265,17 @@ object MoulConfigUtils {
keyboardEvent: KeyboardEvent
): Boolean {
val immContext = createInPlaceFullContext(null, IMinecraft.instance.mouseX, IMinecraft.instance.mouseY)
- return component.keyboardEvent(keyboardEvent, immContext.translated(x, y, w, h))
+ 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(
@@ -277,12 +295,14 @@ object MoulConfigUtils {
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())
+ val immContext = GuiImmediateContext(
+ context,
+ 0, 0, 0, 0,
+ mouseX, mouseY,
+ mouseX, mouseY,
+ mouseX.toFloat(),
+ mouseY.toFloat()
+ )
return immContext
}
diff --git a/src/main/kotlin/util/SBData.kt b/src/main/kotlin/util/SBData.kt
index e785ff6..1a4734c 100644
--- a/src/main/kotlin/util/SBData.kt
+++ b/src/main/kotlin/util/SBData.kt
@@ -19,6 +19,11 @@ object SBData {
"CLICK THIS TO SUGGEST IT IN CHAT [NO DASHES]",
)
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
@@ -26,6 +31,10 @@ object SBData {
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"
@@ -37,11 +46,15 @@ object SBData {
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()
}
}
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 f15cadb..e7f955a 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,32 +12,41 @@ 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 GARDEN = forMode("garden")
- }
-
- 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 hasCustomMining
+ get() = RepoManager.miningData.customMiningAreas[this]?.isSpecialMining ?: false
+
+ 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 631b444..b4d583a 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,43 @@ 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
/**
* 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 +70,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 +92,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,9 +113,11 @@ 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 }
@@ -119,14 +135,45 @@ var 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, 2)
+ appendLiteral("/")
+ appendValue(ChronoField.DAY_OF_MONTH, 2)
+ 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)
@@ -135,8 +182,8 @@ private val petDataCache = WeakCache.memoize<ItemStack, Optional<HypixelPetInfo>
}
fun ItemStack.getUpgradeStars(): Int {
- return extraAttributes.getInt("upgrade_level").takeIf { it > 0 }
- ?: extraAttributes.getInt("dungeon_item_level").takeIf { it > 0 }
+ return extraAttributes.getInt("upgrade_level").getOrNull()?.takeIf { it > 0 }
+ ?: extraAttributes.getInt("dungeon_item_level").getOrNull()?.takeIf { it > 0 }
?: 0
}
@@ -145,7 +192,7 @@ fun ItemStack.getUpgradeStars(): Int {
value class ReforgeId(val id: String)
fun ItemStack.getReforgeId(): ReforgeId? {
- return extraAttributes.getString("modifier").takeIf { it.isNotBlank() }?.let(::ReforgeId)
+ return extraAttributes.getString("modifier").getOrNull()?.takeIf { it.isNotBlank() }?.let(::ReforgeId)
}
val ItemStack.petData: HypixelPetInfo?
@@ -159,8 +206,8 @@ fun ItemStack.setSkyBlockId(skyblockId: SkyblockId): ItemStack {
val ItemStack.skyBlockId: SkyblockId?
get() {
- return when (val id = extraAttributes.getString("id")) {
- "" -> {
+ return when (val id = extraAttributes.getString("id").getOrNull()) {
+ "", null -> {
null
}
@@ -170,25 +217,67 @@ 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()
+ when {
+ potionName != null -> SkyblockId("POTION_${potionName.uppercase()};$potionLevel")
+ potionData != null -> SkyblockId("POTION_${potionData.uppercase()};$potionLevel")
+ potionType != null -> SkyblockId("POTION_${potionType.uppercase()}")
+ 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..dc98dc0 100644
--- a/src/main/kotlin/util/StringUtil.kt
+++ b/src/main/kotlin/util/StringUtil.kt
@@ -9,6 +9,8 @@ object StringUtil {
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/TestUtil.kt b/src/main/kotlin/util/TestUtil.kt
index 45e3dde..da8ba38 100644
--- a/src/main/kotlin/util/TestUtil.kt
+++ b/src/main/kotlin/util/TestUtil.kt
@@ -2,6 +2,7 @@ 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/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..2c546ba 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.gui.FirmButtonComponent
import moe.nea.firmament.keybindings.IKeyBinding
+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: 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)
+ }
+ }
+ }
}
suspend fun waitForInput(keybind: IKeyBinding): Unit = suspendCancellableCoroutine { cont ->
- val unregister =
- InputHandler.registerContinuation(InputHandler.KeyInputContinuation(keybind) { cont.resume(Unit) })
- cont.invokeOnCancellation {
- unregister()
- }
+ 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/MultiFileDataHolder.kt b/src/main/kotlin/util/data/MultiFileDataHolder.kt
new file mode 100644
index 0000000..94c6f05
--- /dev/null
+++ b/src/main/kotlin/util/data/MultiFileDataHolder.kt
@@ -0,0 +1,63 @@
+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*/
+ IDataHolder.badLoads.add(configName)
+ 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/json/DashlessUUIDSerializer.kt b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt
index acb1dc8..6bafebe 100644
--- a/src/main/kotlin/util/json/DashlessUUIDSerializer.kt
+++ b/src/main/kotlin/util/json/DashlessUUIDSerializer.kt
@@ -10,6 +10,7 @@ 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 +18,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/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/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/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/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/NbtPrism.kt b/src/main/kotlin/util/mc/NbtPrism.kt
new file mode 100644
index 0000000..f034210
--- /dev/null
+++ b/src/main/kotlin/util/mc/NbtPrism.kt
@@ -0,0 +1,91 @@
+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.brigadier.context.CommandContext
+import com.mojang.brigadier.suggestion.Suggestions
+import com.mojang.brigadier.suggestion.SuggestionsBuilder
+import com.mojang.serialization.JsonOps
+import java.util.concurrent.CompletableFuture
+import kotlin.collections.indices
+import kotlin.collections.map
+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..1b7dcba 100644
--- a/src/main/kotlin/util/mc/SkullItemData.kt
+++ b/src/main/kotlin/util/mc/SkullItemData.kt
@@ -10,7 +10,6 @@ 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
@@ -51,7 +50,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/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..f713a81
--- /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.TriState
+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.MATRICES_COLOR_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("InnerCutoutRadius", UniformType.FLOAT)
+ .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.FLOAT)
+ .build()
+}
+
+object CustomRenderLayers {
+ inline fun memoizeTextured(crossinline func: (Identifier) -> RenderLayer) = 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, TriState.DEFAULT, 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,
+ 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..a833c86 100644
--- a/src/main/kotlin/util/render/DrawContextExt.kt
+++ b/src/main/kotlin/util/render/DrawContextExt.kt
@@ -3,50 +3,16 @@ package moe.nea.firmament.util.render
import com.mojang.blaze3d.systems.RenderSystem
import me.shedaniel.math.Color
import org.joml.Matrix4f
+import util.render.CustomRenderLayers
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.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))
- }
-}
-
@Deprecated("Use the other drawGuiTexture")
fun DrawContext.drawGuiTexture(
x: Int, y: Int, z: Int, width: Int, height: Int, sprite: Identifier
@@ -91,10 +57,11 @@ fun DrawContext.drawLine(fromX: Int, fromY: Int, toX: Int, toY: Int, color: Colo
}
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)
+ val buf = vertexConsumers.getBuffer(CustomRenderLayers.LINES)
+ val matrix = this.matrices.peek()
+ buf.vertex(matrix, 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)
+ buf.vertex(matrix, toX.toFloat(), toY.toFloat(), 0F).color(color.color)
.normal(toX - fromX.toFloat(), toY - fromY.toFloat(), 0F)
}
}
diff --git a/src/main/kotlin/util/render/FacingThePlayerContext.kt b/src/main/kotlin/util/render/FacingThePlayerContext.kt
index daa8da9..670beb6 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 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
diff --git a/src/main/kotlin/util/render/FirmamentShaders.kt b/src/main/kotlin/util/render/FirmamentShaders.kt
index ba67dbb..cc6cd49 100644
--- a/src/main/kotlin/util/render/FirmamentShaders.kt
+++ b/src/main/kotlin/util/render/FirmamentShaders.kt
@@ -1,9 +1,10 @@
package moe.nea.firmament.util.render
+import com.mojang.blaze3d.vertex.VertexFormat
+import net.minecraft.client.gl.CompiledShader
import net.minecraft.client.gl.Defines
-import net.minecraft.client.gl.ShaderProgramKey
+import net.minecraft.client.gl.ShaderProgram
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
@@ -11,20 +12,9 @@ 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..63a13ec 100644
--- a/src/main/kotlin/util/render/LerpUtils.kt
+++ b/src/main/kotlin/util/render/LerpUtils.kt
@@ -1,33 +1,36 @@
-
package moe.nea.firmament.util.render
import me.shedaniel.math.Color
-val pi = Math.PI
-val tau = 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()
+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(τ) - 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/RenderCircleProgress.kt b/src/main/kotlin/util/render/RenderCircleProgress.kt
index 805633c..81dde6f 100644
--- a/src/main/kotlin/util/render/RenderCircleProgress.kt
+++ b/src/main/kotlin/util/render/RenderCircleProgress.kt
@@ -1,93 +1,101 @@
package moe.nea.firmament.util.render
import com.mojang.blaze3d.systems.RenderSystem
+import com.mojang.blaze3d.vertex.VertexFormat
import io.github.notenoughupdates.moulconfig.platform.next
+import java.util.OptionalInt
import org.joml.Matrix4f
-import org.joml.Vector2f
-import kotlin.math.atan2
-import kotlin.math.tan
+import util.render.CustomRenderLayers
import net.minecraft.client.gui.DrawContext
-import net.minecraft.client.render.BufferRenderer
+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.util.BufferAllocator
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
object RenderCircleProgress {
- fun renderCircle(
+ fun renderCircularSlice(
drawContext: DrawContext,
- texture: Identifier,
- progress: Float,
+ layer: RenderLayer,
u1: Float,
u2: Float,
v1: Float,
v2: Float,
+ angleRadians: ClosedFloatingPointRange<Float>,
+ color: Int = -1,
+ innerCutoutRadius: Float = 0F
) {
- 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),
- )
+ drawContext.draw()
+ val sections = angleRadians.nonNegligibleSubSectionsAlignedWith((τ / 8f).toFloat())
+ .zipWithNext().toList()
+ BufferAllocator(layer.vertexFormat.vertexSize * sections.size * 3).use { allocator ->
- 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)
- }
- }
+ val bufferBuilder = BufferBuilder(allocator, VertexFormat.DrawMode.TRIANGLES, layer.vertexFormat)
+ val matrix: Matrix4f = drawContext.matrices.peek().positionMatrix
+ 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, second.x, second.y, 0F)
- .texture(lerp(u1, u2, ilerp(second.x)), lerp(v1, v2, ilerp(second.y)))
- .color(-1)
+ .vertex(matrix, secondPoint.x, secondPoint.y, 0F)
+ .texture(lerp(u1, u2, ilerp(secondPoint.x)), lerp(v1, v2, ilerp(secondPoint.y)))
+ .color(color)
.next()
bufferBuilder
- .vertex(matrix, first.x, first.y, 0F)
- .texture(lerp(u1, u2, ilerp(first.x)), lerp(v1, v2, ilerp(first.y)))
- .color(-1)
+ .vertex(matrix, firstPoint.x, firstPoint.y, 0F)
+ .texture(lerp(u1, u2, ilerp(firstPoint.x)), lerp(v1, v2, ilerp(firstPoint.y)))
+ .color(color)
.next()
bufferBuilder
.vertex(matrix, 0F, 0F, 0F)
.texture(lerp(u1, u2, ilerp(0F)), lerp(v1, v2, ilerp(0F)))
- .color(-1)
+ .color(color)
.next()
}
+
+ bufferBuilder.end().use { buffer ->
+ // TODO: write a better utility to pass uniforms :sob: ill even take a mixin at this point
+ if (innerCutoutRadius <= 0) {
+ layer.draw(buffer)
+ return
+ }
+ val vertexBuffer = layer.vertexFormat.uploadImmediateVertexBuffer(buffer.buffer)
+ val indexBufferConstructor = RenderSystem.getSequentialBuffer(VertexFormat.DrawMode.TRIANGLES)
+ val indexBuffer = indexBufferConstructor.getIndexBuffer(buffer.drawParameters.indexCount)
+ RenderSystem.getDevice().createCommandEncoder().createRenderPass(
+ MC.instance.framebuffer.colorAttachment,
+ OptionalInt.empty(),
+ ).use { renderPass ->
+ renderPass.setPipeline(layer.pipeline)
+ renderPass.setUniform("InnerCutoutRadius", innerCutoutRadius)
+ renderPass.setIndexBuffer(indexBuffer, indexBufferConstructor.indexType)
+ renderPass.setVertexBuffer(0, vertexBuffer)
+ renderPass.drawIndexed(0, buffer.drawParameters.indexCount)
+ }
+ }
}
- RenderSystem.disableBlend()
}
-
+ fun renderCircle(
+ drawContext: DrawContext,
+ texture: Identifier,
+ progress: Float,
+ u1: Float,
+ u2: Float,
+ v1: Float,
+ v2: Float,
+ ) {
+ 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..4963920 100644
--- a/src/main/kotlin/util/render/RenderInWorldContext.kt
+++ b/src/main/kotlin/util/render/RenderInWorldContext.kt
@@ -5,15 +5,12 @@ 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 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
@@ -23,51 +20,17 @@ import net.minecraft.util.math.BlockPos
import net.minecraft.util.math.Vec3d
import moe.nea.firmament.events.WorldRenderLastEvent
import moe.nea.firmament.util.FirmFormatters
+import moe.nea.firmament.util.IntUtil.toRGBA
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) {
@@ -82,7 +45,7 @@ class RenderInWorldContext private constructor(
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()
}
@@ -155,7 +118,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()
}
@@ -182,8 +145,7 @@ class RenderInWorldContext private constructor(
fun line(points: List<Vec3d>, 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
@@ -203,7 +165,6 @@ class RenderInWorldContext private constructor(
.next()
}
- RenderLayers.LINES.draw(buffer.end())
}
// TODO: put the favourite icons in front of items again
@@ -244,53 +205,54 @@ class RenderInWorldContext private constructor(
}
}
- private fun buildCube(matrix: Matrix4f, buf: VertexConsumer, color: Int) {
+ private fun buildCube(matrix: Matrix4f, buf: VertexConsumer, colorInt: Int) {
+ val (r, g, b, a) = colorInt.toRGBA()
+
// 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)
+ buf.vertex(matrix, 0F, 0F, 0F).color(r, g, b, a)
+ buf.vertex(matrix, 0F, 0F, 1F).color(r, g, b, a)
+ buf.vertex(matrix, 1F, 0F, 1F).color(r, g, b, a)
+ buf.vertex(matrix, 1F, 0F, 0F).color(r, g, b, a)
// 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)
+ buf.vertex(matrix, 0F, 1F, 0F).color(r, g, b, a)
+ buf.vertex(matrix, 1F, 1F, 0F).color(r, g, b, a)
+ buf.vertex(matrix, 1F, 1F, 1F).color(r, g, b, a)
+ buf.vertex(matrix, 0F, 1F, 1F).color(r, g, b, a)
// 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)
+ buf.vertex(matrix, 0F, 0F, 0F).color(r, g, b, a)
+ buf.vertex(matrix, 0F, 0F, 1F).color(r, g, b, a)
+ buf.vertex(matrix, 0F, 1F, 1F).color(r, g, b, a)
+ buf.vertex(matrix, 0F, 1F, 0F).color(r, g, b, a)
// 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)
+ buf.vertex(matrix, 1F, 0F, 0F).color(r, g, b, a)
+ buf.vertex(matrix, 1F, 1F, 0F).color(r, g, b, a)
+ buf.vertex(matrix, 1F, 1F, 1F).color(r, g, b, a)
+ buf.vertex(matrix, 1F, 0F, 1F).color(r, g, b, a)
// 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)
+ buf.vertex(matrix, 0F, 0F, 0F).color(r, g, b, a)
+ buf.vertex(matrix, 1F, 0F, 0F).color(r, g, b, a)
+ buf.vertex(matrix, 1F, 1F, 0F).color(r, g, b, a)
+ buf.vertex(matrix, 0F, 1F, 0F).color(r, g, b, a)
// 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)
+ buf.vertex(matrix, 0F, 0F, 1F).color(r, g, b, a)
+ buf.vertex(matrix, 0F, 1F, 1F).color(r, g, b, a)
+ buf.vertex(matrix, 1F, 1F, 1F).color(r, g, b, a)
+ buf.vertex(matrix, 1F, 0F, 1F).color(r, g, b, a)
}
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()
+// 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,
@@ -302,10 +264,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..8f8bdcf 100644
--- a/src/main/kotlin/util/render/TranslatedScissors.kt
+++ b/src/main/kotlin/util/render/TranslatedScissors.kt
@@ -1,11 +1,15 @@
package moe.nea.firmament.util.render
+import org.joml.Matrix4f
import org.joml.Vector4f
import net.minecraft.client.gui.DrawContext
fun DrawContext.enableScissorWithTranslation(x1: Float, y1: Float, x2: Float, y2: Float) {
- val pMat = matrices.peek().positionMatrix
+ enableScissor(x1.toInt(), y1.toInt(), x2.toInt(), y2.toInt())
+}
+fun DrawContext.enableScissorWithoutTranslation(x1: Float, y1: Float, x2: Float, y2: Float) {
+ val pMat = matrices.peek().positionMatrix.invert(Matrix4f())
val target = Vector4f()
target.set(x1, y1, 0f, 1f)
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 6c7096c..7a776b5 100644
--- a/src/main/kotlin/util/skyblock/ItemType.kt
+++ b/src/main/kotlin/util/skyblock/ItemType.kt
@@ -67,6 +67,8 @@ value class ItemType private constructor(val name: String) {
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/SackUtil.kt b/src/main/kotlin/util/skyblock/SackUtil.kt
index fd67c44..c46542e 100644
--- a/src/main/kotlin/util/skyblock/SackUtil.kt
+++ b/src/main/kotlin/util/skyblock/SackUtil.kt
@@ -93,7 +93,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 cfd8429..4f208dd 100644
--- a/src/main/kotlin/util/skyblock/SkyBlockItems.kt
+++ b/src/main/kotlin/util/skyblock/SkyBlockItems.kt
@@ -3,9 +3,20 @@ 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")
}
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 ab3de43..cfda2e9 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
@@ -135,15 +156,17 @@ 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.clickCommand(command: String): MutableText {
require(command.startsWith("/"))
return this.styled {
- it.withClickEvent(ClickEvent(ClickEvent.Action.RUN_COMMAND, command))
+ it.withClickEvent(ClickEvent.RunCommand(command))
}
}
@@ -174,4 +197,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)
diff --git a/src/main/resources/assets/firmament/gui/config/macros/combos.xml b/src/main/resources/assets/firmament/gui/config/macros/combos.xml
new file mode 100644
index 0000000..5141125
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/config/macros/combos.xml
@@ -0,0 +1,55 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig"
+>
+ <Panel background="TRANSPARENT" insets="10">
+ <Column>
+ <ScrollPanel width="380" height="300">
+ <Align horizontal="CENTER">
+ <Array data="@actions">
+ <!-- evenBackground="#8B8B8B" oddBackground="#C6C6C6" -->
+ <Panel background="TRANSPARENT" insets="3">
+ <Panel background="VANILLA" insets="6">
+ <Column>
+ <Row>
+ <Text text="@command" width="280"/>
+ </Row>
+ <Row>
+ <Text text="@formattedCombo" width="250"/>
+ <Align horizontal="RIGHT">
+ <Row>
+ <firm:Button onClick="@edit">
+ <Text text="Edit"/>
+ </firm:Button>
+ <Spacer width="12"/>
+ <firm:Button onClick="@delete">
+ <Text text="Delete"/>
+ </firm:Button>
+ </Row>
+ </Align>
+ </Row>
+ </Column>
+ </Panel>
+
+ </Panel>
+ </Array>
+ </Align>
+ </ScrollPanel>
+ <Align horizontal="RIGHT">
+ <Row>
+ <firm:Button onClick="@discard">
+ <Text text="Discard Changes"/>
+ </firm:Button>
+ <firm:Button onClick="@saveAndClose">
+ <Text text="Save &amp; Close"/>
+ </firm:Button>
+ <firm:Button onClick="@save">
+ <Text text="Save"/>
+ </firm:Button>
+ <firm:Button onClick="@addCommand">
+ <Text text="Add Combo Command"/>
+ </firm:Button>
+ </Row>
+ </Align>
+ </Column>
+ </Panel>
+</Root>
diff --git a/src/main/resources/assets/firmament/gui/config/macros/editor_combo.xml b/src/main/resources/assets/firmament/gui/config/macros/editor_combo.xml
new file mode 100644
index 0000000..50a1d99
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/config/macros/editor_combo.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig"
+>
+ <Center>
+ <Panel background="VANILLA" insets="10">
+ <Column>
+ <Row>
+ <firm:Button onClick="@back">
+ <Text text="←"/>
+ </firm:Button>
+ <Text text="Editing command macro"/>
+ </Row>
+ <Row>
+ <Text text="Command: /"/>
+ <Align horizontal="RIGHT">
+ <TextField value="@command" width="200"/>
+ </Align>
+ </Row>
+ <Row>
+ <Text text="Key Combo:"/>
+ <Align horizontal="RIGHT">
+ <firm:Button onClick="@addStep">
+ <Text text="+"/>
+ </firm:Button>
+ </Align>
+ </Row>
+ <Array data="@combo">
+ <Row>
+ <firm:Fixed width="160">
+ <Indirect value="@button"/>
+ </firm:Fixed>
+ <Align horizontal="RIGHT">
+ <firm:Button onClick="@delete">
+ <Text text="Delete"/>
+ </firm:Button>
+ </Align>
+ </Row>
+ </Array>
+ </Column>
+ </Panel>
+ </Center>
+</Root>
diff --git a/src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml b/src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml
new file mode 100644
index 0000000..e4dc2b4
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/config/macros/editor_wheel.xml
@@ -0,0 +1,43 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig"
+>
+ <Center>
+ <Panel background="VANILLA" insets="10">
+ <Column>
+ <Row>
+ <firm:Button onClick="@back">
+ <Text text="←"/>
+ </firm:Button>
+ <Text text="Editing wheel macro"/>
+ </Row>
+ <Row>
+ <Text text="Key (Hold):"/>
+ <Align horizontal="RIGHT">
+ <firm:Fixed width="160">
+ <Indirect value="@button"/>
+ </firm:Fixed>
+ </Align>
+ </Row>
+ <Row>
+ <Text text="Menu Options:"/>
+ <Align horizontal="RIGHT">
+ <firm:Button onClick="@addOption">
+ <Text text="+"/>
+ </firm:Button>
+ </Align>
+ </Row>
+ <Array data="@editableCommands">
+ <Row>
+ <Text text="/"/>
+ <TextField value="@text" width="160"/>
+ <Align horizontal="RIGHT">
+ <firm:Button onClick="@delete">
+ <Text text="Delete"/>
+ </firm:Button>
+ </Align>
+ </Row>
+ </Array>
+ </Column>
+ </Panel>
+ </Center>
+</Root>
diff --git a/src/main/resources/assets/firmament/gui/config/macros/index.xml b/src/main/resources/assets/firmament/gui/config/macros/index.xml
new file mode 100644
index 0000000..f6a1545
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/config/macros/index.xml
@@ -0,0 +1,27 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Root xmlns="http://notenoughupdates.org/moulconfig"
+>
+ <Center>
+ <Row>
+ <Tabs>
+ <Tab>
+ <Tab.Header>
+ <Text text="Combo Macros"/>
+ </Tab.Header>
+ <Tab.Body>
+ <Fragment value="firmament:gui/config/macros/combos.xml" bind="@combos"/>
+ </Tab.Body>
+ </Tab>
+ <Tab>
+ <Tab.Header>
+ <Text text="Macro Wheel"/>
+ </Tab.Header>
+ <Tab.Body>
+ <Fragment value="firmament:gui/config/macros/wheel.xml" bind="@wheels"/>
+ </Tab.Body>
+ </Tab>
+ </Tabs>
+ <Meta beforeClose="@beforeClose"/>
+ </Row>
+ </Center>
+</Root>
diff --git a/src/main/resources/assets/firmament/gui/config/macros/wheel.xml b/src/main/resources/assets/firmament/gui/config/macros/wheel.xml
new file mode 100644
index 0000000..19922fe
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/config/macros/wheel.xml
@@ -0,0 +1,54 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Root xmlns="http://notenoughupdates.org/moulconfig" xmlns:firm="http://firmament.nea.moe/moulconfig"
+>
+ <Panel background="TRANSPARENT" insets="10">
+ <Column>
+ <ScrollPanel width="380" height="300">
+ <Align horizontal="CENTER">
+ <Array data="@wheels">
+ <Panel background="TRANSPARENT" insets="3">
+ <Panel background="VANILLA" insets="6">
+ <Column>
+ <Row>
+ <Text text="@keyCombo" width="250"/>
+ <Align horizontal="RIGHT">
+ <Row>
+ <firm:Button onClick="@edit">
+ <Text text="Edit"/>
+ </firm:Button>
+ <Spacer width="12"/>
+ <firm:Button onClick="@delete">
+ <Text text="Delete"/>
+ </firm:Button>
+ </Row>
+ </Align>
+ </Row>
+ <Array data="@commands">
+ <Text text="@text" width="280"/>
+ </Array>
+ </Column>
+ </Panel>
+
+ </Panel>
+ </Array>
+ </Align>
+ </ScrollPanel>
+ <Align horizontal="RIGHT">
+ <Row>
+ <firm:Button onClick="@discard">
+ <Text text="Discard Changes"/>
+ </firm:Button>
+ <firm:Button onClick="@saveAndClose">
+ <Text text="Save &amp; Close"/>
+ </firm:Button>
+ <firm:Button onClick="@save">
+ <Text text="Save"/>
+ </firm:Button>
+ <firm:Button onClick="@addWheel">
+ <Text text="Add Wheel"/>
+ </firm:Button>
+ </Row>
+ </Align>
+ </Column>
+ </Panel>
+</Root>
diff --git a/src/main/resources/assets/firmament/gui/license_viewer/index.xml b/src/main/resources/assets/firmament/gui/license_viewer/index.xml
new file mode 100644
index 0000000..c23153d
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/license_viewer/index.xml
@@ -0,0 +1,65 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Root xmlns="http://notenoughupdates.org/moulconfig"
+ xmlns:firm="http://firmament.nea.moe/moulconfig"
+>
+ <Center>
+ <Panel background="VANILLA">
+ <Column>
+ <Center>
+ <Scale scale="2">
+ <Text text="Firmament Licenses"/>
+ </Scale>
+ </Center>
+ <!-- <firm:Line/>-->
+ <ScrollPanel width="306" height="250">
+ <Panel insets="3" background="TRANSPARENT">
+ <Array data="@softwares">
+ <Center>
+ <firm:Fixed width="300">
+ <Panel background="VANILLA" insets="8">
+ <Column>
+ <Scale scale="1.2">
+ <Text text="@projectName"/>
+ </Scale>
+ <When condition="@hasWebPresence">
+ <Row>
+ <firm:Button onClick="@open">
+ <Text text="Navigate to WebSite"/>
+ </firm:Button>
+ </Row>
+ <Spacer/>
+ </When>
+ <Text text="@projectDescription" width="280"/>
+ <Array data="@developers">
+ <Row>
+ <Text text="by "/>
+ <Text text="@name"/>
+ </Row>
+ </Array>
+ <Array data="@licenses">
+ <When condition="@hasUrl">
+ <firm:Button onClick="@open">
+ <Center>
+ <Row>
+ <Text text="License: "/>
+ <Text text="@name"/>
+ </Row>
+ </Center>
+ </firm:Button>
+ <Row>
+ <Text text="License: "/>
+ <Text text="@name"/>
+ </Row>
+ </When>
+ </Array>
+ </Column>
+ </Panel>
+ </firm:Fixed>
+ </Center>
+ </Array>
+ </Panel>
+ </ScrollPanel>
+ </Column>
+ </Panel>
+ </Center>
+</Root>
diff --git a/src/main/resources/assets/firmament/gui/mining_block_info/index.xml b/src/main/resources/assets/firmament/gui/mining_block_info/index.xml
new file mode 100644
index 0000000..6404995
--- /dev/null
+++ b/src/main/resources/assets/firmament/gui/mining_block_info/index.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8" ?>
+<Root xmlns="http://notenoughupdates.org/moulconfig"
+ xmlns:firm="http://firmament.nea.moe/moulconfig"
+>
+ <Gui>
+ <Column>
+ <Row>
+ <Text text="Search: "/>
+ <TextField value="@search"/>
+ </Row>
+ <ScrollPanel width="200" height="150">
+ <Array data="@ores">
+ <Column>
+ <Text text="@oreName"/>
+ <Array data="@blocks">
+ <Row>
+ <When condition="@isSelected">
+ <Center>
+ <Text text="§a+" textAlign="CENTER" width="10"/>
+ </Center>
+ <Spacer width="10" height="0"/>
+ </When>
+ <firm:Hover lines="@restrictions">
+ <Row>
+ <ItemStack value="@item"/>
+ <Align horizontal="LEFT" vertical="CENTER">
+ <Text text="@itemName"/>
+ </Align>
+ </Row>
+ </firm:Hover>
+ </Row>
+ </Array>
+ </Column>
+ </Array>
+ </ScrollPanel>
+ </Column>
+ </Gui>
+</Root>
diff --git a/src/main/resources/assets/firmament/logo.png b/src/main/resources/assets/firmament/logo.png
index e00a2fa..e3f063a 100644
--- a/src/main/resources/assets/firmament/logo.png
+++ b/src/main/resources/assets/firmament/logo.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/shaders/cape/parallax.fsh b/src/main/resources/assets/firmament/shaders/cape/parallax.fsh
new file mode 100644
index 0000000..bc9a440
--- /dev/null
+++ b/src/main/resources/assets/firmament/shaders/cape/parallax.fsh
@@ -0,0 +1,53 @@
+#version 150
+
+#moj_import <minecraft:fog.glsl>
+#define M_PI 3.1415926535897932384626433832795
+#define M_TAU (2.0 * M_PI)
+uniform sampler2D Sampler0;
+uniform sampler2D Sampler1;
+uniform sampler2D Sampler3;
+
+uniform vec4 ColorModulator;
+uniform float FogStart;
+uniform float FogEnd;
+uniform vec4 FogColor;
+uniform float Animation;
+
+in float vertexDistance;
+in vec4 vertexColor;
+in vec4 lightMapColor;
+in vec4 overlayColor;
+in vec2 texCoord0;
+
+out vec4 fragColor;
+
+float highlightDistance(vec2 coord, vec2 direction, float time) {
+ vec2 dir = normalize(direction);
+ float projection = dot(coord, dir);
+ float animationTime = sin(projection + time * 13 * M_TAU);
+ if (animationTime < 0.997) {
+ return 0.0;
+ }
+ return animationTime;
+}
+
+void main() {
+ vec4 color = texture(Sampler0, texCoord0);
+ if (color.g > 0.99) {
+ // TODO: maybe this speed in each direction should be a uniform
+ color = texture(Sampler1, texCoord0 + Animation * vec2(3.0, -2.0));
+ }
+
+ vec4 highlightColor = texture(Sampler3, texCoord0);
+ if (highlightColor.a > 0.5) {
+ color = highlightColor;
+ float animationHighlight = highlightDistance(texCoord0, vec2(-12.0, 2.0), Animation);
+ color.rgb += (animationHighlight);
+ }
+ #ifdef ALPHA_CUTOUT
+ if (color.a < ALPHA_CUTOUT) {
+ discard;
+ }
+ #endif
+ fragColor = linear_fog(color, vertexDistance, FogStart, FogEnd, FogColor);
+}
diff --git a/src/main/resources/assets/firmament/shaders/circle_discard_color.fsh b/src/main/resources/assets/firmament/shaders/circle_discard_color.fsh
new file mode 100644
index 0000000..ae46059
--- /dev/null
+++ b/src/main/resources/assets/firmament/shaders/circle_discard_color.fsh
@@ -0,0 +1,22 @@
+#version 150
+
+in vec4 vertexColor;
+in vec2 texCoord0;
+
+uniform vec4 ColorModulator;
+uniform float InnerCutoutRadius;
+
+out vec4 fragColor;
+
+void main() {
+ vec4 color = vertexColor;
+ if (color.a == 0.0) {
+ discard;
+ }
+ float d = length(texCoord0 - vec2(0.5));
+ if (d > 0.5 || d < InnerCutoutRadius)
+ {
+ discard;
+ }
+ fragColor = color * ColorModulator;
+}
diff --git a/src/main/resources/assets/firmament/textures/cape/REUSE.toml b/src/main/resources/assets/firmament/textures/cape/REUSE.toml
new file mode 100644
index 0000000..ba721f7
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/cape/REUSE.toml
@@ -0,0 +1,19 @@
+#SPDX-FileCopyrightText: 2025 Linnea Gräf <nea@nea.moe>
+#
+#SPDX-License-Identifier: CC0-1.0
+version = 1
+
+[[annotations]]
+path = ["firmament_star.png", "parallax_background.png", "parallax_template.png"]
+SPDX-License-Identifier = "CC-BY-4.0"
+SPDX-FileCopyrightText = ["ic22487", "Linnea Gräf"]
+
+[[annotations]]
+path = ["firm_static.png"]
+SPDX-License-Identifier = "CC-BY-4.0"
+SPDX-FileCopyrightText = ["ic22487", "kathund"]
+
+[[annotations]]
+path = ["fsr_static.png"]
+SPDX-License-Identifier = "CC-BY-4.0"
+SPDX-FileCopyrightText = ["Tendan"]
diff --git a/src/main/resources/assets/firmament/textures/cape/firm_static.png b/src/main/resources/assets/firmament/textures/cape/firm_static.png
new file mode 100644
index 0000000..b01511c
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/cape/firm_static.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/cape/firmament_star.png b/src/main/resources/assets/firmament/textures/cape/firmament_star.png
new file mode 100644
index 0000000..520d309
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/cape/firmament_star.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/cape/fsr_static.png b/src/main/resources/assets/firmament/textures/cape/fsr_static.png
new file mode 100644
index 0000000..de9cf35
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/cape/fsr_static.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/cape/parallax_background.png b/src/main/resources/assets/firmament/textures/cape/parallax_background.png
new file mode 100644
index 0000000..05ef0fa
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/cape/parallax_background.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/cape/parallax_template.png b/src/main/resources/assets/firmament/textures/cape/parallax_template.png
new file mode 100644
index 0000000..7084c12
--- /dev/null
+++ b/src/main/resources/assets/firmament/textures/cape/parallax_template.png
Binary files differ
diff --git a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
index 97dd0ea..c897840 100644
--- a/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
+++ b/src/main/resources/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
Binary files differ
diff --git a/src/main/resources/fabric.mod.json b/src/main/resources/fabric.mod.json
index 3b988b1..115778f 100644
--- a/src/main/resources/fabric.mod.json
+++ b/src/main/resources/fabric.mod.json
@@ -40,6 +40,9 @@
"modmenu": [
"moe.nea.firmament.compat.modmenu.FirmamentModMenuPlugin"
],
+ "jade": [
+ "moe.nea.firmament.compat.jade.FirmamentJadePlugin"
+ ],
"jarvis": [
"moe.nea.firmament.jarvis.JarvisIntegration"
]
@@ -48,7 +51,7 @@
"firmament.mixins.json"
],
"depends": {
- "fabric": "*",
+ "fabric-api": ">=${fabric_api_version}",
"fabric-language-kotlin": ">=${fabric_kotlin_version}",
"minecraft": ">=${minecraft_version}"
},
diff --git a/src/main/resources/firmament.accesswidener b/src/main/resources/firmament.accesswidener
index 7418529..71f63ac 100644
--- a/src/main/resources/firmament.accesswidener
+++ b/src/main/resources/firmament.accesswidener
@@ -2,16 +2,17 @@ accessWidener v2 named
accessible class net/minecraft/client/render/RenderLayer$MultiPhase
accessible class net/minecraft/client/render/RenderLayer$MultiPhaseParameters
accessible class net/minecraft/client/font/TextRenderer$Drawer
+
accessible field net/minecraft/client/gui/hud/InGameHud SCOREBOARD_ENTRY_COMPARATOR Ljava/util/Comparator;
+
accessible field net/minecraft/client/network/ClientPlayNetworkHandler combinedDynamicRegistries Lnet/minecraft/registry/DynamicRegistryManager$Immutable;
accessible method net/minecraft/registry/RegistryOps <init> (Lcom/mojang/serialization/DynamicOps;Lnet/minecraft/registry/RegistryOps$RegistryInfoGetter;)V
accessible class net/minecraft/registry/RegistryOps$CachedRegistryInfoGetter
+accessible class net/minecraft/client/render/model/ModelBaker$BakerImpl
+accessible method net/minecraft/client/render/model/ModelBaker$BakerImpl <init> (Lnet/minecraft/client/render/model/ModelBaker;Lnet/minecraft/client/render/model/ErrorCollectingSpriteGetter;)V
accessible field net/minecraft/entity/mob/CreeperEntity CHARGED Lnet/minecraft/entity/data/TrackedData;
accessible method net/minecraft/entity/decoration/ArmorStandEntity setSmall (Z)V
-accessible field net/minecraft/entity/passive/AbstractHorseEntity items Lnet/minecraft/inventory/SimpleInventory;
-accessible field net/minecraft/entity/passive/AbstractHorseEntity SADDLED_FLAG I
-accessible field net/minecraft/entity/passive/AbstractHorseEntity HORSE_FLAGS Lnet/minecraft/entity/data/TrackedData;
accessible method net/minecraft/resource/NamespaceResourceManager loadMetadata (Lnet/minecraft/resource/InputSupplier;)Lnet/minecraft/resource/metadata/ResourceMetadata;
accessible method net/minecraft/client/gui/DrawContext drawTexturedQuad (Ljava/util/function/Function;Lnet/minecraft/util/Identifier;IIIIFFFFI)V
@@ -20,3 +21,11 @@ mutable field net/minecraft/screen/slot/Slot y I
accessible field net/minecraft/entity/player/PlayerEntity PLAYER_MODEL_PARTS Lnet/minecraft/entity/data/TrackedData;
accessible field net/minecraft/client/render/WorldRenderer chunks Lnet/minecraft/client/render/BuiltChunkStorage;
+accessible field net/minecraft/client/render/OverlayTexture texture Lnet/minecraft/client/texture/NativeImageBackedTexture;
+
+accessible method net/minecraft/client/render/RenderPhase$Texture getId ()Ljava/util/Optional;
+accessible field net/minecraft/client/render/RenderLayer$MultiPhase phases Lnet/minecraft/client/render/RenderLayer$MultiPhaseParameters;
+accessible field net/minecraft/client/render/RenderLayer$MultiPhaseParameters texture Lnet/minecraft/client/render/RenderPhase$TextureBase;
+accessible field net/minecraft/client/network/ClientPlayerInteractionManager currentBreakingPos Lnet/minecraft/util/math/BlockPos;
+
+mutable field net/minecraft/client/render/entity/state/LivingEntityRenderState headItemRenderState Lnet/minecraft/client/render/item/ItemRenderState;
diff --git a/src/main/resources/legacy_data/enchantments.json b/src/main/resources/legacy_data/enchantments.json
new file mode 100644
index 0000000..8eeaa6e
--- /dev/null
+++ b/src/main/resources/legacy_data/enchantments.json
@@ -0,0 +1,560 @@
+[
+ {
+ "id": 0,
+ "name": "protection",
+ "displayName": "Protection",
+ "maxLevel": 4,
+ "minCost": {
+ "a": 11,
+ "b": -10
+ },
+ "maxCost": {
+ "a": 11,
+ "b": 1
+ },
+ "exclude": [
+ "blast_protection",
+ "fire_protection",
+ "projectile_protection"
+ ],
+ "category": "armor",
+ "weight": 10,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 1,
+ "name": "fire_protection",
+ "displayName": "Fire Protection",
+ "maxLevel": 4,
+ "minCost": {
+ "a": 8,
+ "b": 2
+ },
+ "maxCost": {
+ "a": 8,
+ "b": 10
+ },
+ "exclude": [
+ "blast_protection",
+ "protection",
+ "projectile_protection"
+ ],
+ "category": "armor",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 2,
+ "name": "feather_falling",
+ "displayName": "Feather Falling",
+ "maxLevel": 4,
+ "minCost": {
+ "a": 6,
+ "b": -1
+ },
+ "maxCost": {
+ "a": 6,
+ "b": 5
+ },
+ "exclude": [],
+ "category": "armor_feet",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 3,
+ "name": "blast_protection",
+ "displayName": "Blast Protection",
+ "maxLevel": 4,
+ "minCost": {
+ "a": 8,
+ "b": -3
+ },
+ "maxCost": {
+ "a": 8,
+ "b": 5
+ },
+ "exclude": [
+ "fire_protection",
+ "protection",
+ "projectile_protection"
+ ],
+ "category": "armor",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 4,
+ "name": "projectile_protection",
+ "displayName": "Projectile Protection",
+ "maxLevel": 4,
+ "minCost": {
+ "a": 6,
+ "b": -3
+ },
+ "maxCost": {
+ "a": 6,
+ "b": 3
+ },
+ "exclude": [
+ "protection",
+ "blast_protection",
+ "fire_protection"
+ ],
+ "category": "armor",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 5,
+ "name": "respiration",
+ "displayName": "Respiration",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 10,
+ "b": 0
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 30
+ },
+ "exclude": [],
+ "category": "armor_head",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 6,
+ "name": "aqua_affinity",
+ "displayName": "Aqua Affinity",
+ "maxLevel": 1,
+ "minCost": {
+ "a": 0,
+ "b": 1
+ },
+ "maxCost": {
+ "a": 0,
+ "b": 41
+ },
+ "exclude": [],
+ "category": "armor_head",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 7,
+ "name": "thorns",
+ "displayName": "Thorns",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 20,
+ "b": -10
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "armor_chest",
+ "weight": 1,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 8,
+ "name": "depth_strider",
+ "displayName": "Depth Strider",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 10,
+ "b": 0
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 15
+ },
+ "exclude": [
+ "frost_walker"
+ ],
+ "category": "armor_feet",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 16,
+ "name": "sharpness",
+ "displayName": "Sharpness",
+ "maxLevel": 5,
+ "minCost": {
+ "a": 11,
+ "b": -10
+ },
+ "maxCost": {
+ "a": 11,
+ "b": 10
+ },
+ "exclude": [
+ "smite",
+ "bane_of_arthropods"
+ ],
+ "category": "weapon",
+ "weight": 10,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 17,
+ "name": "smite",
+ "displayName": "Smite",
+ "maxLevel": 5,
+ "minCost": {
+ "a": 8,
+ "b": -3
+ },
+ "maxCost": {
+ "a": 8,
+ "b": 17
+ },
+ "exclude": [
+ "sharpness",
+ "bane_of_arthropods"
+ ],
+ "category": "weapon",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 18,
+ "name": "bane_of_arthropods",
+ "displayName": "Bane of Arthropods",
+ "maxLevel": 5,
+ "minCost": {
+ "a": 8,
+ "b": -3
+ },
+ "maxCost": {
+ "a": 8,
+ "b": 17
+ },
+ "exclude": [
+ "smite",
+ "sharpness"
+ ],
+ "category": "weapon",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 19,
+ "name": "knockback",
+ "displayName": "Knockback",
+ "maxLevel": 2,
+ "minCost": {
+ "a": 20,
+ "b": -15
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "weapon",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 20,
+ "name": "fire_aspect",
+ "displayName": "Fire Aspect",
+ "maxLevel": 2,
+ "minCost": {
+ "a": 20,
+ "b": -10
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "weapon",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 21,
+ "name": "looting",
+ "displayName": "Looting",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 9,
+ "b": 6
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "weapon",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 32,
+ "name": "efficiency",
+ "displayName": "Efficiency",
+ "maxLevel": 5,
+ "minCost": {
+ "a": 10,
+ "b": -9
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "digger",
+ "weight": 10,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 33,
+ "name": "silk_touch",
+ "displayName": "Silk Touch",
+ "maxLevel": 1,
+ "minCost": {
+ "a": 0,
+ "b": 15
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [
+ "fortune"
+ ],
+ "category": "digger",
+ "weight": 1,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 34,
+ "name": "unbreaking",
+ "displayName": "Unbreaking",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 8,
+ "b": -3
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "breakable",
+ "weight": 5,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 35,
+ "name": "fortune",
+ "displayName": "Fortune",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 9,
+ "b": 6
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [
+ "silk_touch"
+ ],
+ "category": "digger",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 48,
+ "name": "power",
+ "displayName": "Power",
+ "maxLevel": 5,
+ "minCost": {
+ "a": 10,
+ "b": -9
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 6
+ },
+ "exclude": [],
+ "category": "bow",
+ "weight": 10,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 49,
+ "name": "punch",
+ "displayName": "Punch",
+ "maxLevel": 2,
+ "minCost": {
+ "a": 20,
+ "b": -8
+ },
+ "maxCost": {
+ "a": 20,
+ "b": 17
+ },
+ "exclude": [],
+ "category": "bow",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 50,
+ "name": "flame",
+ "displayName": "Flame",
+ "maxLevel": 1,
+ "minCost": {
+ "a": 0,
+ "b": 20
+ },
+ "maxCost": {
+ "a": 0,
+ "b": 50
+ },
+ "exclude": [],
+ "category": "bow",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 51,
+ "name": "infinity",
+ "displayName": "Infinity",
+ "maxLevel": 1,
+ "minCost": {
+ "a": 0,
+ "b": 20
+ },
+ "maxCost": {
+ "a": 0,
+ "b": 50
+ },
+ "exclude": [
+ "mending"
+ ],
+ "category": "bow",
+ "weight": 1,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 61,
+ "name": "luck_of_the_sea",
+ "displayName": "Luck of the Sea",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 9,
+ "b": 6
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "fishing_rod",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ },
+ {
+ "id": 62,
+ "name": "lure",
+ "displayName": "Lure",
+ "maxLevel": 3,
+ "minCost": {
+ "a": 9,
+ "b": 6
+ },
+ "maxCost": {
+ "a": 10,
+ "b": 51
+ },
+ "exclude": [],
+ "category": "fishing_rod",
+ "weight": 2,
+ "treasureOnly": false,
+ "curse": false,
+ "tradeable": true,
+ "discoverable": true
+ }
+]
diff --git a/src/main/resources/legacy_data/items.json b/src/main/resources/legacy_data/items.json
new file mode 100644
index 0000000..a32702c
--- /dev/null
+++ b/src/main/resources/legacy_data/items.json
@@ -0,0 +1,3733 @@
+[
+ {
+ "id": 1,
+ "displayName": "Stone",
+ "name": "stone",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Stone"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Granite"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Polished Granite"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Diorite"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Polished Diorite"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Andesite"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Polished Andesite"
+ }
+ ]
+ },
+ {
+ "id": 2,
+ "displayName": "Grass Block",
+ "name": "grass",
+ "stackSize": 64
+ },
+ {
+ "id": 3,
+ "displayName": "Dirt",
+ "name": "dirt",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Dirt"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Coarse Dirt"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Podzol"
+ }
+ ]
+ },
+ {
+ "id": 4,
+ "displayName": "Cobblestone",
+ "name": "cobblestone",
+ "stackSize": 64
+ },
+ {
+ "id": 5,
+ "displayName": "Wooden Planks",
+ "name": "planks",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Oak Wood Planks"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Spruce Wood Planks"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Birch Wood Planks"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Jungle Wood Planks"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Acacia Wood Planks"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Dark Oak Wood Planks"
+ }
+ ]
+ },
+ {
+ "id": 6,
+ "displayName": "Sapling",
+ "name": "sapling",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Oak Sapling"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Spruce Sapling"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Birch Sapling"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Jungle Sapling"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Acacia Sapling"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Dark Oak Sapling"
+ }
+ ]
+ },
+ {
+ "id": 7,
+ "displayName": "Bedrock",
+ "name": "bedrock",
+ "stackSize": 64
+ },
+ {
+ "id": 12,
+ "displayName": "Sand",
+ "name": "sand",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Sand"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Red Sand"
+ }
+ ]
+ },
+ {
+ "id": 13,
+ "displayName": "Gravel",
+ "name": "gravel",
+ "stackSize": 64
+ },
+ {
+ "id": 14,
+ "displayName": "Gold Ore",
+ "name": "gold_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 15,
+ "displayName": "Iron Ore",
+ "name": "iron_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 16,
+ "displayName": "Coal Ore",
+ "name": "coal_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 17,
+ "displayName": "Wood",
+ "name": "log",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Oak Wood"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Spruce Wood"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Birch Wood"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Jungle Wood"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Acacia Wood"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Dark Oak Wood"
+ }
+ ]
+ },
+ {
+ "id": 18,
+ "displayName": "Leaves",
+ "name": "leaves",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Oak Leaves"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Spruce Leaves"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Birch Leaves"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Jungle Leaves"
+ }
+ ]
+ },
+ {
+ "id": 19,
+ "displayName": "Sponge",
+ "name": "sponge",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Sponge"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Wet Sponge"
+ }
+ ]
+ },
+ {
+ "id": 20,
+ "displayName": "Glass",
+ "name": "glass",
+ "stackSize": 64
+ },
+ {
+ "id": 21,
+ "displayName": "Lapis Lazuli Ore",
+ "name": "lapis_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 22,
+ "displayName": "Lapis Lazuli Block",
+ "name": "lapis_block",
+ "stackSize": 64
+ },
+ {
+ "id": 23,
+ "displayName": "Dispenser",
+ "name": "dispenser",
+ "stackSize": 64
+ },
+ {
+ "id": 24,
+ "displayName": "Sandstone",
+ "name": "sandstone",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Sandstone"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Chiseled Sandstone"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Smooth Sandstone"
+ }
+ ]
+ },
+ {
+ "id": 25,
+ "displayName": "Note Block",
+ "name": "noteblock",
+ "stackSize": 64
+ },
+ {
+ "id": 27,
+ "displayName": "Powered Rail",
+ "name": "golden_rail",
+ "stackSize": 64
+ },
+ {
+ "id": 28,
+ "displayName": "Detector Rail",
+ "name": "detector_rail",
+ "stackSize": 64
+ },
+ {
+ "id": 29,
+ "displayName": "Sticky Piston",
+ "name": "sticky_piston",
+ "stackSize": 64
+ },
+ {
+ "id": 30,
+ "displayName": "Cobweb",
+ "name": "web",
+ "stackSize": 64
+ },
+ {
+ "id": 31,
+ "displayName": "Grass",
+ "name": "tallgrass",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Shrub"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Tall Grass"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Fern"
+ }
+ ]
+ },
+ {
+ "id": 32,
+ "displayName": "Dead Bush",
+ "name": "deadbush",
+ "stackSize": 64
+ },
+ {
+ "id": 33,
+ "displayName": "Piston",
+ "name": "piston",
+ "stackSize": 64
+ },
+ {
+ "id": 35,
+ "displayName": "Wool",
+ "name": "wool",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "White Wool"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Orange Wool"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Magenta Wool"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Light blue Wool"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Yellow Wool"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Lime Wool"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Pink Wool"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Gray Wool"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Light gray Wool"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Cyan Wool"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Purple Wool"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Blue Wool"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Brown Wool"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Green Wool"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Red Wool"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Black Wool"
+ }
+ ]
+ },
+ {
+ "id": 37,
+ "displayName": "Dandelion",
+ "name": "yellow_flower",
+ "stackSize": 64
+ },
+ {
+ "id": 38,
+ "displayName": "Poppy",
+ "name": "red_flower",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Poppy"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Blue Orchid"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Allium"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Azure Bluet"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Red Tulip"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Orange Tulip"
+ },
+ {
+ "metadata": 6,
+ "displayName": "White Tulip"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Pink Tulip"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Oxeye Daisy"
+ }
+ ]
+ },
+ {
+ "id": 39,
+ "displayName": "Brown Mushroom",
+ "name": "brown_mushroom",
+ "stackSize": 64
+ },
+ {
+ "id": 40,
+ "displayName": "Red Mushroom",
+ "name": "red_mushroom",
+ "stackSize": 64
+ },
+ {
+ "id": 41,
+ "displayName": "Block of Gold",
+ "name": "gold_block",
+ "stackSize": 64
+ },
+ {
+ "id": 42,
+ "displayName": "Block of Iron",
+ "name": "iron_block",
+ "stackSize": 64
+ },
+ {
+ "id": 44,
+ "displayName": "Stone Slab",
+ "name": "stone_slab",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Stone Slab"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Sandstone Slab"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Wooden Slab"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Cobblestone Slab"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Bricks Slab"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Stone Bricks Slab"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Nether Brick Slab"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Quartz Slab"
+ }
+ ]
+ },
+ {
+ "id": 45,
+ "displayName": "Brick",
+ "name": "brick_block",
+ "stackSize": 64
+ },
+ {
+ "id": 46,
+ "displayName": "TNT",
+ "name": "tnt",
+ "stackSize": 64
+ },
+ {
+ "id": 47,
+ "displayName": "Bookshelf",
+ "name": "bookshelf",
+ "stackSize": 64
+ },
+ {
+ "id": 48,
+ "displayName": "Moss Stone",
+ "name": "mossy_cobblestone",
+ "stackSize": 64
+ },
+ {
+ "id": 49,
+ "displayName": "Obsidian",
+ "name": "obsidian",
+ "stackSize": 64
+ },
+ {
+ "id": 50,
+ "displayName": "Torch",
+ "name": "torch",
+ "stackSize": 64
+ },
+ {
+ "id": 52,
+ "displayName": "Monster Spawner",
+ "name": "mob_spawner",
+ "stackSize": 64
+ },
+ {
+ "id": 53,
+ "displayName": "Oak Wood Stairs",
+ "name": "oak_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 54,
+ "displayName": "Chest",
+ "name": "chest",
+ "stackSize": 64
+ },
+ {
+ "id": 56,
+ "displayName": "Diamond Ore",
+ "name": "diamond_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 57,
+ "displayName": "Block of Diamond",
+ "name": "diamond_block",
+ "stackSize": 64
+ },
+ {
+ "id": 58,
+ "displayName": "Crafting Table",
+ "name": "crafting_table",
+ "stackSize": 64
+ },
+ {
+ "id": 60,
+ "displayName": "Farmland",
+ "name": "farmland",
+ "stackSize": 64
+ },
+ {
+ "id": 61,
+ "displayName": "Furnace",
+ "name": "furnace",
+ "stackSize": 64
+ },
+ {
+ "id": 65,
+ "displayName": "Ladder",
+ "name": "ladder",
+ "stackSize": 64
+ },
+ {
+ "id": 66,
+ "displayName": "Rail",
+ "name": "rail",
+ "stackSize": 64
+ },
+ {
+ "id": 67,
+ "displayName": "Cobblestone Stairs",
+ "name": "stone_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 69,
+ "displayName": "Lever",
+ "name": "lever",
+ "stackSize": 64
+ },
+ {
+ "id": 70,
+ "displayName": "Stone Pressure Plate",
+ "name": "stone_pressure_plate",
+ "stackSize": 64
+ },
+ {
+ "id": 72,
+ "displayName": "Wooden Pressure Plate",
+ "name": "wooden_pressure_plate",
+ "stackSize": 64
+ },
+ {
+ "id": 73,
+ "displayName": "Redstone Ore",
+ "name": "redstone_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 76,
+ "displayName": "Redstone Torch",
+ "name": "redstone_torch",
+ "stackSize": 64
+ },
+ {
+ "id": 77,
+ "displayName": "Stone Button",
+ "name": "stone_button",
+ "stackSize": 64
+ },
+ {
+ "id": 78,
+ "displayName": "Snow",
+ "name": "snow_layer",
+ "stackSize": 64
+ },
+ {
+ "id": 79,
+ "displayName": "Ice",
+ "name": "ice",
+ "stackSize": 64
+ },
+ {
+ "id": 80,
+ "displayName": "Snow",
+ "name": "snow",
+ "stackSize": 64
+ },
+ {
+ "id": 81,
+ "displayName": "Cactus",
+ "name": "cactus",
+ "stackSize": 64
+ },
+ {
+ "id": 82,
+ "displayName": "Clay",
+ "name": "clay",
+ "stackSize": 64
+ },
+ {
+ "id": 84,
+ "displayName": "Jukebox",
+ "name": "jukebox",
+ "stackSize": 64
+ },
+ {
+ "id": 85,
+ "displayName": "Oak Fence",
+ "name": "fence",
+ "stackSize": 64
+ },
+ {
+ "id": 86,
+ "displayName": "Pumpkin",
+ "name": "pumpkin",
+ "stackSize": 64
+ },
+ {
+ "id": 87,
+ "displayName": "Netherrack",
+ "name": "netherrack",
+ "stackSize": 64
+ },
+ {
+ "id": 88,
+ "displayName": "Soul Sand",
+ "name": "soul_sand",
+ "stackSize": 64
+ },
+ {
+ "id": 89,
+ "displayName": "Glowstone",
+ "name": "glowstone",
+ "stackSize": 64
+ },
+ {
+ "id": 91,
+ "displayName": "Jack o'Lantern",
+ "name": "lit_pumpkin",
+ "stackSize": 64
+ },
+ {
+ "id": 95,
+ "displayName": "Stained Glass",
+ "name": "stained_glass",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "White Stained Glass"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Orange Stained Glass"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Magenta Stained Glass"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Light Blue Stained Glass"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Yellow Stained Glass"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Lime Stained Glass"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Pink Stained Glass"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Gray Stained Glass"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Light Gray Stained Glass"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Cyan Stained Glass"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Purple Stained Glass"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Blue Stained Glass"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Brown Stained Glass"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Green Stained Glass"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Red Stained Glass"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Black Stained Glass"
+ }
+ ]
+ },
+ {
+ "id": 96,
+ "displayName": "Wooden Trapdoor",
+ "name": "trapdoor",
+ "stackSize": 64
+ },
+ {
+ "id": 97,
+ "displayName": "Monster Egg",
+ "name": "monster_egg",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Stone Monster Egg"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Cobblestone Monster Egg"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Stone Brick Monster Egg"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Mossy Stone Brick Monster Egg"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Cracked Stone Brick Monster Egg"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Chiseled Stone Brick Monster Egg"
+ }
+ ]
+ },
+ {
+ "id": 98,
+ "displayName": "Stone Bricks",
+ "name": "stonebrick",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Stone Bricks"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Mossy Stone Bricks"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Cracked Stone Bricks"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Chiseled Stone Bricks"
+ }
+ ]
+ },
+ {
+ "id": 99,
+ "displayName": "Brown Mushroom Block",
+ "name": "brown_mushroom_block",
+ "stackSize": 64
+ },
+ {
+ "id": 100,
+ "displayName": "Red Mushroom Block",
+ "name": "red_mushroom_block",
+ "stackSize": 64
+ },
+ {
+ "id": 101,
+ "displayName": "Iron Bars",
+ "name": "iron_bars",
+ "stackSize": 64
+ },
+ {
+ "id": 102,
+ "displayName": "Glass Pane",
+ "name": "glass_pane",
+ "stackSize": 64
+ },
+ {
+ "id": 103,
+ "displayName": "Melon",
+ "name": "melon_block",
+ "stackSize": 64
+ },
+ {
+ "id": 106,
+ "displayName": "Vines",
+ "name": "vine",
+ "stackSize": 64
+ },
+ {
+ "id": 107,
+ "displayName": "Oak Fence Gate",
+ "name": "fence_gate",
+ "stackSize": 64
+ },
+ {
+ "id": 108,
+ "displayName": "Brick Stairs",
+ "name": "brick_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 109,
+ "displayName": "Stone Brick Stairs",
+ "name": "stone_brick_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 110,
+ "displayName": "Mycelium",
+ "name": "mycelium",
+ "stackSize": 64
+ },
+ {
+ "id": 111,
+ "displayName": "Lily Pad",
+ "name": "waterlily",
+ "stackSize": 64
+ },
+ {
+ "id": 112,
+ "displayName": "Nether Brick",
+ "name": "nether_brick",
+ "stackSize": 64
+ },
+ {
+ "id": 113,
+ "displayName": "Nether Brick Fence",
+ "name": "nether_brick_fence",
+ "stackSize": 64
+ },
+ {
+ "id": 114,
+ "displayName": "Nether Brick Stairs",
+ "name": "nether_brick_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 116,
+ "displayName": "Enchantment Table",
+ "name": "enchanting_table",
+ "stackSize": 64
+ },
+ {
+ "id": 120,
+ "displayName": "End Portal Frame",
+ "name": "end_portal_frame",
+ "stackSize": 64
+ },
+ {
+ "id": 121,
+ "displayName": "End Stone",
+ "name": "end_stone",
+ "stackSize": 64
+ },
+ {
+ "id": 122,
+ "displayName": "Dragon Egg",
+ "name": "dragon_egg",
+ "stackSize": 64
+ },
+ {
+ "id": 123,
+ "displayName": "Redstone Lamp",
+ "name": "redstone_lamp",
+ "stackSize": 64
+ },
+ {
+ "id": 126,
+ "displayName": "Wood Slab",
+ "name": "wooden_slab",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Oak Wood Slab"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Spruce Wood Slab"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Birch Wood Slab"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Jungle Wood Slab"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Acacia Wood Slab"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Dark Oak Wood Slab"
+ }
+ ]
+ },
+ {
+ "id": 128,
+ "displayName": "Sandstone Stairs",
+ "name": "sandstone_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 129,
+ "displayName": "Emerald Ore",
+ "name": "emerald_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 130,
+ "displayName": "Ender Chest",
+ "name": "ender_chest",
+ "stackSize": 64
+ },
+ {
+ "id": 131,
+ "displayName": "Tripwire Hook",
+ "name": "tripwire_hook",
+ "stackSize": 64
+ },
+ {
+ "id": 133,
+ "displayName": "Block of Emerald",
+ "name": "emerald_block",
+ "stackSize": 64
+ },
+ {
+ "id": 134,
+ "displayName": "Spruce Wood Stairs",
+ "name": "spruce_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 135,
+ "displayName": "Birch Wood Stairs",
+ "name": "birch_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 136,
+ "displayName": "Jungle Wood Stairs",
+ "name": "jungle_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 137,
+ "displayName": "Command Block",
+ "name": "command_block",
+ "stackSize": 64
+ },
+ {
+ "id": 138,
+ "displayName": "Beacon",
+ "name": "beacon",
+ "stackSize": 64
+ },
+ {
+ "id": 139,
+ "displayName": "Cobblestone Wall",
+ "name": "cobblestone_wall",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Cobblestone Wall"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Mossy Cobblestone Wall"
+ }
+ ]
+ },
+ {
+ "id": 143,
+ "displayName": "Wooden Button",
+ "name": "wooden_button",
+ "stackSize": 64
+ },
+ {
+ "id": 145,
+ "displayName": "Anvil",
+ "name": "anvil",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Anvil"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Slightly Damaged Anvil"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Very Damaged Anvil"
+ }
+ ]
+ },
+ {
+ "id": 146,
+ "displayName": "Trapped Chest",
+ "name": "trapped_chest",
+ "stackSize": 64
+ },
+ {
+ "id": 147,
+ "displayName": "Weighted Pressure Plate (Light)",
+ "name": "light_weighted_pressure_plate",
+ "stackSize": 64
+ },
+ {
+ "id": 148,
+ "displayName": "Weighted Pressure Plate (Heavy)",
+ "name": "heavy_weighted_pressure_plate",
+ "stackSize": 64
+ },
+ {
+ "id": 151,
+ "displayName": "Daylight Detector",
+ "name": "daylight_detector",
+ "stackSize": 64
+ },
+ {
+ "id": 152,
+ "displayName": "Block of Redstone",
+ "name": "redstone_block",
+ "stackSize": 64
+ },
+ {
+ "id": 153,
+ "displayName": "Nether Quartz",
+ "name": "quartz_ore",
+ "stackSize": 64
+ },
+ {
+ "id": 154,
+ "displayName": "Hopper",
+ "name": "hopper",
+ "stackSize": 64
+ },
+ {
+ "id": 155,
+ "displayName": "Block of Quartz",
+ "name": "quartz_block",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Block of Quartz"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Chiseled Quartz Block"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Pillar Quartz Block"
+ }
+ ]
+ },
+ {
+ "id": 156,
+ "displayName": "Quartz Stairs",
+ "name": "quartz_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 157,
+ "displayName": "Activator Rail",
+ "name": "activator_rail",
+ "stackSize": 64
+ },
+ {
+ "id": 158,
+ "displayName": "Dropper",
+ "name": "dropper",
+ "stackSize": 64
+ },
+ {
+ "id": 159,
+ "displayName": "Stained Clay",
+ "name": "stained_hardened_clay",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "White Stained Clay"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Orange Stained Clay"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Magenta Stained Clay"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Light Blue Stained Clay"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Yellow Stained Clay"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Lime Stained Clay"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Pink Stained Clay"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Gray Stained Clay"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Light Gray Stained Clay"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Cyan Stained Clay"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Purple Stained Clay"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Blue Stained Clay"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Brown Stained Clay"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Green Stained Clay"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Red Stained Clay"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Black Stained Clay"
+ }
+ ]
+ },
+ {
+ "id": 160,
+ "displayName": "Stained Glass Pane",
+ "name": "stained_glass_pane",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "White Stained Glass Pane"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Orange Stained Glass Pane"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Magenta Stained Glass Pane"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Light Blue Stained Glass Pane"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Yellow Stained Glass Pane"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Lime Stained Glass Pane"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Pink Stained Glass Pane"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Gray Stained Glass Pane"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Light Gray Stained Glass Pane"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Cyan Stained Glass Pane"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Purple Stained Glass Pane"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Blue Stained Glass Pane"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Brown Stained Glass Pane"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Green Stained Glass Pane"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Red Stained Glass Pane"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Black Stained Glass Pane"
+ }
+ ]
+ },
+ {
+ "id": 161,
+ "displayName": "Leaves",
+ "name": "leaves2",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Acacia Leaves"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Dark Oak Leaves"
+ }
+ ]
+ },
+ {
+ "id": 162,
+ "displayName": "Wood",
+ "name": "log2",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Acacia Wood"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Dark Oak Wood"
+ }
+ ]
+ },
+ {
+ "id": 163,
+ "displayName": "Acacia Wood Stairs",
+ "name": "acacia_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 164,
+ "displayName": "Dark Oak Wood Stairs",
+ "name": "dark_oak_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 165,
+ "displayName": "Slime Block",
+ "name": "slime",
+ "stackSize": 64
+ },
+ {
+ "id": 166,
+ "displayName": "Barrier",
+ "name": "barrier",
+ "stackSize": 64
+ },
+ {
+ "id": 167,
+ "displayName": "Iron Trapdoor",
+ "name": "iron_trapdoor",
+ "stackSize": 64
+ },
+ {
+ "id": 168,
+ "displayName": "Prismarine",
+ "name": "prismarine",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Prismarine"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Prismarine Bricks"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Dark Prismarine"
+ }
+ ]
+ },
+ {
+ "id": 169,
+ "displayName": "Sea Lantern",
+ "name": "sea_lantern",
+ "stackSize": 64
+ },
+ {
+ "id": 170,
+ "displayName": "Hay Bale",
+ "name": "hay_block",
+ "stackSize": 64
+ },
+ {
+ "id": 171,
+ "displayName": "Carpet",
+ "name": "carpet",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "White Carpet"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Orange Carpet"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Magenta Carpet"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Light Blue Carpet"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Yellow Carpet"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Lime Carpet"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Pink Carpet"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Gray Carpet"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Light Gray Carpet"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Cyan Carpet"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Purple Carpet"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Blue Carpet"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Brown Carpet"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Green Carpet"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Red Carpet"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Black Carpet"
+ }
+ ]
+ },
+ {
+ "id": 172,
+ "displayName": "Hardened Clay",
+ "name": "hardened_clay",
+ "stackSize": 64
+ },
+ {
+ "id": 173,
+ "displayName": "Block of Coal",
+ "name": "coal_block",
+ "stackSize": 64
+ },
+ {
+ "id": 174,
+ "displayName": "Packed Ice",
+ "name": "packed_ice",
+ "stackSize": 64
+ },
+ {
+ "id": 175,
+ "displayName": "Large Flowers",
+ "name": "double_plant",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Sunflower"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Lilac"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Double Tallgrass"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Large Fern"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Rose Bush"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Peony"
+ }
+ ]
+ },
+ {
+ "id": 179,
+ "displayName": "Red Sandstone",
+ "name": "red_sandstone",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Red Sandstone"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Chiseled Red Sandstone"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Smooth Red Sandstone"
+ }
+ ]
+ },
+ {
+ "id": 180,
+ "displayName": "Red Sandstone Stairs",
+ "name": "red_sandstone_stairs",
+ "stackSize": 64
+ },
+ {
+ "id": 182,
+ "displayName": "Red Sandstone Slab",
+ "name": "stone_slab2",
+ "stackSize": 64
+ },
+ {
+ "id": 183,
+ "displayName": "Spruce Fence Gate",
+ "name": "spruce_fence_gate",
+ "stackSize": 64
+ },
+ {
+ "id": 184,
+ "displayName": "Birch Fence Gate",
+ "name": "birch_fence_gate",
+ "stackSize": 64
+ },
+ {
+ "id": 185,
+ "displayName": "Jungle Fence Gate",
+ "name": "jungle_fence_gate",
+ "stackSize": 64
+ },
+ {
+ "id": 186,
+ "displayName": "Dark Oak Fence Gate",
+ "name": "dark_oak_fence_gate",
+ "stackSize": 64
+ },
+ {
+ "id": 187,
+ "displayName": "Acacia Fence Gate",
+ "name": "acacia_fence_gate",
+ "stackSize": 64
+ },
+ {
+ "id": 188,
+ "displayName": "Spruce Fence",
+ "name": "spruce_fence",
+ "stackSize": 64
+ },
+ {
+ "id": 189,
+ "displayName": "Birch Fence",
+ "name": "birch_fence",
+ "stackSize": 64
+ },
+ {
+ "id": 190,
+ "displayName": "Jungle Fence",
+ "name": "jungle_fence",
+ "stackSize": 64
+ },
+ {
+ "id": 191,
+ "displayName": "Dark Oak Fence",
+ "name": "dark_oak_fence",
+ "stackSize": 64
+ },
+ {
+ "id": 192,
+ "displayName": "Acacia Fence",
+ "name": "acacia_fence",
+ "stackSize": 64
+ },
+ {
+ "id": 256,
+ "displayName": "Iron Shovel",
+ "name": "iron_shovel",
+ "stackSize": 1,
+ "maxDurability": 250,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 257,
+ "displayName": "Iron Pickaxe",
+ "name": "iron_pickaxe",
+ "stackSize": 1,
+ "maxDurability": 250,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 258,
+ "displayName": "Iron Axe",
+ "name": "iron_axe",
+ "stackSize": 1,
+ "maxDurability": 250,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 259,
+ "displayName": "Flint and Steel",
+ "name": "flint_and_steel",
+ "stackSize": 1,
+ "maxDurability": 64,
+ "enchantCategories": [
+ "breakable",
+ "vanishable"
+ ]
+ },
+ {
+ "id": 260,
+ "displayName": "Apple",
+ "name": "apple",
+ "stackSize": 64
+ },
+ {
+ "id": 261,
+ "displayName": "Bow",
+ "name": "bow",
+ "stackSize": 1,
+ "maxDurability": 384,
+ "enchantCategories": [
+ "breakable",
+ "bow",
+ "vanishable"
+ ]
+ },
+ {
+ "id": 262,
+ "displayName": "Arrow",
+ "name": "arrow",
+ "stackSize": 64
+ },
+ {
+ "id": 263,
+ "displayName": "Coal",
+ "name": "coal",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Coal"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Charcoal"
+ }
+ ]
+ },
+ {
+ "id": 264,
+ "displayName": "Diamond",
+ "name": "diamond",
+ "stackSize": 64
+ },
+ {
+ "id": 265,
+ "displayName": "Iron Ingot",
+ "name": "iron_ingot",
+ "stackSize": 64
+ },
+ {
+ "id": 266,
+ "displayName": "Gold Ingot",
+ "name": "gold_ingot",
+ "stackSize": 64
+ },
+ {
+ "id": 267,
+ "displayName": "Iron Sword",
+ "name": "iron_sword",
+ "stackSize": 1,
+ "maxDurability": 250,
+ "enchantCategories": [
+ "weapon",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 268,
+ "displayName": "Wooden Sword",
+ "name": "wooden_sword",
+ "stackSize": 1,
+ "maxDurability": 59,
+ "enchantCategories": [
+ "weapon",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "oak_planks",
+ "spruce_planks",
+ "birch_planks",
+ "jungle_planks",
+ "acacia_planks",
+ "dark_oak_planks",
+ "crimson_planks",
+ "warped_planks"
+ ]
+ },
+ {
+ "id": 269,
+ "displayName": "Wooden Shovel",
+ "name": "wooden_shovel",
+ "stackSize": 1,
+ "maxDurability": 59,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "oak_planks",
+ "spruce_planks",
+ "birch_planks",
+ "jungle_planks",
+ "acacia_planks",
+ "dark_oak_planks",
+ "crimson_planks",
+ "warped_planks"
+ ]
+ },
+ {
+ "id": 270,
+ "displayName": "Wooden Pickaxe",
+ "name": "wooden_pickaxe",
+ "stackSize": 1,
+ "maxDurability": 59,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "oak_planks",
+ "spruce_planks",
+ "birch_planks",
+ "jungle_planks",
+ "acacia_planks",
+ "dark_oak_planks",
+ "crimson_planks",
+ "warped_planks"
+ ]
+ },
+ {
+ "id": 271,
+ "displayName": "Wooden Axe",
+ "name": "wooden_axe",
+ "stackSize": 1,
+ "maxDurability": 59,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "oak_planks",
+ "spruce_planks",
+ "birch_planks",
+ "jungle_planks",
+ "acacia_planks",
+ "dark_oak_planks",
+ "crimson_planks",
+ "warped_planks"
+ ]
+ },
+ {
+ "id": 272,
+ "displayName": "Stone Sword",
+ "name": "stone_sword",
+ "stackSize": 1,
+ "maxDurability": 131,
+ "enchantCategories": [
+ "weapon",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "cobblestone",
+ "blackstone"
+ ]
+ },
+ {
+ "id": 273,
+ "displayName": "Stone Shovel",
+ "name": "stone_shovel",
+ "stackSize": 1,
+ "maxDurability": 131,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "cobblestone",
+ "blackstone"
+ ]
+ },
+ {
+ "id": 274,
+ "displayName": "Stone Pickaxe",
+ "name": "stone_pickaxe",
+ "stackSize": 1,
+ "maxDurability": 131,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "cobblestone",
+ "blackstone"
+ ]
+ },
+ {
+ "id": 275,
+ "displayName": "Stone Axe",
+ "name": "stone_axe",
+ "stackSize": 1,
+ "maxDurability": 131,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "cobblestone",
+ "blackstone"
+ ]
+ },
+ {
+ "id": 276,
+ "displayName": "Diamond Sword",
+ "name": "diamond_sword",
+ "stackSize": 1,
+ "maxDurability": 1561,
+ "enchantCategories": [
+ "weapon",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 277,
+ "displayName": "Diamond Shovel",
+ "name": "diamond_shovel",
+ "stackSize": 1,
+ "maxDurability": 1561,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 278,
+ "displayName": "Diamond Pickaxe",
+ "name": "diamond_pickaxe",
+ "stackSize": 1,
+ "maxDurability": 1561,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 279,
+ "displayName": "Diamond Axe",
+ "name": "diamond_axe",
+ "stackSize": 1,
+ "maxDurability": 1561,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 280,
+ "displayName": "Stick",
+ "name": "stick",
+ "stackSize": 64
+ },
+ {
+ "id": 281,
+ "displayName": "Bowl",
+ "name": "bowl",
+ "stackSize": 64
+ },
+ {
+ "id": 282,
+ "displayName": "Mushroom Stew",
+ "name": "mushroom_stew",
+ "stackSize": 1
+ },
+ {
+ "id": 283,
+ "displayName": "Golden Sword",
+ "name": "golden_sword",
+ "stackSize": 1,
+ "maxDurability": 32,
+ "enchantCategories": [
+ "weapon",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 284,
+ "displayName": "Golden Shovel",
+ "name": "golden_shovel",
+ "stackSize": 1,
+ "maxDurability": 32,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 285,
+ "displayName": "Golden Pickaxe",
+ "name": "golden_pickaxe",
+ "stackSize": 1,
+ "maxDurability": 32,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 286,
+ "displayName": "Golden Axe",
+ "name": "golden_axe",
+ "stackSize": 1,
+ "maxDurability": 32,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 287,
+ "displayName": "String",
+ "name": "string",
+ "stackSize": 64
+ },
+ {
+ "id": 288,
+ "displayName": "Feather",
+ "name": "feather",
+ "stackSize": 64
+ },
+ {
+ "id": 289,
+ "displayName": "Gunpowder",
+ "name": "gunpowder",
+ "stackSize": 64
+ },
+ {
+ "id": 290,
+ "displayName": "Wooden Hoe",
+ "name": "wooden_hoe",
+ "stackSize": 1,
+ "maxDurability": 59,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "oak_planks",
+ "spruce_planks",
+ "birch_planks",
+ "jungle_planks",
+ "acacia_planks",
+ "dark_oak_planks",
+ "crimson_planks",
+ "warped_planks"
+ ]
+ },
+ {
+ "id": 291,
+ "displayName": "Stone Hoe",
+ "name": "stone_hoe",
+ "stackSize": 1,
+ "maxDurability": 131,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "cobblestone",
+ "blackstone"
+ ]
+ },
+ {
+ "id": 292,
+ "displayName": "Iron Hoe",
+ "name": "iron_hoe",
+ "stackSize": 1,
+ "maxDurability": 250,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 293,
+ "displayName": "Diamond Hoe",
+ "name": "diamond_hoe",
+ "stackSize": 1,
+ "maxDurability": 1561,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 294,
+ "displayName": "Golden Hoe",
+ "name": "golden_hoe",
+ "stackSize": 1,
+ "maxDurability": 32,
+ "enchantCategories": [
+ "digger",
+ "breakable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 295,
+ "displayName": "Seeds",
+ "name": "wheat_seeds",
+ "stackSize": 64
+ },
+ {
+ "id": 296,
+ "displayName": "Wheat",
+ "name": "wheat",
+ "stackSize": 64
+ },
+ {
+ "id": 297,
+ "displayName": "Bread",
+ "name": "bread",
+ "stackSize": 64
+ },
+ {
+ "id": 298,
+ "displayName": "Leather Cap",
+ "name": "leather_helmet",
+ "stackSize": 1,
+ "maxDurability": 55,
+ "enchantCategories": [
+ "armor",
+ "armor_head",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "leather"
+ ]
+ },
+ {
+ "id": 299,
+ "displayName": "Leather Tunic",
+ "name": "leather_chestplate",
+ "stackSize": 1,
+ "maxDurability": 80,
+ "enchantCategories": [
+ "armor",
+ "armor_chest",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "leather"
+ ]
+ },
+ {
+ "id": 300,
+ "displayName": "Leather Pants",
+ "name": "leather_leggings",
+ "stackSize": 1,
+ "maxDurability": 75,
+ "enchantCategories": [
+ "armor",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "leather"
+ ]
+ },
+ {
+ "id": 301,
+ "displayName": "Leather Boots",
+ "name": "leather_boots",
+ "stackSize": 1,
+ "maxDurability": 65,
+ "enchantCategories": [
+ "armor",
+ "armor_feet",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "leather"
+ ]
+ },
+ {
+ "id": 302,
+ "displayName": "Chain Helmet",
+ "name": "chainmail_helmet",
+ "stackSize": 1,
+ "maxDurability": 165,
+ "enchantCategories": [
+ "armor",
+ "armor_head",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 303,
+ "displayName": "Chain Chestplate",
+ "name": "chainmail_chestplate",
+ "stackSize": 1,
+ "maxDurability": 240,
+ "enchantCategories": [
+ "armor",
+ "armor_chest",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 304,
+ "displayName": "Chain Leggings",
+ "name": "chainmail_leggings",
+ "stackSize": 1,
+ "maxDurability": 225,
+ "enchantCategories": [
+ "armor",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 305,
+ "displayName": "Chain Boots",
+ "name": "chainmail_boots",
+ "stackSize": 1,
+ "maxDurability": 195,
+ "enchantCategories": [
+ "armor",
+ "armor_feet",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 306,
+ "displayName": "Iron Helmet",
+ "name": "iron_helmet",
+ "stackSize": 1,
+ "maxDurability": 165,
+ "enchantCategories": [
+ "armor",
+ "armor_head",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 307,
+ "displayName": "Iron Chestplate",
+ "name": "iron_chestplate",
+ "stackSize": 1,
+ "maxDurability": 240,
+ "enchantCategories": [
+ "armor",
+ "armor_chest",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 308,
+ "displayName": "Iron Leggings",
+ "name": "iron_leggings",
+ "stackSize": 1,
+ "maxDurability": 225,
+ "enchantCategories": [
+ "armor",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 309,
+ "displayName": "Iron Boots",
+ "name": "iron_boots",
+ "stackSize": 1,
+ "maxDurability": 195,
+ "enchantCategories": [
+ "armor",
+ "armor_feet",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "iron_ingot"
+ ]
+ },
+ {
+ "id": 310,
+ "displayName": "Diamond Helmet",
+ "name": "diamond_helmet",
+ "stackSize": 1,
+ "maxDurability": 363,
+ "enchantCategories": [
+ "armor",
+ "armor_head",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 311,
+ "displayName": "Diamond Chestplate",
+ "name": "diamond_chestplate",
+ "stackSize": 1,
+ "maxDurability": 528,
+ "enchantCategories": [
+ "armor",
+ "armor_chest",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 312,
+ "displayName": "Diamond Leggings",
+ "name": "diamond_leggings",
+ "stackSize": 1,
+ "maxDurability": 495,
+ "enchantCategories": [
+ "armor",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 313,
+ "displayName": "Diamond Boots",
+ "name": "diamond_boots",
+ "stackSize": 1,
+ "maxDurability": 429,
+ "enchantCategories": [
+ "armor",
+ "armor_feet",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "diamond"
+ ]
+ },
+ {
+ "id": 314,
+ "displayName": "Golden Helmet",
+ "name": "golden_helmet",
+ "stackSize": 1,
+ "maxDurability": 77,
+ "enchantCategories": [
+ "armor",
+ "armor_head",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 315,
+ "displayName": "Golden Chestplate",
+ "name": "golden_chestplate",
+ "stackSize": 1,
+ "maxDurability": 112,
+ "enchantCategories": [
+ "armor",
+ "armor_chest",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 316,
+ "displayName": "Golden Leggings",
+ "name": "golden_leggings",
+ "stackSize": 1,
+ "maxDurability": 105,
+ "enchantCategories": [
+ "armor",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 317,
+ "displayName": "Golden Boots",
+ "name": "golden_boots",
+ "stackSize": 1,
+ "maxDurability": 91,
+ "enchantCategories": [
+ "armor",
+ "armor_feet",
+ "breakable",
+ "wearable",
+ "vanishable"
+ ],
+ "repairWith": [
+ "gold_ingot"
+ ]
+ },
+ {
+ "id": 318,
+ "displayName": "Flint",
+ "name": "flint",
+ "stackSize": 64
+ },
+ {
+ "id": 319,
+ "displayName": "Raw Porkchop",
+ "name": "porkchop",
+ "stackSize": 64
+ },
+ {
+ "id": 320,
+ "displayName": "Cooked Porkchop",
+ "name": "cooked_porkchop",
+ "stackSize": 64
+ },
+ {
+ "id": 321,
+ "displayName": "Painting",
+ "name": "painting",
+ "stackSize": 64
+ },
+ {
+ "id": 322,
+ "displayName": "Golden Apple",
+ "name": "golden_apple",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Golden Apple"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Enchanted Golden Apple"
+ }
+ ]
+ },
+ {
+ "id": 323,
+ "displayName": "Sign",
+ "name": "sign",
+ "stackSize": 16
+ },
+ {
+ "id": 324,
+ "displayName": "Oak Door",
+ "name": "wooden_door",
+ "stackSize": 64
+ },
+ {
+ "id": 325,
+ "displayName": "Bucket",
+ "name": "bucket",
+ "stackSize": 16
+ },
+ {
+ "id": 326,
+ "displayName": "Water Bucket",
+ "name": "water_bucket",
+ "stackSize": 1
+ },
+ {
+ "id": 327,
+ "displayName": "Lava Bucket",
+ "name": "lava_bucket",
+ "stackSize": 1
+ },
+ {
+ "id": 328,
+ "displayName": "Minecart",
+ "name": "minecart",
+ "stackSize": 1
+ },
+ {
+ "id": 329,
+ "displayName": "Saddle",
+ "name": "saddle",
+ "stackSize": 1
+ },
+ {
+ "id": 330,
+ "displayName": "Iron Door",
+ "name": "iron_door",
+ "stackSize": 64
+ },
+ {
+ "id": 331,
+ "displayName": "Redstone",
+ "name": "redstone",
+ "stackSize": 64
+ },
+ {
+ "id": 332,
+ "displayName": "Snowball",
+ "name": "snowball",
+ "stackSize": 16
+ },
+ {
+ "id": 333,
+ "displayName": "Boat",
+ "name": "boat",
+ "stackSize": 1
+ },
+ {
+ "id": 334,
+ "displayName": "Leather",
+ "name": "leather",
+ "stackSize": 64
+ },
+ {
+ "id": 335,
+ "displayName": "Milk",
+ "name": "milk_bucket",
+ "stackSize": 1
+ },
+ {
+ "id": 336,
+ "displayName": "Brick",
+ "name": "brick",
+ "stackSize": 64
+ },
+ {
+ "id": 337,
+ "displayName": "Clay",
+ "name": "clay_ball",
+ "stackSize": 64
+ },
+ {
+ "id": 338,
+ "displayName": "Sugar Canes",
+ "name": "reeds",
+ "stackSize": 64
+ },
+ {
+ "id": 339,
+ "displayName": "Paper",
+ "name": "paper",
+ "stackSize": 64
+ },
+ {
+ "id": 340,
+ "displayName": "Book",
+ "name": "book",
+ "stackSize": 64
+ },
+ {
+ "id": 341,
+ "displayName": "Slimeball",
+ "name": "slime_ball",
+ "stackSize": 64
+ },
+ {
+ "id": 342,
+ "displayName": "Minecart with Chest",
+ "name": "chest_minecart",
+ "stackSize": 1
+ },
+ {
+ "id": 343,
+ "displayName": "Minecart with Furnace",
+ "name": "furnace_minecart",
+ "stackSize": 1
+ },
+ {
+ "id": 344,
+ "displayName": "Egg",
+ "name": "egg",
+ "stackSize": 16
+ },
+ {
+ "id": 345,
+ "displayName": "Compass",
+ "name": "compass",
+ "stackSize": 64
+ },
+ {
+ "id": 346,
+ "displayName": "Fishing Rod",
+ "name": "fishing_rod",
+ "stackSize": 1,
+ "maxDurability": 64,
+ "enchantCategories": [
+ "breakable",
+ "fishing_rod",
+ "vanishable"
+ ]
+ },
+ {
+ "id": 347,
+ "displayName": "Clock",
+ "name": "clock",
+ "stackSize": 64
+ },
+ {
+ "id": 348,
+ "displayName": "Glowstone Dust",
+ "name": "glowstone_dust",
+ "stackSize": 64
+ },
+ {
+ "id": 349,
+ "displayName": "Fish",
+ "name": "fish",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Raw Fish"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Raw Salmon"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Clownfish"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Pufferfish"
+ }
+ ]
+ },
+ {
+ "id": 350,
+ "displayName": "Cooked Fish",
+ "name": "cooked_fish",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Cooked Fish"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Cooked Salmon"
+ }
+ ]
+ },
+ {
+ "id": 351,
+ "displayName": "Dye",
+ "name": "dye",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Ink Sac"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Rose Red"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Cactus Green"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Cocoa Beans"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Lapis Lazuli"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Purple Dye"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Cyan Dye"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Light Gray Dye"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Gray Dye"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Pink Dye"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Lime Dye"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Dandelion Yellow"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Light Blue Dye"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Magenta Dye"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Orange Dye"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Bone Meal"
+ }
+ ]
+ },
+ {
+ "id": 352,
+ "displayName": "Bone",
+ "name": "bone",
+ "stackSize": 64
+ },
+ {
+ "id": 353,
+ "displayName": "Sugar",
+ "name": "sugar",
+ "stackSize": 64
+ },
+ {
+ "id": 354,
+ "displayName": "Cake",
+ "name": "cake",
+ "stackSize": 1
+ },
+ {
+ "id": 355,
+ "displayName": "Bed",
+ "name": "bed",
+ "stackSize": 1
+ },
+ {
+ "id": 356,
+ "displayName": "Redstone Repeater",
+ "name": "repeater",
+ "stackSize": 64
+ },
+ {
+ "id": 357,
+ "displayName": "Cookie",
+ "name": "cookie",
+ "stackSize": 64
+ },
+ {
+ "id": 358,
+ "displayName": "Map",
+ "name": "filled_map",
+ "stackSize": 64
+ },
+ {
+ "id": 359,
+ "displayName": "Shears",
+ "name": "shears",
+ "stackSize": 1,
+ "maxDurability": 238,
+ "enchantCategories": [
+ "breakable",
+ "vanishable"
+ ]
+ },
+ {
+ "id": 360,
+ "displayName": "Melon",
+ "name": "melon",
+ "stackSize": 64
+ },
+ {
+ "id": 361,
+ "displayName": "Pumpkin Seeds",
+ "name": "pumpkin_seeds",
+ "stackSize": 64
+ },
+ {
+ "id": 362,
+ "displayName": "Melon Seeds",
+ "name": "melon_seeds",
+ "stackSize": 64
+ },
+ {
+ "id": 363,
+ "displayName": "Raw Beef",
+ "name": "beef",
+ "stackSize": 64
+ },
+ {
+ "id": 364,
+ "displayName": "Steak",
+ "name": "cooked_beef",
+ "stackSize": 64
+ },
+ {
+ "id": 365,
+ "displayName": "Raw Chicken",
+ "name": "chicken",
+ "stackSize": 64
+ },
+ {
+ "id": 366,
+ "displayName": "Cooked Chicken",
+ "name": "cooked_chicken",
+ "stackSize": 64
+ },
+ {
+ "id": 367,
+ "displayName": "Rotten Flesh",
+ "name": "rotten_flesh",
+ "stackSize": 64
+ },
+ {
+ "id": 368,
+ "displayName": "Ender Pearl",
+ "name": "ender_pearl",
+ "stackSize": 16
+ },
+ {
+ "id": 369,
+ "displayName": "Blaze Rod",
+ "name": "blaze_rod",
+ "stackSize": 64
+ },
+ {
+ "id": 370,
+ "displayName": "Ghast Tear",
+ "name": "ghast_tear",
+ "stackSize": 64
+ },
+ {
+ "id": 371,
+ "displayName": "Gold Nugget",
+ "name": "gold_nugget",
+ "stackSize": 64
+ },
+ {
+ "id": 372,
+ "displayName": "Nether Wart",
+ "name": "nether_wart",
+ "stackSize": 64
+ },
+ {
+ "id": 373,
+ "displayName": "Potion",
+ "name": "potion",
+ "stackSize": 1
+ },
+ {
+ "id": 374,
+ "displayName": "Glass Bottle",
+ "name": "glass_bottle",
+ "stackSize": 64
+ },
+ {
+ "id": 375,
+ "displayName": "Spider Eye",
+ "name": "spider_eye",
+ "stackSize": 64
+ },
+ {
+ "id": 376,
+ "displayName": "Fermented Spider Eye",
+ "name": "fermented_spider_eye",
+ "stackSize": 64
+ },
+ {
+ "id": 377,
+ "displayName": "Blaze Powder",
+ "name": "blaze_powder",
+ "stackSize": 64
+ },
+ {
+ "id": 378,
+ "displayName": "Magma Cream",
+ "name": "magma_cream",
+ "stackSize": 64
+ },
+ {
+ "id": 379,
+ "displayName": "Brewing Stand",
+ "name": "brewing_stand",
+ "stackSize": 64
+ },
+ {
+ "id": 380,
+ "displayName": "Cauldron",
+ "name": "cauldron",
+ "stackSize": 64
+ },
+ {
+ "id": 381,
+ "displayName": "Eye of Ender",
+ "name": "ender_eye",
+ "stackSize": 64
+ },
+ {
+ "id": 382,
+ "displayName": "Glistering Melon",
+ "name": "speckled_melon",
+ "stackSize": 64
+ },
+ {
+ "id": 383,
+ "displayName": "Spawn Egg",
+ "name": "spawn_egg",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Spawn"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Spawn Dropped item"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Spawn Thrown egg"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Spawn Lead knot"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Spawn Shot arrow"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Spawn Thrown snowball"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Spawn Ghast fireball"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Spawn Blaze fireball"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Spawn Thrown Ender Pearl"
+ },
+ {
+ "metadata": 15,
+ "displayName": "Spawn Thrown Eye of Ender"
+ },
+ {
+ "metadata": 16,
+ "displayName": "Spawn Thrown splash potion"
+ },
+ {
+ "metadata": 17,
+ "displayName": "Spawn Thrown Bottle o' Enchanting"
+ },
+ {
+ "metadata": 18,
+ "displayName": "Spawn Item Frame"
+ },
+ {
+ "metadata": 19,
+ "displayName": "Spawn Wither Skull"
+ },
+ {
+ "metadata": 20,
+ "displayName": "Spawn Primed TNT"
+ },
+ {
+ "metadata": 21,
+ "displayName": "Spawn Falling block"
+ },
+ {
+ "metadata": 21,
+ "displayName": "Spawn Falling block"
+ },
+ {
+ "metadata": 22,
+ "displayName": "Spawn Firework Rocket"
+ },
+ {
+ "metadata": 30,
+ "displayName": "Spawn Armor Stand"
+ },
+ {
+ "metadata": 41,
+ "displayName": "Spawn Boat"
+ },
+ {
+ "metadata": 42,
+ "displayName": "Spawn Minecart"
+ },
+ {
+ "metadata": 42,
+ "displayName": "Spawn Minecart"
+ },
+ {
+ "metadata": 42,
+ "displayName": "Spawn Minecart"
+ },
+ {
+ "metadata": 48,
+ "displayName": "Spawn Mob"
+ },
+ {
+ "metadata": 49,
+ "displayName": "Spawn Monster"
+ },
+ {
+ "metadata": 50,
+ "displayName": "Spawn Creeper"
+ },
+ {
+ "metadata": 51,
+ "displayName": "Spawn Skeleton"
+ },
+ {
+ "metadata": 52,
+ "displayName": "Spawn Spider"
+ },
+ {
+ "metadata": 53,
+ "displayName": "Spawn Giant"
+ },
+ {
+ "metadata": 54,
+ "displayName": "Spawn Zombie"
+ },
+ {
+ "metadata": 55,
+ "displayName": "Spawn Slime"
+ },
+ {
+ "metadata": 56,
+ "displayName": "Spawn Ghast"
+ },
+ {
+ "metadata": 57,
+ "displayName": "Spawn Zombie Pigman"
+ },
+ {
+ "metadata": 58,
+ "displayName": "Spawn Enderman"
+ },
+ {
+ "metadata": 59,
+ "displayName": "Spawn Cave Spider"
+ },
+ {
+ "metadata": 60,
+ "displayName": "Spawn Silverfish"
+ },
+ {
+ "metadata": 61,
+ "displayName": "Spawn Blaze"
+ },
+ {
+ "metadata": 62,
+ "displayName": "Spawn Magma Cube"
+ },
+ {
+ "metadata": 63,
+ "displayName": "Spawn Ender Dragon"
+ },
+ {
+ "metadata": 64,
+ "displayName": "Spawn Wither"
+ },
+ {
+ "metadata": 65,
+ "displayName": "Spawn Bat"
+ },
+ {
+ "metadata": 66,
+ "displayName": "Spawn Witch"
+ },
+ {
+ "metadata": 67,
+ "displayName": "Spawn Endermite"
+ },
+ {
+ "metadata": 68,
+ "displayName": "Spawn Guardian"
+ },
+ {
+ "metadata": 90,
+ "displayName": "Spawn Pig"
+ },
+ {
+ "metadata": 91,
+ "displayName": "Spawn Sheep"
+ },
+ {
+ "metadata": 92,
+ "displayName": "Spawn Cow"
+ },
+ {
+ "metadata": 93,
+ "displayName": "Spawn Chicken"
+ },
+ {
+ "metadata": 94,
+ "displayName": "Spawn Squid"
+ },
+ {
+ "metadata": 95,
+ "displayName": "Spawn Wolf"
+ },
+ {
+ "metadata": 96,
+ "displayName": "Spawn Mooshroom"
+ },
+ {
+ "metadata": 97,
+ "displayName": "Spawn Snow Golem"
+ },
+ {
+ "metadata": 98,
+ "displayName": "Spawn Ocelot"
+ },
+ {
+ "metadata": 99,
+ "displayName": "Spawn Iron Golem"
+ },
+ {
+ "metadata": 100,
+ "displayName": "Spawn Horse"
+ },
+ {
+ "metadata": 101,
+ "displayName": "Spawn Rabbit"
+ },
+ {
+ "metadata": 120,
+ "displayName": "Spawn Villager"
+ },
+ {
+ "metadata": 200,
+ "displayName": "Spawn Ender Crystal"
+ }
+ ]
+ },
+ {
+ "id": 384,
+ "displayName": "Bottle o' Enchanting",
+ "name": "experience_bottle",
+ "stackSize": 64
+ },
+ {
+ "id": 385,
+ "displayName": "Fire Charge",
+ "name": "fire_charge",
+ "stackSize": 64
+ },
+ {
+ "id": 386,
+ "displayName": "Book and Quill",
+ "name": "writable_book",
+ "stackSize": 1
+ },
+ {
+ "id": 387,
+ "displayName": "Written Book",
+ "name": "written_book",
+ "stackSize": 16
+ },
+ {
+ "id": 388,
+ "displayName": "Emerald",
+ "name": "emerald",
+ "stackSize": 64
+ },
+ {
+ "id": 389,
+ "displayName": "Item Frame",
+ "name": "item_frame",
+ "stackSize": 64
+ },
+ {
+ "id": 390,
+ "displayName": "Flower Pot",
+ "name": "flower_pot",
+ "stackSize": 64
+ },
+ {
+ "id": 391,
+ "displayName": "Carrot",
+ "name": "carrot",
+ "stackSize": 64
+ },
+ {
+ "id": 392,
+ "displayName": "Potato",
+ "name": "potato",
+ "stackSize": 64
+ },
+ {
+ "id": 393,
+ "displayName": "Baked Potato",
+ "name": "baked_potato",
+ "stackSize": 64
+ },
+ {
+ "id": 394,
+ "displayName": "Poisonous Potato",
+ "name": "poisonous_potato",
+ "stackSize": 64
+ },
+ {
+ "id": 395,
+ "displayName": "Empty Map",
+ "name": "map",
+ "stackSize": 64
+ },
+ {
+ "id": 396,
+ "displayName": "Golden Carrot",
+ "name": "golden_carrot",
+ "stackSize": 64
+ },
+ {
+ "id": 397,
+ "displayName": "Skull",
+ "name": "skull",
+ "stackSize": 64,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Skeleton Skull"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Wither Skeleton Skull"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Zombie Head"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Head"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Creeper Head"
+ }
+ ]
+ },
+ {
+ "id": 398,
+ "displayName": "Carrot on a Stick",
+ "name": "carrot_on_a_stick",
+ "stackSize": 1,
+ "maxDurability": 25,
+ "enchantCategories": [
+ "breakable",
+ "vanishable"
+ ]
+ },
+ {
+ "id": 399,
+ "displayName": "Nether Star",
+ "name": "nether_star",
+ "stackSize": 64
+ },
+ {
+ "id": 400,
+ "displayName": "Pumpkin Pie",
+ "name": "pumpkin_pie",
+ "stackSize": 64
+ },
+ {
+ "id": 401,
+ "displayName": "Firework Rocket",
+ "name": "fireworks",
+ "stackSize": 64
+ },
+ {
+ "id": 402,
+ "displayName": "Firework Star",
+ "name": "firework_charge",
+ "stackSize": 64
+ },
+ {
+ "id": 403,
+ "displayName": "Enchanted Book",
+ "name": "enchanted_book",
+ "stackSize": 1
+ },
+ {
+ "id": 404,
+ "displayName": "Redstone Comparator",
+ "name": "comparator",
+ "stackSize": 64
+ },
+ {
+ "id": 405,
+ "displayName": "Nether Brick",
+ "name": "netherbrick",
+ "stackSize": 64
+ },
+ {
+ "id": 406,
+ "displayName": "Nether Quartz",
+ "name": "quartz",
+ "stackSize": 64
+ },
+ {
+ "id": 407,
+ "displayName": "Minecart with TNT",
+ "name": "tnt_minecart",
+ "stackSize": 1
+ },
+ {
+ "id": 408,
+ "displayName": "Minecart with Hopper",
+ "name": "hopper_minecart",
+ "stackSize": 1
+ },
+ {
+ "id": 409,
+ "displayName": "Prismarine Shard",
+ "name": "prismarine_shard",
+ "stackSize": 64
+ },
+ {
+ "id": 410,
+ "displayName": "Prismarine Crystals",
+ "name": "prismarine_crystals",
+ "stackSize": 64
+ },
+ {
+ "id": 411,
+ "displayName": "Raw Rabbit",
+ "name": "rabbit",
+ "stackSize": 64
+ },
+ {
+ "id": 412,
+ "displayName": "Cooked Rabbit",
+ "name": "cooked_rabbit",
+ "stackSize": 64
+ },
+ {
+ "id": 413,
+ "displayName": "Rabbit Stew",
+ "name": "rabbit_stew",
+ "stackSize": 1
+ },
+ {
+ "id": 414,
+ "displayName": "Rabbit's Foot",
+ "name": "rabbit_foot",
+ "stackSize": 64
+ },
+ {
+ "id": 415,
+ "displayName": "Rabbit Hide",
+ "name": "rabbit_hide",
+ "stackSize": 64
+ },
+ {
+ "id": 416,
+ "displayName": "Armor Stand",
+ "name": "armor_stand",
+ "stackSize": 16
+ },
+ {
+ "id": 417,
+ "displayName": "Iron Horse Armor",
+ "name": "iron_horse_armor",
+ "stackSize": 1
+ },
+ {
+ "id": 418,
+ "displayName": "Gold Horse Armor",
+ "name": "golden_horse_armor",
+ "stackSize": 1
+ },
+ {
+ "id": 419,
+ "displayName": "Diamond Horse Armor",
+ "name": "diamond_horse_armor",
+ "stackSize": 1
+ },
+ {
+ "id": 420,
+ "displayName": "Lead",
+ "name": "lead",
+ "stackSize": 64
+ },
+ {
+ "id": 421,
+ "displayName": "Name Tag",
+ "name": "name_tag",
+ "stackSize": 64
+ },
+ {
+ "id": 422,
+ "displayName": "Minecart with Command Block",
+ "name": "command_block_minecart",
+ "stackSize": 1
+ },
+ {
+ "id": 423,
+ "displayName": "Raw Mutton",
+ "name": "mutton",
+ "stackSize": 64
+ },
+ {
+ "id": 424,
+ "displayName": "Cooked Mutton",
+ "name": "cooked_mutton",
+ "stackSize": 64
+ },
+ {
+ "id": 425,
+ "displayName": "Banner",
+ "name": "banner",
+ "stackSize": 16,
+ "variations": [
+ {
+ "metadata": 0,
+ "displayName": "Black Banner"
+ },
+ {
+ "metadata": 1,
+ "displayName": "Red Banner"
+ },
+ {
+ "metadata": 2,
+ "displayName": "Green Banner"
+ },
+ {
+ "metadata": 3,
+ "displayName": "Brown Banner"
+ },
+ {
+ "metadata": 4,
+ "displayName": "Blue Banner"
+ },
+ {
+ "metadata": 5,
+ "displayName": "Purple Banner"
+ },
+ {
+ "metadata": 6,
+ "displayName": "Cyan Banner"
+ },
+ {
+ "metadata": 7,
+ "displayName": "Light Gray Banner"
+ },
+ {
+ "metadata": 8,
+ "displayName": "Gray Banner"
+ },
+ {
+ "metadata": 9,
+ "displayName": "Pink Banner"
+ },
+ {
+ "metadata": 10,
+ "displayName": "Lime Banner"
+ },
+ {
+ "metadata": 11,
+ "displayName": "Yellow Banner"
+ },
+ {
+ "metadata": 12,
+ "displayName": "Light Blue Banner"
+ },
+ {
+ "metadata": 13,
+ "displayName": "Magenta Banner"
+ },
+ {
+ "metadata": 14,
+ "displayName": "Orange Banner"
+ },
+ {
+ "metadata": 15,
+ "displayName": "White Banner"
+ }
+ ]
+ },
+ {
+ "id": 427,
+ "displayName": "Spruce Door",
+ "name": "spruce_door",
+ "stackSize": 64
+ },
+ {
+ "id": 428,
+ "displayName": "Birch Door",
+ "name": "birch_door",
+ "stackSize": 64
+ },
+ {
+ "id": 429,
+ "displayName": "Jungle Door",
+ "name": "jungle_door",
+ "stackSize": 64
+ },
+ {
+ "id": 430,
+ "displayName": "Acacia Door",
+ "name": "acacia_door",
+ "stackSize": 64
+ },
+ {
+ "id": 431,
+ "displayName": "Dark Oak Door",
+ "name": "dark_oak_door",
+ "stackSize": 64
+ },
+ {
+ "id": 2256,
+ "displayName": "13 Disc",
+ "name": "record_13",
+ "stackSize": 1
+ },
+ {
+ "id": 2257,
+ "displayName": "Cat Disc",
+ "name": "record_cat",
+ "stackSize": 1
+ },
+ {
+ "id": 2258,
+ "displayName": "Blocks Disc",
+ "name": "record_blocks",
+ "stackSize": 1
+ },
+ {
+ "id": 2259,
+ "displayName": "Chirp Disc",
+ "name": "record_chirp",
+ "stackSize": 1
+ },
+ {
+ "id": 2260,
+ "displayName": "Far Disc",
+ "name": "record_far",
+ "stackSize": 1
+ },
+ {
+ "id": 2261,
+ "displayName": "Mall Disc",
+ "name": "record_mall",
+ "stackSize": 1
+ },
+ {
+ "id": 2262,
+ "displayName": "Mellohi Disc",
+ "name": "record_mellohi",
+ "stackSize": 1
+ },
+ {
+ "id": 2263,
+ "displayName": "Stal Disc",
+ "name": "record_stal",
+ "stackSize": 1
+ },
+ {
+ "id": 2264,
+ "displayName": "Strad Disc",
+ "name": "record_strad",
+ "stackSize": 1
+ },
+ {
+ "id": 2265,
+ "displayName": "Ward Disc",
+ "name": "record_ward",
+ "stackSize": 1
+ },
+ {
+ "id": 2266,
+ "displayName": "11 Disc",
+ "name": "record_11",
+ "stackSize": 1
+ },
+ {
+ "id": 2267,
+ "displayName": "Wait Disc",
+ "name": "record_wait",
+ "stackSize": 1
+ }
+] \ No newline at end of file
diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.png
new file mode 100644
index 0000000..46c86f4
--- /dev/null
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/inventory_button_background.png
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png
new file mode 100644
index 0000000..1831ef3
--- /dev/null
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png
new file mode 100644
index 0000000..5b774b2
--- /dev/null
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta
new file mode 100644
index 0000000..94b9a1d
--- /dev/null
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta
@@ -0,0 +1,10 @@
+{
+ "gui": {
+ "scaling": {
+ "type": "nine_slice",
+ "width": 17,
+ "height": 18,
+ "border": 2
+ }
+ }
+}
diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
new file mode 100644
index 0000000..10d41dd
--- /dev/null
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta
new file mode 100644
index 0000000..5964a6f
--- /dev/null
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta
@@ -0,0 +1,10 @@
+{
+ "gui": {
+ "scaling": {
+ "type": "nine_slice",
+ "width": 91,
+ "height": 184,
+ "border": 7
+ }
+ }
+}
diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png
new file mode 100644
index 0000000..61e9ee5
--- /dev/null
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta
new file mode 100644
index 0000000..cd2857e
--- /dev/null
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta
@@ -0,0 +1,9 @@
+{
+ "gui": {
+ "scaling": {
+ "type": "tile",
+ "width": 162,
+ "height": 18
+ }
+ }
+}
diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png
new file mode 100644
index 0000000..653a99e
--- /dev/null
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta
new file mode 100644
index 0000000..a29299d
--- /dev/null
+++ b/src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta
@@ -0,0 +1,10 @@
+{
+ "gui": {
+ "scaling": {
+ "type": "nine_slice",
+ "width": 176,
+ "height": 222,
+ "border": 10
+ }
+ }
+}
diff --git a/src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta
new file mode 100644
index 0000000..035feaa
--- /dev/null
+++ b/src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta
@@ -0,0 +1,10 @@
+{
+ "pack": {
+ "pack_format": 15,
+ "supported_formats": {
+ "min_inclusive": 15,
+ "max_inclusive": 2147483647
+ },
+ "description": "Adds a more transparent overlay for Firmament"
+ }
+}
diff --git a/src/test/kotlin/MixinTest.kt b/src/test/kotlin/MixinTest.kt
new file mode 100644
index 0000000..55aa7c2
--- /dev/null
+++ b/src/test/kotlin/MixinTest.kt
@@ -0,0 +1,34 @@
+package moe.nea.firmament.test
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import org.spongepowered.asm.mixin.MixinEnvironment
+import org.spongepowered.asm.mixin.transformer.IMixinTransformer
+import moe.nea.firmament.init.MixinPlugin
+
+class MixinTest {
+ @Test
+ fun mixinAudit() {
+ FirmTestBootstrap.bootstrapMinecraft()
+ MixinEnvironment.getCurrentEnvironment().audit()
+ val mp = MixinPlugin.instances.single()
+ Assertions.assertEquals(
+ mp.expectedFullPathMixins,
+ mp.appliedFullPathMixins,
+ )
+ Assertions.assertNotEquals(
+ 0,
+ mp.mixins.size
+ )
+
+ }
+
+ @Test
+ fun hasInstalledMixinTransformer() {
+ Assertions.assertInstanceOf(
+ IMixinTransformer::class.java,
+ MixinEnvironment.getCurrentEnvironment().activeTransformer
+ )
+ }
+}
+
diff --git a/src/test/kotlin/features/macros/KeyComboTrieCreation.kt b/src/test/kotlin/features/macros/KeyComboTrieCreation.kt
new file mode 100644
index 0000000..f0e7a1b
--- /dev/null
+++ b/src/test/kotlin/features/macros/KeyComboTrieCreation.kt
@@ -0,0 +1,103 @@
+package moe.nea.firmament.test.features.macros
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import net.minecraft.client.util.InputUtil
+import moe.nea.firmament.features.macros.Branch
+import moe.nea.firmament.features.macros.ComboKeyAction
+import moe.nea.firmament.features.macros.CommandAction
+import moe.nea.firmament.features.macros.KeyComboTrie
+import moe.nea.firmament.features.macros.Leaf
+import moe.nea.firmament.keybindings.SavedKeyBinding
+
+class KeyComboTrieCreation {
+ val basicAction = CommandAction("ac Hello")
+ val aPress = SavedKeyBinding(InputUtil.GLFW_KEY_A)
+ val bPress = SavedKeyBinding(InputUtil.GLFW_KEY_B)
+ val cPress = SavedKeyBinding(InputUtil.GLFW_KEY_C)
+
+ @Test
+ fun testValidShortTrie() {
+ val actions = listOf(
+ ComboKeyAction(basicAction, listOf(aPress)),
+ ComboKeyAction(basicAction, listOf(bPress)),
+ ComboKeyAction(basicAction, listOf(cPress)),
+ )
+ Assertions.assertEquals(
+ Branch(
+ mapOf(
+ aPress to Leaf(basicAction),
+ bPress to Leaf(basicAction),
+ cPress to Leaf(basicAction),
+ ),
+ ), KeyComboTrie.fromComboList(actions)
+ )
+ }
+
+ @Test
+ fun testOverlappingLeafs() {
+ Assertions.assertThrows(IllegalStateException::class.java) {
+ KeyComboTrie.fromComboList(
+ listOf(
+ ComboKeyAction(basicAction, listOf(aPress, aPress)),
+ ComboKeyAction(basicAction, listOf(aPress, aPress)),
+ )
+ )
+ }
+ Assertions.assertThrows(IllegalStateException::class.java) {
+ KeyComboTrie.fromComboList(
+ listOf(
+ ComboKeyAction(basicAction, listOf(aPress)),
+ ComboKeyAction(basicAction, listOf(aPress)),
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testBranchOverlappingLeaf() {
+ Assertions.assertThrows(IllegalStateException::class.java) {
+ KeyComboTrie.fromComboList(
+ listOf(
+ ComboKeyAction(basicAction, listOf(aPress)),
+ ComboKeyAction(basicAction, listOf(aPress, aPress)),
+ )
+ )
+ }
+ }
+ @Test
+ fun testLeafOverlappingBranch() {
+ Assertions.assertThrows(IllegalStateException::class.java) {
+ KeyComboTrie.fromComboList(
+ listOf(
+ ComboKeyAction(basicAction, listOf(aPress, aPress)),
+ ComboKeyAction(basicAction, listOf(aPress)),
+ )
+ )
+ }
+ }
+
+
+ @Test
+ fun testValidNestedTrie() {
+ val actions = listOf(
+ ComboKeyAction(basicAction, listOf(aPress, aPress)),
+ ComboKeyAction(basicAction, listOf(aPress, bPress)),
+ ComboKeyAction(basicAction, listOf(cPress)),
+ )
+ Assertions.assertEquals(
+ Branch(
+ mapOf(
+ aPress to Branch(
+ mapOf(
+ aPress to Leaf(basicAction),
+ bPress to Leaf(basicAction),
+ )
+ ),
+ cPress to Leaf(basicAction),
+ ),
+ ), KeyComboTrie.fromComboList(actions)
+ )
+ }
+
+}
diff --git a/src/test/kotlin/root.kt b/src/test/kotlin/root.kt
index 045fdd5..000ddda 100644
--- a/src/test/kotlin/root.kt
+++ b/src/test/kotlin/root.kt
@@ -24,6 +24,7 @@ object FirmTestBootstrap {
println("Bootstrap completed at $loadEnd after $loadDuration")
}
+ @JvmStatic
fun bootstrapMinecraft() {
}
}
diff --git a/src/test/kotlin/testutil/AutoBootstrapExtension.kt b/src/test/kotlin/testutil/AutoBootstrapExtension.kt
new file mode 100644
index 0000000..6f225a0
--- /dev/null
+++ b/src/test/kotlin/testutil/AutoBootstrapExtension.kt
@@ -0,0 +1,14 @@
+package moe.nea.firmament.test.testutil
+
+import com.google.auto.service.AutoService
+import org.junit.jupiter.api.extension.BeforeAllCallback
+import org.junit.jupiter.api.extension.Extension
+import org.junit.jupiter.api.extension.ExtensionContext
+import moe.nea.firmament.test.FirmTestBootstrap
+
+@AutoService(Extension::class)
+class AutoBootstrapExtension : Extension, BeforeAllCallback {
+ override fun beforeAll(p0: ExtensionContext) {
+ FirmTestBootstrap.bootstrapMinecraft()
+ }
+}
diff --git a/src/test/kotlin/testutil/ItemResources.kt b/src/test/kotlin/testutil/ItemResources.kt
index 107b565..e996fc2 100644
--- a/src/test/kotlin/testutil/ItemResources.kt
+++ b/src/test/kotlin/testutil/ItemResources.kt
@@ -1,15 +1,24 @@
package moe.nea.firmament.test.testutil
+import com.mojang.datafixers.DSL
+import com.mojang.serialization.Dynamic
+import com.mojang.serialization.JsonOps
+import net.minecraft.SharedConstants
+import net.minecraft.datafixer.Schemas
+import net.minecraft.datafixer.TypeReferences
import net.minecraft.item.ItemStack
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.registry.RegistryOps
import net.minecraft.text.Text
import net.minecraft.text.TextCodecs
+import moe.nea.firmament.features.debug.ExportedTestConstantMeta
import moe.nea.firmament.test.FirmTestBootstrap
import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.mc.MCTabListAPI
object ItemResources {
init {
@@ -24,18 +33,62 @@ object ItemResources {
}
fun loadSNbt(path: String): NbtCompound {
- return StringNbtReader.parse(loadString(path))
+ return StringNbtReader.readCompound(loadString(path))
}
+
fun getNbtOps(): RegistryOps<NbtElement> = MC.currentOrDefaultRegistries.getOps(NbtOps.INSTANCE)
+ fun tryMigrateNbt(
+ nbtCompound: NbtCompound,
+ typ: DSL.TypeReference?,
+ ): NbtElement {
+ val source = nbtCompound.get("source", ExportedTestConstantMeta.CODEC)
+ nbtCompound.remove("source")
+ if (source.isPresent) {
+ val wrappedNbtSource = if (typ == TypeReferences.TEXT_COMPONENT && source.get().dataVersion < 4325) {
+ // Per 1.21.5 text components are wrapped in a string, which firmament unwrapped in the snbt files
+ NbtString.of(
+ NbtOps.INSTANCE.convertTo(JsonOps.INSTANCE, nbtCompound)
+ .toString()
+ )
+ } else {
+ nbtCompound
+ }
+ if (typ != null) {
+ return Schemas.getFixer()
+ .update(
+ typ,
+ Dynamic(NbtOps.INSTANCE, wrappedNbtSource),
+ source.get().dataVersion,
+ SharedConstants.getGameVersion().saveVersion.id
+ ).value
+ } else {
+ wrappedNbtSource
+ }
+ }
+ return nbtCompound
+ }
+
+ fun loadTablist(name: String): MCTabListAPI.CurrentTabList {
+ return MCTabListAPI.CurrentTabList.CODEC.parse(
+ getNbtOps(),
+ tryMigrateNbt(loadSNbt("testdata/tablist/$name.snbt"), null),
+ ).getOrThrow { IllegalStateException("Could not load tablist '$name': $it") }
+ }
+
fun loadText(name: String): Text {
- return TextCodecs.CODEC.parse(getNbtOps(), loadSNbt("testdata/chat/$name.snbt"))
- .getOrThrow { IllegalStateException("Could not load test chat '$name': $it") }
+ return TextCodecs.CODEC.parse(
+ getNbtOps(),
+ tryMigrateNbt(loadSNbt("testdata/chat/$name.snbt"), TypeReferences.TEXT_COMPONENT)
+ ).getOrThrow { IllegalStateException("Could not load test chat '$name': $it") }
}
fun loadItem(name: String): ItemStack {
- // TODO: make the load work with enchantments
- return ItemStack.CODEC.parse(getNbtOps(), loadSNbt("testdata/items/$name.snbt"))
- .getOrThrow { IllegalStateException("Could not load test item '$name': $it") }
+ try {
+ val itemNbt = loadSNbt("testdata/items/$name.snbt")
+ return ItemStack.CODEC.parse(getNbtOps(), tryMigrateNbt(itemNbt, TypeReferences.ITEM_STACK)).orThrow
+ } catch (ex: Exception) {
+ throw RuntimeException("Could not load item resource '$name'", ex)
+ }
}
}
diff --git a/src/test/kotlin/testutil/KotestPlugin.kt b/src/test/kotlin/testutil/KotestPlugin.kt
deleted file mode 100644
index 6db50fb..0000000
--- a/src/test/kotlin/testutil/KotestPlugin.kt
+++ /dev/null
@@ -1,16 +0,0 @@
-package moe.nea.firmament.test.testutil
-
-import io.kotest.core.config.AbstractProjectConfig
-import io.kotest.core.extensions.Extension
-import moe.nea.firmament.test.FirmTestBootstrap
-
-class KotestPlugin : AbstractProjectConfig() {
- override fun extensions(): List<Extension> {
- return listOf()
- }
-
- override suspend fun beforeProject() {
- FirmTestBootstrap.bootstrapMinecraft()
- super.beforeProject()
- }
-}
diff --git a/src/test/kotlin/util/ColorCodeTest.kt b/src/test/kotlin/util/ColorCodeTest.kt
index 949749e..7c581c5 100644
--- a/src/test/kotlin/util/ColorCodeTest.kt
+++ b/src/test/kotlin/util/ColorCodeTest.kt
@@ -1,57 +1,57 @@
package moe.nea.firmament.test.util
-import io.kotest.core.spec.style.AnnotationSpec
import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
import moe.nea.firmament.util.removeColorCodes
-class ColorCodeTest : AnnotationSpec() {
- @Test
- fun testWhatever() {
- Assertions.assertEquals("", "".removeColorCodes())
- Assertions.assertEquals("", "§".removeColorCodes())
- Assertions.assertEquals("", "§a".removeColorCodes())
- Assertions.assertEquals("ab", "a§ab".removeColorCodes())
- Assertions.assertEquals("ab", "a§ab§§".removeColorCodes())
- Assertions.assertEquals("abc", "a§ab§§c".removeColorCodes())
- Assertions.assertEquals("bc", "§ab§§c".removeColorCodes())
- Assertions.assertEquals("b§lc", "§ab§l§§c".removeColorCodes(true))
- Assertions.assertEquals("b§lc§l", "§ab§l§§c§l".removeColorCodes(true))
- Assertions.assertEquals("§lb§lc", "§l§ab§l§§c".removeColorCodes(true))
- }
-
- @Test
- fun testEdging() {
- Assertions.assertEquals("", "§".removeColorCodes())
- Assertions.assertEquals("a", "a§".removeColorCodes())
- Assertions.assertEquals("b", "§ab§".removeColorCodes())
- }
-
- @Test
- fun `testDouble§`() {
- Assertions.assertEquals("1", "§§1".removeColorCodes())
- }
-
- @Test
- fun testKeepNonColor() {
- Assertions.assertEquals("§k§l§m§n§o§r", "§k§l§m§f§n§o§r".removeColorCodes(true))
- }
-
- @Test
- fun testPlainString() {
- Assertions.assertEquals("bcdefgp", "bcdefgp".removeColorCodes())
- Assertions.assertEquals("", "".removeColorCodes())
- }
-
- @Test
- fun testSomeNormalTestCases() {
- Assertions.assertEquals(
- "You are not currently in a party.",
- "§r§cYou are not currently in a party.§r".removeColorCodes()
- )
- Assertions.assertEquals(
- "Ancient Necron's Chestplate ✪✪✪✪",
- "§dAncient Necron's Chestplate §6✪§6✪§6✪§6✪".removeColorCodes()
- )
- }
+class ColorCodeTest {
+ @Test
+ fun testWhatever() {
+ Assertions.assertEquals("", "".removeColorCodes())
+ Assertions.assertEquals("", "§".removeColorCodes())
+ Assertions.assertEquals("", "§a".removeColorCodes())
+ Assertions.assertEquals("ab", "a§ab".removeColorCodes())
+ Assertions.assertEquals("ab", "a§ab§§".removeColorCodes())
+ Assertions.assertEquals("abc", "a§ab§§c".removeColorCodes())
+ Assertions.assertEquals("bc", "§ab§§c".removeColorCodes())
+ Assertions.assertEquals("b§lc", "§ab§l§§c".removeColorCodes(true))
+ Assertions.assertEquals("b§lc§l", "§ab§l§§c§l".removeColorCodes(true))
+ Assertions.assertEquals("§lb§lc", "§l§ab§l§§c".removeColorCodes(true))
+ }
+
+ @Test
+ fun testEdging() {
+ Assertions.assertEquals("", "§".removeColorCodes())
+ Assertions.assertEquals("a", "a§".removeColorCodes())
+ Assertions.assertEquals("b", "§ab§".removeColorCodes())
+ }
+
+ @Test
+ fun `testDouble§`() {
+ Assertions.assertEquals("1", "§§1".removeColorCodes())
+ }
+
+ @Test
+ fun testKeepNonColor() {
+ Assertions.assertEquals("§k§l§m§n§o§r", "§k§l§m§f§n§o§r".removeColorCodes(true))
+ }
+
+ @Test
+ fun testPlainString() {
+ Assertions.assertEquals("bcdefgp", "bcdefgp".removeColorCodes())
+ Assertions.assertEquals("", "".removeColorCodes())
+ }
+
+ @Test
+ fun testSomeNormalTestCases() {
+ Assertions.assertEquals(
+ "You are not currently in a party.",
+ "§r§cYou are not currently in a party.§r".removeColorCodes()
+ )
+ Assertions.assertEquals(
+ "Ancient Necron's Chestplate ✪✪✪✪",
+ "§dAncient Necron's Chestplate §6✪§6✪§6✪§6✪".removeColorCodes()
+ )
+ }
}
diff --git a/src/test/kotlin/util/TextUtilText.kt b/src/test/kotlin/util/TextUtilText.kt
index 46ed3b4..94ab222 100644
--- a/src/test/kotlin/util/TextUtilText.kt
+++ b/src/test/kotlin/util/TextUtilText.kt
@@ -1,16 +1,18 @@
package moe.nea.firmament.test.util
-import io.kotest.core.spec.style.AnnotationSpec
import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
import moe.nea.firmament.test.testutil.ItemResources
import moe.nea.firmament.util.getLegacyFormatString
-class TextUtilText : AnnotationSpec() {
+class TextUtilText {
@Test
fun testThing() {
// TODO: add more tests that are directly validated with 1.8.9 code
val text = ItemResources.loadText("all-chat")
- Assertions.assertEquals("§r§r§8[§r§9302§r§8] §r§6♫ §r§b[MVP§r§d+§r§b] lrg89§r§f: test§r",
- text.getLegacyFormatString())
+ Assertions.assertEquals(
+ "§r§r§8[§r§9302§r§8] §r§6♫ §r§b[MVP§r§d+§r§b] lrg89§r§f: test§r",
+ text.getLegacyFormatString()
+ )
}
}
diff --git a/src/test/kotlin/util/math/GChainReconciliationTest.kt b/src/test/kotlin/util/math/GChainReconciliationTest.kt
new file mode 100644
index 0000000..380ea5c
--- /dev/null
+++ b/src/test/kotlin/util/math/GChainReconciliationTest.kt
@@ -0,0 +1,75 @@
+package moe.nea.firmament.test.util.math
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Assertions.*
+import org.junit.jupiter.api.Test
+import moe.nea.firmament.util.math.GChainReconciliation
+import moe.nea.firmament.util.math.GChainReconciliation.rotated
+
+class GChainReconciliationTest {
+
+ fun <T> assertEqualCycles(
+ expected: List<T>,
+ actual: List<T>
+ ) {
+ for (offset in expected.indices) {
+ val rotated = expected.rotated(offset)
+ val matchesAtRotation = run {
+ for ((i, v) in actual.withIndex()) {
+ if (rotated[i % rotated.size] != v)
+ return@run false
+ }
+ true
+ }
+ if (matchesAtRotation)
+ return
+ }
+ assertEquals(expected, actual, "Expected arrays to be cycle equivalent")
+ }
+
+ @Test
+ fun testUnfixableCycleNotBeingModified() {
+ assertEquals(
+ listOf(1, 2, 3, 4, 6, 1, 2, 3, 4, 6),
+ GChainReconciliation.reconcileCycles(
+ listOf(1, 2, 3, 4, 6, 1, 2, 3, 4, 6),
+ listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1)
+ )
+ )
+ }
+
+ @Test
+ fun testMultipleIndependentHoles() {
+ assertEqualCycles(
+ listOf(1, 2, 3, 4, 5, 6),
+ GChainReconciliation.reconcileCycles(
+ listOf(1, 3, 4, 5, 6, 1, 3, 4, 5, 6),
+ listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1)
+ )
+ )
+
+ }
+
+ @Test
+ fun testBigHole() {
+ assertEqualCycles(
+ listOf(1, 2, 3, 4, 5, 6),
+ GChainReconciliation.reconcileCycles(
+ listOf(1, 4, 5, 6, 1, 4, 5, 6),
+ listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1)
+ )
+ )
+
+ }
+
+ @Test
+ fun testOneMissingBeingDetected() {
+ assertEqualCycles(
+ listOf(1, 2, 3, 4, 5, 6),
+ GChainReconciliation.reconcileCycles(
+ listOf(1, 2, 3, 4, 5, 6, 1, 2, 3, 4, 5, 6),
+ listOf(2, 3, 4, 5, 1, 2, 3, 4, 5, 1)
+ )
+ )
+ }
+}
diff --git a/src/test/kotlin/util/math/ProjectionsBoxTest.kt b/src/test/kotlin/util/math/ProjectionsBoxTest.kt
new file mode 100644
index 0000000..04720a3
--- /dev/null
+++ b/src/test/kotlin/util/math/ProjectionsBoxTest.kt
@@ -0,0 +1,28 @@
+package moe.nea.firmament.test.util.math
+
+import java.util.stream.Stream
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.DynamicTest
+import org.junit.jupiter.api.TestFactory
+import kotlin.streams.asStream
+import net.minecraft.util.math.Vec2f
+import moe.nea.firmament.util.math.Projections
+
+class ProjectionsBoxTest {
+ val Double.degrees get() = Math.toRadians(this)
+
+ @TestFactory
+ fun testProjections(): Stream<DynamicTest> {
+ return sequenceOf(
+ 0.0.degrees to Vec2f(1F, 0F),
+ 63.4349.degrees to Vec2f(0.5F, 1F),
+ ).map { (angle, expected) ->
+ DynamicTest.dynamicTest("ProjectionsBoxTest::projectAngleOntoUnitBox(${angle})") {
+ val actual = Projections.Two.projectAngleOntoUnitBox(angle)
+ fun msg() = "Expected (${expected.x}, ${expected.y}) got (${actual.x}, ${actual.y})"
+ Assertions.assertEquals(expected.x, actual.x, 0.0001F, ::msg)
+ Assertions.assertEquals(expected.y, actual.y, 0.0001F, ::msg)
+ }
+ }.asStream()
+ }
+}
diff --git a/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt
index 206a357..9d25aad 100644
--- a/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt
+++ b/src/test/kotlin/util/skyblock/AbilityUtilsTest.kt
@@ -1,7 +1,7 @@
package moe.nea.firmament.test.util.skyblock
-import io.kotest.core.spec.style.AnnotationSpec
import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
import kotlin.time.Duration.Companion.minutes
import kotlin.time.Duration.Companion.seconds
import net.minecraft.text.Text
@@ -9,7 +9,7 @@ import moe.nea.firmament.test.testutil.ItemResources
import moe.nea.firmament.util.skyblock.AbilityUtils
import moe.nea.firmament.util.unformattedString
-class AbilityUtilsTest : AnnotationSpec() {
+class AbilityUtilsTest {
fun List<AbilityUtils.ItemAbility>.stripDescriptions() = map {
it.copy(descriptionLines = it.descriptionLines.map { Text.literal(it.unformattedString) })
@@ -24,9 +24,11 @@ class AbilityUtilsTest : AnnotationSpec() {
false,
AbilityUtils.AbilityActivation.RIGHT_CLICK,
null,
- listOf("Throw your pickaxe to create an",
- "explosion mining all ores in a 3 block",
- "radius.").map(Text::literal),
+ listOf(
+ "Throw your pickaxe to create an",
+ "explosion mining all ores in a 3 block",
+ "radius."
+ ).map(Text::literal),
48.seconds
)
),
@@ -43,8 +45,10 @@ class AbilityUtilsTest : AnnotationSpec() {
true,
AbilityUtils.AbilityActivation.RIGHT_CLICK,
null,
- listOf("Grants +200% ⸕ Mining Speed for",
- "10s.").map(Text::literal),
+ listOf(
+ "Grants +200% ⸕ Mining Speed for",
+ "10s."
+ ).map(Text::literal),
2.minutes
)
),
@@ -58,8 +62,10 @@ class AbilityUtilsTest : AnnotationSpec() {
listOf(
AbilityUtils.ItemAbility(
"Instant Transmission", true, AbilityUtils.AbilityActivation.RIGHT_CLICK, 23,
- listOf("Teleport 12 blocks ahead of you and",
- "gain +50 ✦ Speed for 3 seconds.").map(Text::literal),
+ listOf(
+ "Teleport 12 blocks ahead of you and",
+ "gain +50 ✦ Speed for 3 seconds."
+ ).map(Text::literal),
null
),
AbilityUtils.ItemAbility(
@@ -67,9 +73,11 @@ class AbilityUtilsTest : AnnotationSpec() {
false,
AbilityUtils.AbilityActivation.SNEAK_RIGHT_CLICK,
90,
- listOf("Teleport to your targeted block up",
- "to 61 blocks away.",
- "Soulflow Cost: 1").map(Text::literal),
+ listOf(
+ "Teleport to your targeted block up",
+ "to 61 blocks away.",
+ "Soulflow Cost: 1"
+ ).map(Text::literal),
null
)
),
diff --git a/src/test/kotlin/util/skyblock/ItemTypeTest.kt b/src/test/kotlin/util/skyblock/ItemTypeTest.kt
index cca3d13..c0ef2a3 100644
--- a/src/test/kotlin/util/skyblock/ItemTypeTest.kt
+++ b/src/test/kotlin/util/skyblock/ItemTypeTest.kt
@@ -1,26 +1,28 @@
package moe.nea.firmament.test.util.skyblock
-import io.kotest.core.spec.style.ShouldSpec
-import io.kotest.matchers.shouldBe
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.DynamicTest
+import org.junit.jupiter.api.TestFactory
import moe.nea.firmament.test.testutil.ItemResources
import moe.nea.firmament.util.skyblock.ItemType
-class ItemTypeTest
- : ShouldSpec(
- {
- context("ItemType.fromItemstack") {
- listOf(
- "pets/lion-item" to ItemType.PET,
- "pets/rabbit-selected" to ItemType.PET,
- "pets/mithril-golem-not-selected" to ItemType.PET,
- "aspect-of-the-void" to ItemType.SWORD,
- "titanium-drill" to ItemType.DRILL,
- "diamond-pickaxe" to ItemType.PICKAXE,
- "gemstone-gauntlet" to ItemType.GAUNTLET,
- ).forEach { (name, typ) ->
- should("return $typ for $name") {
- ItemType.fromItemStack(ItemResources.loadItem(name)) shouldBe typ
- }
+class ItemTypeTest {
+ @TestFactory
+ fun fromItemstack() =
+ listOf(
+ "pets/lion-item" to ItemType.PET,
+ "pets/rabbit-selected" to ItemType.PET,
+ "pets/mithril-golem-not-selected" to ItemType.PET,
+ "aspect-of-the-void" to ItemType.SWORD,
+ "titanium-drill" to ItemType.DRILL,
+ "diamond-pickaxe" to ItemType.PICKAXE,
+ "gemstone-gauntlet" to ItemType.GAUNTLET,
+ ).map { (name, typ) ->
+ DynamicTest.dynamicTest("return $typ for $name") {
+ Assertions.assertEquals(
+ typ,
+ ItemType.fromItemStack(ItemResources.loadItem(name))
+ )
}
}
- })
+}
diff --git a/src/test/kotlin/util/skyblock/SackUtilTest.kt b/src/test/kotlin/util/skyblock/SackUtilTest.kt
index f93cd2b..e0e3e63 100644
--- a/src/test/kotlin/util/skyblock/SackUtilTest.kt
+++ b/src/test/kotlin/util/skyblock/SackUtilTest.kt
@@ -1,12 +1,12 @@
package moe.nea.firmament.test.util.skyblock
-import io.kotest.core.spec.style.AnnotationSpec
import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
import moe.nea.firmament.test.testutil.ItemResources
import moe.nea.firmament.util.skyblock.SackUtil
import moe.nea.firmament.util.skyblock.SkyBlockItems
-class SackUtilTest : AnnotationSpec() {
+class SackUtilTest {
@Test
fun testOneRottenFlesh() {
Assertions.assertEquals(
diff --git a/src/test/kotlin/util/skyblock/TabListAPITest.kt b/src/test/kotlin/util/skyblock/TabListAPITest.kt
new file mode 100644
index 0000000..26eafe0
--- /dev/null
+++ b/src/test/kotlin/util/skyblock/TabListAPITest.kt
@@ -0,0 +1,48 @@
+package moe.nea.firmament.test.util.skyblock
+
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import moe.nea.firmament.test.testutil.ItemResources
+import moe.nea.firmament.util.skyblock.TabListAPI
+
+class TabListAPITest {
+ val tablist = ItemResources.loadTablist("dungeon_hub")
+
+ @Test
+ fun checkWithTitle() {
+ Assertions.assertEquals(
+ listOf(
+ "Profile: Strawberry",
+ " SB Level: [210] 26/100 XP",
+ " Bank: 1.4B",
+ " Interest: 12 Hours (689.1k)",
+ ),
+ TabListAPI.getWidgetLines(TabListAPI.WidgetName.PROFILE, includeTitle = true, from = tablist).map { it.string })
+ }
+
+ @Test
+ fun checkEndOfColumn() {
+ Assertions.assertEquals(
+ listOf(
+ " Bonzo IV: 110/150",
+ " Scarf II: 25/50",
+ " The Professor IV: 141/150",
+ " Thorn I: 29/50",
+ " Livid II: 91/100",
+ " Sadan V: 388/500",
+ " Necron VI: 531/750",
+ ),
+ TabListAPI.getWidgetLines(TabListAPI.WidgetName.COLLECTION, from = tablist).map { it.string }
+ )
+ }
+
+ @Test
+ fun checkWithoutTitle() {
+ Assertions.assertEquals(
+ listOf(
+ " Undead: 1,907",
+ " Wither: 318",
+ ),
+ TabListAPI.getWidgetLines(TabListAPI.WidgetName.ESSENCE, from = tablist).map { it.string })
+ }
+}
diff --git a/src/test/kotlin/util/skyblock/TimestampTest.kt b/src/test/kotlin/util/skyblock/TimestampTest.kt
new file mode 100644
index 0000000..b960cb9
--- /dev/null
+++ b/src/test/kotlin/util/skyblock/TimestampTest.kt
@@ -0,0 +1,28 @@
+package moe.nea.firmament.test.util.skyblock
+
+import java.time.Instant
+import java.time.ZonedDateTime
+import org.junit.jupiter.api.Assertions
+import org.junit.jupiter.api.Test
+import moe.nea.firmament.test.testutil.ItemResources
+import moe.nea.firmament.util.SBData
+import moe.nea.firmament.util.timestamp
+
+class TimestampTest {
+
+ @Test
+ fun testLongTimestamp() {
+ Assertions.assertEquals(
+ Instant.ofEpochSecond(1658091600),
+ ItemResources.loadItem("hyperion").timestamp
+ )
+ }
+
+ @Test
+ fun testStringTimestamp() {
+ Assertions.assertEquals(
+ ZonedDateTime.of(2021, 10, 11, 15, 39, 0, 0, SBData.hypixelTimeZone).toInstant(),
+ ItemResources.loadItem("backpack-in-menu").timestamp
+ )
+ }
+}
diff --git a/src/test/resources/testdata/chat/all-chat.snbt b/src/test/resources/testdata/chat/all-chat.snbt
index 15cc2de..386194b 100644
--- a/src/test/resources/testdata/chat/all-chat.snbt
+++ b/src/test/resources/testdata/chat/all-chat.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
extra: [
{
bold: 0b,
diff --git a/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt b/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt
index 924a558..d7b8b90 100644
--- a/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt
+++ b/src/test/resources/testdata/chat/sacks/gain-and-lose-regular.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
color: "#FFAA00",
extra: [
{
diff --git a/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt b/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt
index 924a558..d7b8b90 100644
--- a/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt
+++ b/src/test/resources/testdata/chat/sacks/gain-rotten-flesh.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
color: "#FFAA00",
extra: [
{
diff --git a/src/test/resources/testdata/items/aspect-of-the-void.snbt b/src/test/resources/testdata/items/aspect-of-the-void.snbt
index 180c069..9ffd385 100644
--- a/src/test/resources/testdata/items/aspect-of-the-void.snbt
+++ b/src/test/resources/testdata/items/aspect-of-the-void.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
components: {
"minecraft:attribute_modifiers": {
modifiers: [
diff --git a/src/test/resources/testdata/items/backpack-in-menu.snbt b/src/test/resources/testdata/items/backpack-in-menu.snbt
new file mode 100644
index 0000000..2f22768
--- /dev/null
+++ b/src/test/resources/testdata/items/backpack-in-menu.snbt
@@ -0,0 +1,122 @@
+{
+ components: {
+ "minecraft:custom_data": {
+ backpack_color: "BROWN",
+ originTag: "CRAFTING_GRID_COLLECT",
+ timestamp: "10/11/21 3:39 PM",
+ uuid: "3d7c83e8-c619-4603-8cfb-c95ceed90864"
+ },
+ "minecraft:custom_name": {
+ extra: [
+ {
+ color: "gold",
+ text: "Backpack Slot 3"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "minecraft:lore": [
+ {
+ extra: [
+ {
+ color: "gold",
+ text: "Jumbo Backpack"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "gray",
+ text: ""
+ },
+ {
+ color: "gray",
+ text: "This backpack has "
+ },
+ {
+ color: "green",
+ text: "45"
+ },
+ {
+ color: "gray",
+ text: " slots."
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " "
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "gray",
+ text: ""
+ },
+ {
+ color: "yellow",
+ text: "Left-click to open!"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "gray",
+ text: ""
+ },
+ {
+ color: "yellow",
+ text: "Right-click to remove!"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ }
+ ],
+ "minecraft:profile": {
+ id: [I;
+ 1252359403,
+ 1319582828,
+ -1927151386,
+ 833492163
+ ],
+ properties: [
+ {
+ name: "textures",
+ signature: "U/49v6SXIw8bAmqM6T7t1BIR736N3Adpx7MlWncnT8zcFEm97zwRx9/tyaUy/XxBHaPGSL6BbgW2TdBtfb9gf0emCAZyWmnzSTtqDGiWpxnQM8v3+gHS8zD7Xrho0a/hU33xTbQ2knj2iRz8C+FReoJFxCjS++aXq6IqliIb3GhqB5b1egaiG2Q3t+yerl2Xue4nhdYM3wtGsYApC/ClR3TEuBcJv1WUVZM8rEoU29pbVnyMCKineG6mIN7W86SmzcT2SF+zMVyD0/mI7R2hRT2lbXnkMpM6FFscdnlvzjjPB9brtAWY7JGJ63b9C+khnvZUlhlQ/3E/08dFnON31VeabJXOmfrbfAgsF0Hgfs7Io+HzoXSXr/FCxNCCFMWlSwORmG2WCT4VRFzG2SThatPVPGJkuR/tLLOLzXo4RKOMzY5EIwa2XSxRUI4+5z2SZY11ofGic3bZD3wvICs2EZ54Pi508ZOda0qI9w5Q/TazC+jX/I5Nq2TLqLj+uU/+UX8eKXvHdk8QpBynyv9SyHo21jVXpiUgL1AsdzBp9cTZHNJuYtBxgDogr3SyAKPmw3BOzVeUi6qW8k4lgtefLKYteVSh52PjFgvQZUR1GNmFaJ+hlgKz8yONp+wXhw3nyL4dMOd2Z/dVVSywBp0tyHuN5l3PfaInK4s8qSydaW0=",
+ value: "ewogICJ0aW1lc3RhbXAiIDogMTcxOTUzODgxNTgyNCwKICAicHJvZmlsZUlkIiA6ICJkOWYxNTlhYWYxZjY0NGZlOTEwOTg0NzI2ZDBjMWJjMCIsCiAgInByb2ZpbGVOYW1lIiA6ICJtYW5vbmFtaXNzaW9uRyIsCiAgInNpZ25hdHVyZVJlcXVpcmVkIiA6IHRydWUsCiAgInRleHR1cmVzIiA6IHsKICAgICJTS0lOIiA6IHsKICAgICAgInVybCIgOiAiaHR0cDovL3RleHR1cmVzLm1pbmVjcmFmdC5uZXQvdGV4dHVyZS81YWQwYjQwNTIxMjYyYjdhM2Y5OWU2M2JkZGQ0YTNlNTQxOTY1Njc3ZTE0MTRlYWZhMTQyZThiYmE5ZGZlNDgxIiwKICAgICAgIm1ldGFkYXRhIiA6IHsKICAgICAgICAibW9kZWwiIDogInNsaW0iCiAgICAgIH0KICAgIH0KICB9Cn0="
+ }
+ ]
+ },
+ "minecraft:tooltip_display": {
+ hidden_components: [
+ "minecraft:jukebox_playable",
+ "minecraft:painting/variant",
+ "minecraft:map_id",
+ "minecraft:fireworks",
+ "minecraft:attribute_modifiers",
+ "minecraft:unbreakable",
+ "minecraft:written_book_content",
+ "minecraft:banner_patterns",
+ "minecraft:trim",
+ "minecraft:potion_contents",
+ "minecraft:block_entity_data",
+ "minecraft:dyed_color"
+ ]
+ }
+ },
+ count: 3,
+ id: "minecraft:player_head"
+}
diff --git a/src/test/resources/testdata/items/books/feather_falling.snbt b/src/test/resources/testdata/items/books/feather_falling.snbt
new file mode 100644
index 0000000..4a0b7c6
--- /dev/null
+++ b/src/test/resources/testdata/items/books/feather_falling.snbt
@@ -0,0 +1,39 @@
+{
+ source: {
+ dataVersion: 4189,
+ },
+ components: {
+ "minecraft:attribute_modifiers": {
+ modifiers: [
+ ],
+ show_in_tooltip: 0b
+ },
+ "minecraft:custom_data": {
+ enchantments: {
+ feather_falling: 6
+ },
+ id: "ENCHANTED_BOOK",
+ timestamp: 1737123521091L,
+ uuid: "b8128489-9ed0-4a1a-94c0-d3279ffe45ac"
+ },
+ "minecraft:custom_name": '{"extra":[{"color":"blue","text":"Enchanted Book"}],"italic":false,"text":""}',
+ "minecraft:hide_additional_tooltip": {
+ },
+ "minecraft:lore": [
+ '{"extra":[{"color":"blue","text":"Feather Falling VI"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Increases how high you can fall"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"before taking fall damage by "},{"color":"green","text":"6"},{"color":"gray","text":" and"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"reduces fall damage by "},{"color":"green","text":"30%"},{"color":"gray","text":"."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Applicable on: "},{"color":"blue","text":"Boots"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"gray","text":"Apply Cost: "},{"color":"dark_aqua","text":"60 Exp Levels"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Use this on an item in an Anvil to"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"apply it!"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"blue","text":"RARE"}],"italic":false,"text":""}'
+ ]
+ },
+ count: 1,
+ id: "minecraft:enchanted_book"
+}
diff --git a/src/test/resources/testdata/items/diamond-pickaxe.snbt b/src/test/resources/testdata/items/diamond-pickaxe.snbt
index cce12f9..aa5e590 100644
--- a/src/test/resources/testdata/items/diamond-pickaxe.snbt
+++ b/src/test/resources/testdata/items/diamond-pickaxe.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
components: {
"minecraft:attribute_modifiers": {
modifiers: [
diff --git a/src/test/resources/testdata/items/gemstone-gauntlet.snbt b/src/test/resources/testdata/items/gemstone-gauntlet.snbt
index 92ce739..92bb806 100644
--- a/src/test/resources/testdata/items/gemstone-gauntlet.snbt
+++ b/src/test/resources/testdata/items/gemstone-gauntlet.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
components: {
"minecraft:attribute_modifiers": {
modifiers: [
diff --git a/src/test/resources/testdata/items/hyperion.snbt b/src/test/resources/testdata/items/hyperion.snbt
index c57d457..f0025b9 100644
--- a/src/test/resources/testdata/items/hyperion.snbt
+++ b/src/test/resources/testdata/items/hyperion.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
components: {
"minecraft:attribute_modifiers": {
modifiers: [
diff --git a/src/test/resources/testdata/items/implosion-belt.snbt b/src/test/resources/testdata/items/implosion-belt.snbt
index b73542d..875047d 100644
--- a/src/test/resources/testdata/items/implosion-belt.snbt
+++ b/src/test/resources/testdata/items/implosion-belt.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
components: {
"minecraft:attribute_modifiers": {
modifiers: [
diff --git a/src/test/resources/testdata/items/necron-boots.snbt b/src/test/resources/testdata/items/necron-boots.snbt
new file mode 100644
index 0000000..fd740ce
--- /dev/null
+++ b/src/test/resources/testdata/items/necron-boots.snbt
@@ -0,0 +1,71 @@
+{
+ source: {
+ dataVersion: 4189,
+ },
+ components: {
+ "minecraft:attribute_modifiers": {
+ modifiers: [
+ ],
+ show_in_tooltip: 0b
+ },
+ "minecraft:custom_data": {
+ enchantments: {
+ depth_strider: 3,
+ feather_falling: 10,
+ growth: 5,
+ protection: 5
+ },
+ id: "POWER_WITHER_BOOTS",
+ modifier: "ancient",
+ rarity_upgrades: 1,
+ timestamp: 1704550620000L,
+ upgrade_level: 5,
+ uuid: "8b6c7485-cb59-44d3-ac8f-9e52a611cc64"
+ },
+ "minecraft:custom_name": '{"extra":[{"color":"light_purple","text":"Ancient Necron\'s Boots "},{"color":"gold","text":"✪✪✪✪✪"}],"italic":false,"text":""}',
+ "minecraft:dyed_color": {
+ rgb: 15167036,
+ show_in_tooltip: 0b
+ },
+ "minecraft:enchantments": {
+ levels: {
+ "minecraft:depth_strider": 3
+ }
+ },
+ "minecraft:hide_additional_tooltip": {
+ },
+ "minecraft:lore": [
+ '{"extra":[{"color":"gray","text":"Gear Score: "},{"color":"light_purple","text":"713 "},{"color":"dark_gray","text":"(2753)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Strength: "},{"color":"red","text":"+79 "},{"color":"blue","text":"(+35) "},{"color":"dark_gray","text":"(+333.75)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Crit Chance: "},{"color":"red","text":"+15% "},{"color":"blue","text":"(+15%) "},{"color":"dark_gray","text":"(+23.1%)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Crit Damage: "},{"color":"red","text":"+71% "},{"color":"blue","text":"(+38%) "},{"color":"dark_gray","text":"(+302.6%)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Health: "},{"color":"green","text":"+241.5 "},{"color":"blue","text":"(+7) "},{"color":"dark_gray","text":"(+1,010.15)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Defense: "},{"color":"green","text":"+120.5 "},{"color":"blue","text":"(+7) "},{"color":"dark_gray","text":"(+498.4)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Intelligence: "},{"color":"green","text":"+36 "},{"color":"blue","text":"(+25) "},{"color":"dark_gray","text":"(+155.75)"}],"italic":false,"text":""}',
+ '{"extra":[" ",{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"❁"},{"color":"dark_gray","text":"] "},{"color":"dark_gray","text":"["},{"color":"dark_gray","text":"⚔"},{"color":"dark_gray","text":"]"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Depth Strider III"},{"color":"blue","text":", "},{"color":"blue","text":"Feather Falling X"},{"color":"blue","text":", "},{"color":"blue","text":"Growth V"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Protection V"}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Reduces the damage you take from"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"withers by "},{"color":"red","text":"10%"},{"color":"gray","text":"."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"gold","text":"Full Set Bonus: Witherborn "},{"color":"gray","text":"(3/4)"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Spawns a wither minion every "},{"color":"yellow","text":"30"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"yellow","text":""},{"color":"gray","text":"seconds up to a maximum "},{"color":"green","text":"1 "},{"color":"gray","text":"wither."}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Your withers will travel to and"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"explode on nearby enemies."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"color":"blue","text":"Ancient Bonus"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":"Grants "},{"color":"green","text":"+1 "},{"color":"blue","text":"☠ Crit Damage "},{"color":"gray","text":"per"}],"italic":false,"text":""}',
+ '{"extra":[{"color":"gray","text":""},{"color":"red","text":"Catacombs "},{"color":"gray","text":"level."}],"italic":false,"text":""}',
+ '{"italic":false,"text":""}',
+ '{"extra":[{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"},"",{"bold":false,"extra":[" "],"italic":false,"obfuscated":false,"strikethrough":false,"text":"","underlined":false},{"bold":true,"color":"light_purple","text":"MYTHIC DUNGEON BOOTS "},{"bold":true,"color":"light_purple","obfuscated":true,"text":"a"}],"italic":false,"text":""}'
+ ],
+ "minecraft:unbreakable": {
+ show_in_tooltip: 0b
+ }
+ },
+ count: 1,
+ id: "minecraft:leather_boots"
+}
diff --git a/src/test/resources/testdata/items/pets/lion-item.snbt b/src/test/resources/testdata/items/pets/lion-item.snbt
index 6e92685..c364032 100644
--- a/src/test/resources/testdata/items/pets/lion-item.snbt
+++ b/src/test/resources/testdata/items/pets/lion-item.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
components: {
"minecraft:attribute_modifiers": {
modifiers: [
diff --git a/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt b/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt
index c0ef585..79f32c9 100644
--- a/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt
+++ b/src/test/resources/testdata/items/pets/mithril-golem-not-selected.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
components: {
"minecraft:custom_data": {
id: "PET",
diff --git a/src/test/resources/testdata/items/pets/rabbit-selected.snbt b/src/test/resources/testdata/items/pets/rabbit-selected.snbt
index 48a6f6f..d4c7235 100644
--- a/src/test/resources/testdata/items/pets/rabbit-selected.snbt
+++ b/src/test/resources/testdata/items/pets/rabbit-selected.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
components: {
"minecraft:custom_data": {
id: "PET",
diff --git a/src/test/resources/testdata/items/rune-in-sack.snbt b/src/test/resources/testdata/items/rune-in-sack.snbt
index b15488a..4624c0f 100644
--- a/src/test/resources/testdata/items/rune-in-sack.snbt
+++ b/src/test/resources/testdata/items/rune-in-sack.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
components: {
"minecraft:custom_data": {
},
diff --git a/src/test/resources/testdata/items/titanium-drill.snbt b/src/test/resources/testdata/items/titanium-drill.snbt
index e3b6819..e49c6b0 100644
--- a/src/test/resources/testdata/items/titanium-drill.snbt
+++ b/src/test/resources/testdata/items/titanium-drill.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
components: {
"minecraft:attribute_modifiers": {
modifiers: [
diff --git a/src/test/resources/testdata/tablist/dungeon_hub.snbt b/src/test/resources/testdata/tablist/dungeon_hub.snbt
new file mode 100644
index 0000000..fed57ad
--- /dev/null
+++ b/src/test/resources/testdata/tablist/dungeon_hub.snbt
@@ -0,0 +1,1170 @@
+{
+ body: [
+ {
+ extra: [
+ " ",
+ {
+ bold: 1b,
+ color: "green",
+ text: "Players "
+ },
+ {
+ color: "white",
+ text: "(15)"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "aqua",
+ text: "210"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "aqua",
+ text: "lrg89"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "light_purple",
+ text: "322"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "aqua",
+ text: "Basilickk"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "light_purple",
+ text: "330"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "aqua",
+ text: "Schauli23 "
+ },
+ {
+ color: "gray",
+ text: "Σ"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "dark_green",
+ text: "187"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "aqua",
+ text: "bombardiro13"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "yellow",
+ text: "119"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "aqua",
+ text: "Horuu"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "dark_green",
+ text: "188"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "Kirito_Hacker "
+ },
+ {
+ bold: 1b,
+ color: "gray",
+ text: "ꕁ"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "blue",
+ text: "281"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "LasseFTW1N "
+ },
+ {
+ bold: 1b,
+ color: "dark_purple",
+ text: "࿇"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "dark_aqua",
+ text: "274"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "VN_Tuan "
+ },
+ {
+ bold: 1b,
+ color: "aqua",
+ text: "ᛝ"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "aqua",
+ text: "205"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "buttonpurse_1212"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "dark_green",
+ text: "193"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "Moly____ "
+ },
+ {
+ bold: 1b,
+ color: "gray",
+ text: "⚛"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "dark_green",
+ text: "187"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "BehavingTurtle4"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "dark_green",
+ text: "169"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "Kalmaria "
+ },
+ {
+ color: "gold",
+ text: "ௐ"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "yellow",
+ text: "84"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "green",
+ text: "Cxter"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "white",
+ text: "48"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "gray",
+ text: "FredyFazballs"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "gray",
+ text: "21"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "gray",
+ text: "Finn1446"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ bold: 1b,
+ color: "green",
+ text: "Players "
+ },
+ {
+ color: "white",
+ text: "(15)"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ bold: 1b,
+ color: "dark_aqua",
+ text: "Info"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "aqua",
+ text: "Area: "
+ },
+ {
+ color: "gray",
+ text: "Dungeon Hub"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Server: ",
+ {
+ color: "dark_gray",
+ text: "mini90J"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Gems: ",
+ {
+ color: "green",
+ text: "65"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Fairy Souls: ",
+ {
+ color: "light_purple",
+ text: "7"
+ },
+ {
+ color: "dark_purple",
+ text: "/"
+ },
+ {
+ color: "light_purple",
+ text: "7"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Unclaimed chests: ",
+ {
+ color: "gold",
+ text: "0"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ text: ""
+ },
+ {
+ bold: 1b,
+ color: "yellow",
+ text: "Profile: "
+ },
+ {
+ color: "green",
+ text: "Strawberry"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " SB Level",
+ {
+ color: "white",
+ text: ": "
+ },
+ {
+ color: "dark_gray",
+ text: "["
+ },
+ {
+ color: "aqua",
+ text: "210"
+ },
+ {
+ color: "dark_gray",
+ text: "] "
+ },
+ {
+ color: "aqua",
+ text: "26"
+ },
+ {
+ color: "dark_aqua",
+ text: "/"
+ },
+ {
+ color: "aqua",
+ text: "100 XP"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Bank: ",
+ {
+ color: "gold",
+ text: "1.4B"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Interest: ",
+ {
+ color: "yellow",
+ text: "12 Hours"
+ },
+ {
+ color: "gold",
+ text: " (689.1k)"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "yellow",
+ text: "Collection:"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Bonzo IV: ",
+ {
+ color: "yellow",
+ text: "110"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "150"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Scarf II: ",
+ {
+ color: "yellow",
+ text: "25"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "50"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " The Professor IV: ",
+ {
+ color: "yellow",
+ text: "141"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "150"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Thorn I: ",
+ {
+ color: "yellow",
+ text: "29"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "50"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Livid II: ",
+ {
+ color: "yellow",
+ text: "91"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "100"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Sadan V: ",
+ {
+ color: "yellow",
+ text: "388"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "500"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Necron VI: ",
+ {
+ color: "yellow",
+ text: "531"
+ },
+ {
+ color: "gold",
+ text: "/"
+ },
+ {
+ color: "yellow",
+ text: "750"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ bold: 1b,
+ color: "dark_aqua",
+ text: "Info"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "gold",
+ text: "Dungeons:"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ color: "white",
+ text: "Catacombs 39: "
+ },
+ {
+ color: "green",
+ text: "15%"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ color: "green",
+ text: "Mage 36: "
+ },
+ {
+ color: "green",
+ text: "12.9%"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "light_purple",
+ text: "RNG Meter"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ color: "green",
+ text: "Catacombs Floor I"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " ",
+ {
+ color: "gray",
+ text: "None"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "aqua",
+ text: "Essence:"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Undead: ",
+ {
+ color: "light_purple",
+ text: "1,907"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ " Wither: ",
+ {
+ color: "light_purple",
+ text: "318"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "aqua",
+ text: "Party: "
+ },
+ {
+ color: "gray",
+ text: "No party"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ },
+ {
+ italic: 0b,
+ text: ""
+ }
+ ],
+ footer: {
+ extra: [
+ "\n",
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "green",
+ text: "Active Effects"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ {
+ color: "gray",
+ text: ""
+ },
+ {
+ color: "gray",
+ text: "You have "
+ },
+ {
+ color: "yellow",
+ text: "2 "
+ },
+ {
+ color: "gray",
+ text: 'active effects. Use "'
+ },
+ {
+ color: "gold",
+ text: "/effects"
+ },
+ {
+ color: "gray",
+ text: '" to see them!'
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ {
+ color: "yellow",
+ text: "Haste II"
+ },
+ "",
+ {
+ bold: 0b,
+ italic: 0b,
+ obfuscated: 0b,
+ strikethrough: 0b,
+ text: "",
+ underlined: 0b
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ "",
+ {
+ bold: 0b,
+ extra: [
+ "§s"
+ ],
+ italic: 0b,
+ obfuscated: 0b,
+ strikethrough: 0b,
+ text: "",
+ underlined: 0b
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ {
+ bold: 1b,
+ color: "light_purple",
+ text: "Cookie Buff"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ {
+ color: "gray",
+ text: ""
+ },
+ {
+ color: "gray",
+ text: "Not active! Obtain booster cookies from the community"
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ {
+ color: "gray",
+ text: "shop in the hub."
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ "",
+ {
+ bold: 0b,
+ extra: [
+ "§s"
+ ],
+ italic: 0b,
+ obfuscated: 0b,
+ strikethrough: 0b,
+ text: "",
+ underlined: 0b
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ "\n",
+ {
+ extra: [
+ {
+ color: "green",
+ extra: [
+ {
+ bold: 1b,
+ color: "red",
+ text: "STORE.HYPIXEL.NET"
+ }
+ ],
+ text: "Ranks, Boosters & MORE! "
+ }
+ ],
+ italic: 0b,
+ text: ""
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ header: {
+ extra: [
+ {
+ color: "aqua",
+ extra: [
+ {
+ bold: 1b,
+ color: "yellow",
+ text: "MC.HYPIXEL.NET"
+ }
+ ],
+ text: "You are playing on "
+ },
+ "\n",
+ {
+ extra: [
+ "",
+ {
+ bold: 0b,
+ extra: [
+ "§s"
+ ],
+ italic: 0b,
+ obfuscated: 0b,
+ strikethrough: 0b,
+ text: "",
+ underlined: 0b
+ }
+ ],
+ italic: 0b,
+ text: ""
+ }
+ ],
+ italic: 0b,
+ text: ""
+ },
+ source: {
+ dataVersion: 4325,
+ modVersion: "Firmament 3.1.0-dev+mc1.21.5+g2de6cfb"
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/Compat.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/Compat.kt
new file mode 100644
index 0000000..d95712b
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/Compat.kt
@@ -0,0 +1,11 @@
+package moe.nea.firmament.features.texturepack
+
+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/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt
index dc3b109..462b1e1 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomBlockTextures.kt
@@ -3,6 +3,8 @@
package moe.nea.firmament.features.texturepack
import java.util.concurrent.CompletableFuture
+import java.util.concurrent.Executor
+import java.util.function.Function
import net.fabricmc.loader.api.FabricLoader
import kotlinx.serialization.ExperimentalSerializationApi
import kotlinx.serialization.KSerializer
@@ -19,8 +21,11 @@ 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.client.render.model.Baker
+import net.minecraft.client.render.model.BlockStateModel
+import net.minecraft.client.render.model.ReferencedModelsCollector
+import net.minecraft.client.render.model.SimpleBlockStateModel
+import net.minecraft.client.render.model.json.ModelVariant
import net.minecraft.registry.RegistryKey
import net.minecraft.registry.RegistryKeys
import net.minecraft.resource.ResourceManager
@@ -28,11 +33,13 @@ import net.minecraft.resource.SinglePreparationResourceReloader
import net.minecraft.util.Identifier
import net.minecraft.util.math.BlockPos
import net.minecraft.util.profiler.Profiler
+import net.minecraft.util.thread.AsyncHelper
import moe.nea.firmament.Firmament
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.EarlyResourceReloadEvent
import moe.nea.firmament.events.FinalizeResourceManagerEvent
import moe.nea.firmament.events.SkyblockServerUpdateEvent
+import moe.nea.firmament.features.texturepack.CustomBlockTextures.createBakedModels
import moe.nea.firmament.features.texturepack.CustomGlobalTextures.logger
import moe.nea.firmament.util.IdentifierSerializer
import moe.nea.firmament.util.MC
@@ -43,249 +50,301 @@ 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)
- }
-
- 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()
- }
- })
- }
+ @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() = block.withPrefixedPath("block/")
+
+ /**
+ * Guaranteed to be set after [BakedReplacements.modelBakingFuture] is complete.
+ */
+ @Transient
+ lateinit var blockModel: BlockStateModel
+
+ @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>) {
+ /**
+ * Fulfilled by [createBakedModels] which is called during model baking. Once completed, all [Replacement.blockModel] will be set.
+ */
+ val modelBakingFuture = CompletableFuture<Unit>()
+
+ /**
+ * @returns a list of all [Replacement]s.
+ */
+ fun collectAllReplacements(): Sequence<Replacement> {
+ return data.values.asSequence()
+ .flatMap { it.lookup.values }
+ .flatten()
+ .map { it.replacement }
+ }
+ }
+
+ 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?): BlockStateModel? {
+ return getReplacement(block, blockPos)?.blockModel
+ }
+
+ @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)
+ }
+
+ 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) })
+ }
+
+ @Subscribe
+ fun onStart(event: FinalizeResourceManagerEvent) {
+ event.resourceManager.registerReloader(object :
+ SinglePreparationResourceReloader<BakedReplacements>() {
+ override fun prepare(manager: ResourceManager, profiler: Profiler): BakedReplacements {
+ return preparationFuture.join().also {
+ it.modelBakingFuture.join()
+ }
+ }
+
+ override fun apply(prepared: BakedReplacements, manager: ResourceManager, profiler: Profiler?) {
+ allLocationReplacements = prepared
+ refreshReplacements()
+ }
+ })
+ }
+
+ fun simpleBlockModel(blockId: Identifier): SimpleBlockStateModel.Unbaked {
+ // TODO: does this need to be shared between resolving and baking? I think not, but it would probably be wise to do so in the future.
+ return SimpleBlockStateModel.Unbaked(
+ ModelVariant(blockId)
+ )
+ }
+
+ /**
+ * Used by [moe.nea.firmament.init.SectionBuilderRiser]
+ */
+
+ @JvmStatic
+ fun patchIndigo(original: BlockStateModel, pos: BlockPos?, state: BlockState): BlockStateModel {
+ return getReplacementModel(state, pos) ?: original
+ }
+
+ @JvmStatic
+ fun collectExtraModels(modelsCollector: ReferencedModelsCollector) {
+ preparationFuture.join().collectAllReplacements()
+ .forEach { modelsCollector.resolve(simpleBlockModel(it.blockModelIdentifier)) }
+ }
+
+ @JvmStatic
+ fun createBakedModels(baker: Baker, executor: Executor): CompletableFuture<Void?> {
+ return preparationFuture.thenComposeAsync(Function { replacements ->
+ val byModel = replacements.collectAllReplacements().groupBy { it.blockModelIdentifier }
+ val modelBakingTask = AsyncHelper.mapValues(byModel, { blockId, replacements ->
+ val unbakedModel = SimpleBlockStateModel.Unbaked(
+ ModelVariant(blockId)
+ )
+ val baked = unbakedModel.bake(baker)
+ replacements.forEach {
+ it.blockModel = baked
+ }
+ }, executor)
+ modelBakingTask.thenAcceptAsync { replacements.modelBakingFuture.complete(Unit) }
+ }, executor)
+ }
}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt
index 85dfa32..8a2bde5 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalArmorOverrides.kt
@@ -81,20 +81,27 @@ object CustomGlobalArmorOverrides {
null,
Optional.of(RegistryKey.of(EquipmentAssetKeys.REGISTRY_KEY, model)),
Optional.empty(),
- Optional.empty(), false, false, false
+ Optional.empty(),
+ false,
+ false,
+ false,
+ false
)
}
+ // TODO: BipedEntityRenderer.getEquippedStack create copies of itemstacks for rendering. This means this cache is essentially useless
+ // If i figure out how to circumvent this (maybe track the origin of those copied itemstacks in some sort of variable in the itemstack to track back the original instance) i should reenable this cache.
+ // Then also re add this to the cache clearing function
val overrideCache =
- WeakCache.memoize<ItemStack, EquipmentSlot, Optional<EquippableComponent>>("ArmorOverrides") { stack, slot ->
- val id = stack.skyBlockId ?: return@memoize Optional.empty()
- val override = overrides[id.neuItem] ?: return@memoize Optional.empty()
+ WeakCache.dontMemoize<ItemStack, EquipmentSlot, Optional<EquippableComponent>>("ArmorOverrides") { stack, slot ->
+ val id = stack.skyBlockId ?: return@dontMemoize Optional.empty()
+ val override = overrides[id.neuItem] ?: return@dontMemoize Optional.empty()
for (suboverride in override.overrides) {
if (suboverride.predicate.test(stack)) {
- return@memoize resolveComponent(slot, suboverride.modelIdentifier).intoOptional()
+ return@dontMemoize resolveComponent(slot, suboverride.modelIdentifier).intoOptional()
}
}
- return@memoize resolveComponent(slot, override.modelIdentifier).intoOptional()
+ return@dontMemoize resolveComponent(slot, override.modelIdentifier).intoOptional()
}
var overrides: Map<String, ArmorOverride> = mapOf()
@@ -111,7 +118,7 @@ object CustomGlobalArmorOverrides {
val equipmentLayers = layers.map {
EquipmentModel.Layer(
it.identifier, if (it.tint) {
- Optional.of(EquipmentModel.Dyeable(Optional.empty()))
+ Optional.of(EquipmentModel.Dyeable(Optional.of(0xFFA06540.toInt())))
} else {
Optional.empty()
},
@@ -145,6 +152,7 @@ object CustomGlobalArmorOverrides {
null
}
}
+ bakedOverrides.clear()
val associatedMap = overrides.flatMap { obj -> obj.itemIds.map { it to obj } }
.toMap()
associatedMap.forEach { it.value.bake(manager) }
@@ -152,7 +160,6 @@ object CustomGlobalArmorOverrides {
}
override fun apply(prepared: Map<String, ArmorOverride>, manager: ResourceManager, profiler: Profiler) {
- bakedOverrides.clear()
overrides = prepared
}
})
@@ -160,11 +167,13 @@ object CustomGlobalArmorOverrides {
@JvmStatic
fun overrideArmor(itemStack: ItemStack, slot: EquipmentSlot): Optional<EquippableComponent> {
+ if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return Optional.empty()
return overrideCache.invoke(itemStack, slot)
}
@JvmStatic
fun overrideArmorLayer(id: Identifier): EquipmentModel? {
+ if (!CustomSkyBlockTextures.TConfig.enableArmorOverrides) return null
return bakedOverrides[id]
}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt
index 02c0714..403e3bd 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt
@@ -8,7 +8,6 @@ import org.slf4j.LoggerFactory
import kotlinx.serialization.Serializable
import kotlinx.serialization.UseSerializers
import kotlin.jvm.optionals.getOrNull
-import net.minecraft.client.util.ModelIdentifier
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.SinglePreparationResourceReloader
import net.minecraft.text.Text
@@ -100,12 +99,12 @@ object CustomGlobalTextures : SinglePreparationResourceReloader<CustomGlobalText
manager.getResource(Identifier.of(key.namespace, "filters/screen/${key.path}.json"))
.getOrNull()
?: return@mapNotNull runNull {
- ErrorUtil.softError("Failed to locate screen filter at $key")
+ ErrorUtil.softError("Failed to locate screen filter at $key used by ${it.value.map { it.first }}")
}
val screenFilter =
Firmament.tryDecodeJsonFromStream<ScreenFilter>(guiClassResource.inputStream)
.getOrElse { ex ->
- ErrorUtil.softError("Failed to load screen filter at $key", ex)
+ ErrorUtil.softError("Failed to load screen filter at $key used by ${it.value.map { it.first }}", ex)
return@mapNotNull null
}
ItemOverrideCollection(screenFilter, it.value.map { it.second })
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt
index fca8944..1da840d 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt
@@ -17,11 +17,14 @@ import moe.nea.firmament.features.texturepack.predicates.AndPredicate
import moe.nea.firmament.features.texturepack.predicates.CastPredicate
import moe.nea.firmament.features.texturepack.predicates.DisplayNamePredicate
import moe.nea.firmament.features.texturepack.predicates.ExtraAttributesPredicate
+import moe.nea.firmament.features.texturepack.predicates.GenericComponentPredicate
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 moe.nea.firmament.features.texturepack.predicates.PullingPredicate
+import moe.nea.firmament.features.texturepack.predicates.SkullPredicate
import moe.nea.firmament.util.json.KJsonOps
object CustomModelOverrideParser {
@@ -62,6 +65,8 @@ object CustomModelOverrideParser {
registerPredicateParser("item", ItemPredicate.Parser)
registerPredicateParser("extra_attributes", ExtraAttributesPredicate.Parser)
registerPredicateParser("pet", PetPredicate.Parser)
+ registerPredicateParser("component", GenericComponentPredicate.Parser)
+ registerPredicateParser("skull", SkullPredicate.Parser)
}
private val neverPredicate = listOf(
@@ -79,6 +84,12 @@ object CustomModelOverrideParser {
if (predicateName == "cast") { // 1.21.4
parsedPredicates.add(CastPredicate.Parser.parse(predicates[predicateName]) ?: return neverPredicate)
}
+ if (predicateName == "pull") {
+ parsedPredicates.add(PullingPredicate.Parser.parse(predicates[predicateName]) ?: return neverPredicate)
+ }
+ if (predicateName == "pulling") {
+ parsedPredicates.add(PullingPredicate.AnyPulling)
+ }
if (!predicateName.startsWith("firmament:")) continue
val identifier = Identifier.of(predicateName)
val parser = predicateParsers[identifier] ?: return neverPredicate
@@ -103,6 +114,10 @@ object CustomModelOverrideParser {
Firmament.identifier("predicates/legacy"),
PredicateModel.Unbaked.CODEC
)
+ ItemModelTypes.ID_MAPPER.put(
+ Firmament.identifier("head_model"),
+ HeadModelChooser.Unbaked.CODEC
+ )
}
}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomScreenLayouts.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomScreenLayouts.kt
new file mode 100644
index 0000000..4785e90
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomScreenLayouts.kt
@@ -0,0 +1,224 @@
+package moe.nea.firmament.features.texturepack
+
+import kotlinx.serialization.SerialName
+import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
+import net.minecraft.client.font.TextRenderer
+import net.minecraft.client.gui.DrawContext
+import net.minecraft.client.gui.screen.Screen
+import net.minecraft.client.gui.screen.ingame.HandledScreen
+import net.minecraft.client.render.RenderLayer
+import net.minecraft.registry.Registries
+import net.minecraft.resource.ResourceManager
+import net.minecraft.resource.SinglePreparationResourceReloader
+import net.minecraft.screen.slot.Slot
+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.events.ScreenChangeEvent
+import moe.nea.firmament.features.texturepack.CustomScreenLayouts.Alignment.CENTER
+import moe.nea.firmament.features.texturepack.CustomScreenLayouts.Alignment.LEFT
+import moe.nea.firmament.features.texturepack.CustomScreenLayouts.Alignment.RIGHT
+import moe.nea.firmament.mixins.accessor.AccessorHandledScreen
+import moe.nea.firmament.util.ErrorUtil.intoCatch
+import moe.nea.firmament.util.IdentifierSerializer
+
+object CustomScreenLayouts : SinglePreparationResourceReloader<List<CustomScreenLayouts.CustomScreenLayout>>() {
+
+ @Serializable
+ data class CustomScreenLayout(
+ val predicates: Preds,
+ val background: BackgroundReplacer? = null,
+ val slots: List<SlotReplacer> = listOf(),
+ val playerTitle: TitleReplacer? = null,
+ val containerTitle: TitleReplacer? = null,
+ val repairCostTitle: TitleReplacer? = null,
+ val nameField: ComponentMover? = null,
+ )
+
+ @Serializable
+ data class ComponentMover(
+ val x: Int,
+ val y: Int,
+ val width: Int? = null,
+ val height: Int? = null,
+ )
+
+ @Serializable
+ data class Preds(
+ val label: StringMatcher,
+ @Serializable(with = IdentifierSerializer::class)
+ val screenType: Identifier? = null,
+ ) {
+ fun matches(screen: Screen): Boolean {
+ // TODO: does this deserve the restriction to handled screen
+ val s = screen as? HandledScreen<*>? ?: return false
+ val typeMatches = screenType == null || s.screenHandler.type.equals(Registries.SCREEN_HANDLER
+ .get(screenType));
+
+ return label.matches(s.title) && typeMatches
+ }
+ }
+
+ @Serializable
+ data class BackgroundReplacer(
+ @Serializable(with = IdentifierSerializer::class)
+ val texture: Identifier,
+ // TODO: allow selectively still rendering some components (recipe button, trade backgrounds, furnace flame progress, arrows)
+ val x: Int,
+ val y: Int,
+ val width: Int,
+ val height: Int,
+ ) {
+ fun renderGeneric(context: DrawContext, screen: HandledScreen<*>) {
+ screen as AccessorHandledScreen
+ val originalX: Int = (screen.width - screen.backgroundWidth_Firmament) / 2
+ val originalY: Int = (screen.height - screen.backgroundHeight_Firmament) / 2
+ val modifiedX = originalX + this.x
+ val modifiedY = originalY + this.y
+ val textureWidth = this.width
+ val textureHeight = this.height
+ context.drawTexture(
+ RenderLayer::getGuiTextured,
+ this.texture,
+ modifiedX,
+ modifiedY,
+ 0.0f,
+ 0.0f,
+ textureWidth,
+ textureHeight,
+ textureWidth,
+ textureHeight
+ )
+
+ }
+ }
+
+ @Serializable
+ data class SlotReplacer(
+ // TODO: override getRecipeBookButtonPos as well
+ // TODO: is this index or id (i always forget which one is duplicated per inventory)
+ val index: Int,
+ val x: Int,
+ val y: Int,
+ ) {
+ fun move(slots: List<Slot>) {
+ val slot = slots.getOrNull(index) ?: return
+ slot.x = x
+ slot.y = y
+ }
+ }
+
+ @Serializable
+ enum class Alignment {
+ @SerialName("left")
+ LEFT,
+
+ @SerialName("center")
+ CENTER,
+
+ @SerialName("right")
+ RIGHT
+ }
+
+ @Serializable
+ data class TitleReplacer(
+ val x: Int? = null,
+ val y: Int? = null,
+ val align: Alignment = Alignment.LEFT,
+ val replace: String? = null
+ ) {
+ @Transient
+ val replacedText: Text? = replace?.let(Text::literal)
+
+ fun replaceText(text: Text): Text {
+ if (replacedText != null) return replacedText
+ return text
+ }
+
+ fun replaceY(y: Int): Int {
+ return this.y ?: y
+ }
+
+ fun replaceX(font: TextRenderer, text: Text, x: Int): Int {
+ val baseX = this.x ?: x
+ return baseX + when (this.align) {
+ LEFT -> 0
+ CENTER -> -font.getWidth(text) / 2
+ RIGHT -> -font.getWidth(text)
+ }
+ }
+
+ /**
+ * Not technically part of the package, but it does allow for us to later on seamlessly integrate a color option into this class as well
+ */
+ fun replaceColor(text: Text, color: Int): Int {
+ return CustomTextColors.mapTextColor(text, color)
+ }
+ }
+
+
+ @Subscribe
+ fun onStart(event: FinalizeResourceManagerEvent) {
+ event.resourceManager.registerReloader(CustomScreenLayouts)
+ }
+
+ override fun prepare(
+ manager: ResourceManager,
+ profiler: Profiler
+ ): List<CustomScreenLayout> {
+ val allScreenLayouts = manager.findResources(
+ "overrides/screen_layout",
+ { it.path.endsWith(".json") && it.namespace == "firmskyblock" })
+ val allParsedLayouts = allScreenLayouts.mapNotNull { (path, stream) ->
+ Firmament.tryDecodeJsonFromStream<CustomScreenLayout>(stream.inputStream)
+ .intoCatch("Could not read custom screen layout from $path").orNull()
+ }
+ return allParsedLayouts
+ }
+
+ var customScreenLayouts = listOf<CustomScreenLayout>()
+
+ override fun apply(
+ prepared: List<CustomScreenLayout>,
+ manager: ResourceManager?,
+ profiler: Profiler?
+ ) {
+ this.customScreenLayouts = prepared
+ }
+
+ @get:JvmStatic
+ var activeScreenOverride = null as CustomScreenLayout?
+
+ val DO_NOTHING_TEXT_REPLACER = TitleReplacer()
+
+ @JvmStatic
+ fun <T>getMover(selector: (CustomScreenLayout)-> (T?)) =
+ activeScreenOverride?.let(selector)
+
+ @JvmStatic
+ fun getTextMover(selector: (CustomScreenLayout) -> (TitleReplacer?)) =
+ getMover(selector) ?: DO_NOTHING_TEXT_REPLACER
+
+ @Subscribe
+ fun onScreenOpen(event: ScreenChangeEvent) {
+ if (!CustomSkyBlockTextures.TConfig.allowLayoutChanges) {
+ activeScreenOverride = null
+ return
+ }
+ activeScreenOverride = event.new?.let { screen ->
+ customScreenLayouts.find { it.predicates.matches(screen) }
+ }
+
+ val screen = event.new as? HandledScreen<*> ?: return
+ val handler = screen.screenHandler
+ activeScreenOverride?.let { override ->
+ override.slots.forEach { slotReplacer ->
+ slotReplacer.move(handler.slots)
+ }
+ }
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt
index d9ca5b4..18949ff 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt
@@ -3,7 +3,6 @@ package moe.nea.firmament.features.texturepack
import com.mojang.authlib.minecraft.MinecraftProfileTexture
import com.mojang.authlib.properties.Property
import java.util.Optional
-import org.jetbrains.annotations.Nullable
import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable
import kotlin.jvm.optionals.getOrNull
import net.minecraft.block.SkullBlock
@@ -34,8 +33,10 @@ object CustomSkyBlockTextures : FirmamentFeature {
val enableModelOverrides by toggle("model-overrides") { true }
val enableArmorOverrides by toggle("armor-overrides") { true }
val enableBlockOverrides by toggle("block-overrides") { true }
+ val enableLegacyMinecraftCompat by toggle("legacy-minecraft-path-support") { true }
val enableLegacyCIT by toggle("legacy-cit") { true }
val allowRecoloringUiText by toggle("recolor-text") { true }
+ val allowLayoutChanges by toggle("screen-layouts") { true }
}
override val config: ManagedConfig
@@ -44,7 +45,8 @@ object CustomSkyBlockTextures : FirmamentFeature {
val allItemCaches by lazy {
listOf(
skullTextureCache.cache,
- CustomGlobalArmorOverrides.overrideCache.cache
+ CustomItemModelEvent.cache.cache,
+ // TODO: re-add this once i figure out how to make the cache useful again CustomGlobalArmorOverrides.overrideCache.cache
)
}
@@ -75,7 +77,7 @@ object CustomSkyBlockTextures : FirmamentFeature {
fun onCustomModelId(it: CustomItemModelEvent) {
if (!TConfig.enabled) return
val id = it.itemStack.skyBlockId ?: return
- it.overrideIfExists(Identifier.of("firmskyblock", id.identifier.path))
+ it.overrideIfEmpty(Identifier.of("firmskyblock", id.identifier.path))
}
private val skullTextureCache =
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt
index 4ca1796..3ac895a 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomTextColors.kt
@@ -2,6 +2,7 @@ package moe.nea.firmament.features.texturepack
import java.util.Optional
import kotlinx.serialization.Serializable
+import kotlinx.serialization.Transient
import kotlin.jvm.optionals.getOrNull
import net.minecraft.resource.ResourceManager
import net.minecraft.resource.SinglePreparationResourceReloader
@@ -18,12 +19,25 @@ object CustomTextColors : SinglePreparationResourceReloader<CustomTextColors.Tex
data class TextOverrides(
val defaultColor: Int,
val overrides: List<TextOverride> = listOf()
- )
+ ) {
+ /**
+ * Stub custom text color to allow always returning a text override
+ */
+ @Transient
+ val baseOverride = TextOverride(
+ StringMatcher.Equals("", false),
+ defaultColor,
+ 0,
+ 0
+ )
+ }
@Serializable
data class TextOverride(
val predicate: StringMatcher,
val override: Int,
+ val x: Int = 0,
+ val y: Int = 0,
)
@Subscribe
@@ -31,14 +45,14 @@ object CustomTextColors : SinglePreparationResourceReloader<CustomTextColors.Tex
event.resourceManager.registerReloader(this)
}
- val cache = WeakCache.memoize<Text, Optional<Int>>("CustomTextColor") { text ->
+ val cache = WeakCache.memoize<Text, Optional<TextOverride>>("CustomTextColor") { text ->
val override = textOverrides ?: return@memoize Optional.empty()
- Optional.of(override.overrides.find { it.predicate.matches(text) }?.override ?: override.defaultColor)
+ Optional.ofNullable(override.overrides.find { it.predicate.matches(text) })
}
fun mapTextColor(text: Text, oldColor: Int): Int {
- if (textOverrides == null) return oldColor
- return cache(text).getOrNull() ?: oldColor
+ val override = cache(text).orElse(null)
+ return override?.override ?: textOverrides?.defaultColor ?: oldColor
}
override fun prepare(
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt
index d11fec0..e020d66 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/FirmamentModelPredicate.kt
@@ -1,8 +1,11 @@
-
package moe.nea.firmament.features.texturepack
+import kotlinx.serialization.Serializable
+import net.minecraft.entity.LivingEntity
import net.minecraft.item.ItemStack
+@Serializable(with = FirmamentRootPredicateSerializer::class)
interface FirmamentModelPredicate {
- fun test(stack: ItemStack): Boolean
+ fun test(stack: ItemStack, holder: LivingEntity?): Boolean = test(stack)
+ fun test(stack: ItemStack): Boolean = test(stack, null)
}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt
new file mode 100644
index 0000000..3e8cc4e
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/HeadModelChooser.kt
@@ -0,0 +1,90 @@
+package moe.nea.firmament.features.texturepack
+
+import com.google.gson.JsonObject
+import com.mojang.serialization.MapCodec
+import com.mojang.serialization.codecs.RecordCodecBuilder
+import net.minecraft.client.item.ItemModelManager
+import net.minecraft.client.render.item.ItemRenderState
+import net.minecraft.client.render.item.model.BasicItemModel
+import net.minecraft.client.render.item.model.ItemModel
+import net.minecraft.client.render.item.model.ItemModelTypes
+import net.minecraft.client.render.model.ResolvableModel
+import net.minecraft.client.world.ClientWorld
+import net.minecraft.entity.LivingEntity
+import net.minecraft.item.ItemDisplayContext
+import net.minecraft.item.ItemStack
+import net.minecraft.util.Identifier
+
+object HeadModelChooser {
+ val IS_CHOOSING_HEAD_MODEL = ThreadLocal.withInitial { false }
+
+ interface HasExplicitHeadModelMarker {
+ fun markExplicitHead_Firmament()
+ fun isExplicitHeadModel_Firmament(): Boolean
+ companion object{
+ @JvmStatic
+ fun cast(state: ItemRenderState) = state as HasExplicitHeadModelMarker
+ }
+ }
+
+ data class Baked(val head: ItemModel, val regular: ItemModel) : ItemModel {
+ override fun update(
+ state: ItemRenderState,
+ stack: ItemStack?,
+ resolver: ItemModelManager?,
+ displayContext: ItemDisplayContext,
+ world: ClientWorld?,
+ user: LivingEntity?,
+ seed: Int
+ ) {
+ val instance =
+ if (IS_CHOOSING_HEAD_MODEL.get()) {
+ HasExplicitHeadModelMarker.cast(state).markExplicitHead_Firmament()
+ head
+ } else {
+ regular
+ }
+ instance.update(state, stack, resolver, displayContext, world, user, seed)
+ }
+ }
+
+ data class Unbaked(
+ val head: ItemModel.Unbaked,
+ val regular: ItemModel.Unbaked,
+ ) : ItemModel.Unbaked {
+ override fun getCodec(): MapCodec<out ItemModel.Unbaked> {
+ return CODEC
+ }
+
+ override fun bake(context: ItemModel.BakeContext): ItemModel {
+ return Baked(
+ head.bake(context),
+ regular.bake(context)
+ )
+ }
+
+ override fun resolve(resolver: ResolvableModel.Resolver) {
+ head.resolve(resolver)
+ regular.resolve(resolver)
+ }
+
+ companion object {
+ @JvmStatic
+ fun fromLegacyJson(jsonObject: JsonObject, unbakedModel: ItemModel.Unbaked): ItemModel.Unbaked {
+ val model = jsonObject["firmament:head_model"] ?: return unbakedModel
+ val modelUrl = model.asJsonPrimitive.asString
+ val headModel = BasicItemModel.Unbaked(Identifier.of(modelUrl), listOf())
+ return Unbaked(headModel, unbakedModel)
+ }
+
+ val CODEC = RecordCodecBuilder.mapCodec {
+ it.group(
+ ItemModelTypes.CODEC.fieldOf("head")
+ .forGetter(Unbaked::head),
+ ItemModelTypes.CODEC.fieldOf("regular")
+ .forGetter(Unbaked::regular),
+ ).apply(it, ::Unbaked)
+ }
+ }
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt
index b52e96b..e6b5bcf 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/PredicateModel.kt
@@ -9,12 +9,11 @@ import net.minecraft.client.render.item.ItemRenderState
import net.minecraft.client.render.item.model.BasicItemModel
import net.minecraft.client.render.item.model.ItemModel
import net.minecraft.client.render.item.model.ItemModelTypes
-import net.minecraft.client.render.item.tint.TintSource
import net.minecraft.client.render.model.ResolvableModel
import net.minecraft.client.world.ClientWorld
import net.minecraft.entity.LivingEntity
+import net.minecraft.item.ItemDisplayContext
import net.minecraft.item.ItemStack
-import net.minecraft.item.ModelTransformationMode
import net.minecraft.util.Identifier
import moe.nea.firmament.features.texturepack.predicates.AndPredicate
@@ -29,20 +28,20 @@ class PredicateModel {
)
override fun update(
- state: ItemRenderState,
+ state: ItemRenderState?,
stack: ItemStack,
- resolver: ItemModelManager,
- transformationMode: ModelTransformationMode,
+ resolver: ItemModelManager?,
+ displayContext: ItemDisplayContext?,
world: ClientWorld?,
user: LivingEntity?,
seed: Int
) {
val model =
overrides
- .find { it.predicate.test(stack) }
+ .findLast { it.predicate.test(stack, user) }
?.model
?: fallback
- model.update(state, stack, resolver, transformationMode, world, user, seed)
+ model.update(state, stack, resolver, displayContext, world, user, seed)
}
}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt
index 2b13284..dd28d9f 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/StringMatcher.kt
@@ -13,6 +13,7 @@ import kotlinx.serialization.Serializable
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
+import kotlin.jvm.optionals.getOrNull
import net.minecraft.nbt.NbtString
import net.minecraft.text.Text
import moe.nea.firmament.util.MC
@@ -26,7 +27,7 @@ interface StringMatcher {
}
fun matches(nbt: NbtString): Boolean {
- val string = nbt.asString()
+ val string = nbt.value
val jsonStart = string.indexOf('{')
val stringStart = string.indexOf('"')
val isString = stringStart >= 0 && string.subSequence(0, stringStart).isBlank()
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt
index 99abaaa..70eb814 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/AndPredicate.kt
@@ -3,15 +3,16 @@ package moe.nea.firmament.features.texturepack.predicates
import com.google.gson.JsonArray
import com.google.gson.JsonElement
import com.google.gson.JsonObject
+import net.minecraft.entity.LivingEntity
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) }
- }
+ override fun test(stack: ItemStack, holder: LivingEntity?): Boolean {
+ return children.all { it.test(stack, holder) }
+ }
object Parser : FirmamentModelPredicateParser {
override fun parse(jsonElement: JsonElement): FirmamentModelPredicate {
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt
index 7ccaadf..321f87c 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt
@@ -1,10 +1,11 @@
package moe.nea.firmament.features.texturepack.predicates
import com.google.gson.JsonElement
+import net.minecraft.entity.LivingEntity
+import net.minecraft.entity.player.PlayerEntity
import net.minecraft.item.ItemStack
import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
-import moe.nea.firmament.util.MC
class CastPredicate : FirmamentModelPredicate {
object Parser : FirmamentModelPredicateParser {
@@ -14,7 +15,11 @@ class CastPredicate : FirmamentModelPredicate {
}
}
+ override fun test(stack: ItemStack, holder: LivingEntity?): Boolean {
+ return (holder as? PlayerEntity)?.fishHook != null && holder.mainHandStack === stack
+ }
+
override fun test(stack: ItemStack): Boolean {
- return MC.player?.fishHook != null // TODO pass through more of the model predicate context
+ return false
}
}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt
index 3c8023d..8115739 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ExtraAttributesPredicate.kt
@@ -1,215 +1,220 @@
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 kotlin.jvm.optionals.getOrDefault
+import kotlin.jvm.optionals.getOrNull
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
+import moe.nea.firmament.util.mc.NbtPrism
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)"
- }
- }
+ 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.asString().getOrNull() == string
+ }
+
+ override fun toString(): String {
+ return "MatchNbtStringExactly($string)"
+ }
+ }
+
+ class MatchString(val string: StringMatcher) : NbtMatcher {
+ override fun matches(nbt: NbtElement): Boolean {
+ return nbt.asString().map(string::matches).getOrDefault(false)
+ }
+
+ override fun toString(): String {
+ return "MatchNbtString($string)"
+ }
+ }
}
data class ExtraAttributesPredicate(
@@ -217,55 +222,20 @@ data class ExtraAttributesPredicate(
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) }
- }
+ object Parser : FirmamentModelPredicateParser {
+ override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? {
+ if (jsonElement !is JsonObject) return null
+ val path = jsonElement.get("path") ?: return null
+ val prism = NbtPrism.fromElement(path) ?: return null
+ val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement)
+ ?: return null
+ return ExtraAttributesPredicate(prism, 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/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt
new file mode 100644
index 0000000..71392ef
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/GenericComponentPredicate.kt
@@ -0,0 +1,58 @@
+package moe.nea.firmament.features.texturepack.predicates
+
+import com.google.gson.JsonElement
+import com.google.gson.JsonObject
+import com.mojang.serialization.Codec
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.component.ComponentType
+import net.minecraft.component.type.NbtComponent
+import net.minecraft.entity.LivingEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.nbt.NbtOps
+import net.minecraft.registry.RegistryKey
+import net.minecraft.registry.RegistryKeys
+import net.minecraft.util.Identifier
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
+import moe.nea.firmament.util.MC
+import moe.nea.firmament.util.mc.NbtPrism
+
+data class GenericComponentPredicate<T>(
+ val componentType: ComponentType<T>,
+ val codec: Codec<T>,
+ val path: NbtPrism,
+ val matcher: NbtMatcher,
+) : FirmamentModelPredicate {
+ constructor(componentType: ComponentType<T>, path: NbtPrism, matcher: NbtMatcher)
+ : this(componentType, componentType.codecOrThrow, path, matcher)
+
+ override fun test(stack: ItemStack, holder: LivingEntity?): Boolean {
+ val component = stack.get(componentType) ?: return false
+ // TODO: cache this
+ val nbt =
+ if (component is NbtComponent) component.nbt
+ else codec.encodeStart(NbtOps.INSTANCE, component)
+ .resultOrPartial().getOrNull() ?: return false
+ return path.access(nbt).any { matcher.matches(it) }
+ }
+
+ object Parser : FirmamentModelPredicateParser {
+ override fun parse(jsonElement: JsonElement): GenericComponentPredicate<*>? {
+ if (jsonElement !is JsonObject) return null
+ val path = jsonElement.get("path") ?: return null
+ val prism = NbtPrism.fromElement(path) ?: return null
+ val matcher = NbtMatcher.Parser.parse(jsonElement.get("match") ?: jsonElement)
+ ?: return null
+ val component = MC.currentOrDefaultRegistries
+ .getOrThrow(RegistryKeys.DATA_COMPONENT_TYPE)
+ .getOrThrow(
+ RegistryKey.of(
+ RegistryKeys.DATA_COMPONENT_TYPE,
+ Identifier.of(jsonElement.get("component").asString)
+ )
+ ).value()
+ return GenericComponentPredicate(component, prism, matcher)
+ }
+ }
+
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt
index 3cb80c7..4833dc0 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/ItemPredicate.kt
@@ -17,7 +17,7 @@ class ItemPredicate(
val item: Item
) : FirmamentModelPredicate {
override fun test(stack: ItemStack): Boolean {
- return stack.item == item
+ return stack.isOf(item)
}
object Parser : FirmamentModelPredicateParser {
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PullingPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PullingPredicate.kt
new file mode 100644
index 0000000..fa46a70
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/PullingPredicate.kt
@@ -0,0 +1,26 @@
+package moe.nea.firmament.features.texturepack.predicates
+
+import com.google.gson.JsonElement
+import net.minecraft.entity.LivingEntity
+import net.minecraft.item.BowItem
+import net.minecraft.item.ItemStack
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
+
+class PullingPredicate(val percentage: Double) : FirmamentModelPredicate {
+ companion object {
+ val AnyPulling = PullingPredicate(0.1)
+ }
+
+ object Parser : FirmamentModelPredicateParser {
+ override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? {
+ return PullingPredicate(jsonElement.asDouble)
+ }
+ }
+
+ override fun test(stack: ItemStack, holder: LivingEntity?): Boolean {
+ if (holder == null) return false
+ return BowItem.getPullProgress(holder.itemUseTime) >= percentage
+ }
+
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/SkullPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/SkullPredicate.kt
new file mode 100644
index 0000000..416e86c
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/SkullPredicate.kt
@@ -0,0 +1,63 @@
+package moe.nea.firmament.features.texturepack.predicates
+
+import com.google.gson.JsonElement
+import com.mojang.authlib.minecraft.MinecraftProfileTexture
+import java.util.UUID
+import kotlin.jvm.optionals.getOrNull
+import net.minecraft.component.DataComponentTypes
+import net.minecraft.entity.LivingEntity
+import net.minecraft.item.ItemStack
+import net.minecraft.item.Items
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicate
+import moe.nea.firmament.features.texturepack.FirmamentModelPredicateParser
+import moe.nea.firmament.features.texturepack.StringMatcher
+import moe.nea.firmament.util.mc.decodeProfileTextureProperty
+import moe.nea.firmament.util.parsePotentiallyDashlessUUID
+
+class SkullPredicate(
+ val profileId: UUID?,
+ val textureProfileId: UUID?,
+ val skinUrl: StringMatcher?,
+ val textureValue: StringMatcher?,
+) : FirmamentModelPredicate {
+ object Parser : FirmamentModelPredicateParser {
+ override fun parse(jsonElement: JsonElement): FirmamentModelPredicate? {
+ val obj = jsonElement.asJsonObject
+ val profileId = obj.getAsJsonPrimitive("profileId")
+ ?.asString?.let(::parsePotentiallyDashlessUUID)
+ val textureProfileId = obj.getAsJsonPrimitive("textureProfileId")
+ ?.asString?.let(::parsePotentiallyDashlessUUID)
+ val textureValue = obj.get("textureValue")?.let(StringMatcher::parse)
+ val skinUrl = obj.get("skinUrl")?.let(StringMatcher::parse)
+ return SkullPredicate(profileId, textureProfileId, skinUrl, textureValue)
+ }
+ }
+
+ override fun test(stack: ItemStack, holder: LivingEntity?): Boolean {
+ if (!stack.isOf(Items.PLAYER_HEAD)) return false
+ val profile = stack.get(DataComponentTypes.PROFILE) ?: return false
+ val textureProperty = profile.properties["textures"].firstOrNull()
+ val textureMode = lazy(LazyThreadSafetyMode.NONE) {
+ decodeProfileTextureProperty(textureProperty ?: return@lazy null)
+ }
+ when {
+ profileId != null
+ && profileId != profile.id.getOrNull() ->
+ return false
+
+ textureValue != null
+ && !textureValue.matches(textureProperty?.value ?: "") ->
+ return false
+
+ skinUrl != null
+ && !skinUrl.matches(textureMode.value?.textures?.get(MinecraftProfileTexture.Type.SKIN)?.url ?: "") ->
+ return false
+
+ textureProfileId != null
+ && textureProfileId != textureMode.value?.profileId ->
+ return false
+
+ else -> return true
+ }
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/BuildExtraBlockStateModels.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/BuildExtraBlockStateModels.java
new file mode 100644
index 0000000..6b3c929
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/BuildExtraBlockStateModels.java
@@ -0,0 +1,24 @@
+package moe.nea.firmament.mixins.custommodels;
+
+import com.llamalad7.mixinextras.injector.ModifyReturnValue;
+import com.llamalad7.mixinextras.sugar.Local;
+import moe.nea.firmament.features.texturepack.CustomBlockTextures;
+import net.minecraft.client.render.model.Baker;
+import net.minecraft.client.render.model.ModelBaker;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import java.util.concurrent.CompletableFuture;
+import java.util.concurrent.Executor;
+
+@Mixin(ModelBaker.class)
+public class BuildExtraBlockStateModels {
+ @ModifyReturnValue(method = "bake", at = @At("RETURN"))
+ private CompletableFuture<ModelBaker.BakedModels> injectMoreBlockModels(CompletableFuture<ModelBaker.BakedModels> original, @Local ModelBaker.BakerImpl baker, @Local(argsOnly = true) Executor executor) {
+ Baker b = baker;
+ return original.thenCombine(
+ CustomBlockTextures.createBakedModels(b, executor),
+ (a, _void) -> a
+ );
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/InsertExtraBlockModelDependencies.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/InsertExtraBlockModelDependencies.java
new file mode 100644
index 0000000..91779e7
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/InsertExtraBlockModelDependencies.java
@@ -0,0 +1,28 @@
+package moe.nea.firmament.mixins.custommodels;
+
+import com.llamalad7.mixinextras.sugar.Local;
+import moe.nea.firmament.features.texturepack.CustomBlockTextures;
+import net.minecraft.client.item.ItemAssetsLoader;
+import net.minecraft.client.render.model.BakedModelManager;
+import net.minecraft.client.render.model.BlockStatesLoader;
+import net.minecraft.client.render.model.ReferencedModelsCollector;
+import net.minecraft.client.render.model.UnbakedModel;
+import net.minecraft.util.Identifier;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+import java.util.Map;
+
+@Mixin(BakedModelManager.class)
+public class InsertExtraBlockModelDependencies {
+ @Inject(method = "collect", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/model/ReferencedModelsCollector;addSpecialModel(Lnet/minecraft/util/Identifier;Lnet/minecraft/client/render/model/UnbakedModel;)V", shift = At.Shift.AFTER))
+ private static void insertExtraModels(
+ Map<Identifier, UnbakedModel> modelMap,
+ BlockStatesLoader.LoadedModels stateDefinition,
+ ItemAssetsLoader.Result result,
+ CallbackInfoReturnable cir, @Local ReferencedModelsCollector modelsCollector) {
+ CustomBlockTextures.collectExtraModels(modelsCollector);
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ItemRenderStateExtraInfo.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ItemRenderStateExtraInfo.java
new file mode 100644
index 0000000..2872dd1
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ItemRenderStateExtraInfo.java
@@ -0,0 +1,28 @@
+package moe.nea.firmament.mixins.custommodels;
+
+import moe.nea.firmament.features.texturepack.HeadModelChooser;
+import net.minecraft.client.render.item.ItemRenderState;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(ItemRenderState.class)
+public class ItemRenderStateExtraInfo implements HeadModelChooser.HasExplicitHeadModelMarker {
+ boolean hasExplicitHead_firmament = false;
+
+ @Inject(method = "clear", at = @At("HEAD"))
+ private void clear(CallbackInfo ci) {
+ hasExplicitHead_firmament = false;
+ }
+
+ @Override
+ public void markExplicitHead_Firmament() {
+ hasExplicitHead_firmament = true;
+ }
+
+ @Override
+ public boolean isExplicitHeadModel_Firmament() {
+ return hasExplicitHead_firmament;
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java
index 81ea6cd..951e3be 100644
--- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyArmorLayerSupport.java
@@ -1,23 +1,22 @@
package moe.nea.firmament.mixins.custommodels;
-import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
-import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import moe.nea.firmament.features.texturepack.CustomGlobalArmorOverrides;
import net.minecraft.client.render.entity.equipment.EquipmentModel;
import net.minecraft.client.render.entity.equipment.EquipmentModelLoader;
-import net.minecraft.client.render.entity.equipment.EquipmentRenderer;
import net.minecraft.item.equipment.EquipmentAsset;
import net.minecraft.registry.RegistryKey;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
// TODO: auto import legacy models, maybe!!! in a later patch tho
-@Mixin(EquipmentRenderer.class)
+@Mixin(EquipmentModelLoader.class)
public class PatchLegacyArmorLayerSupport {
- @WrapOperation(method = "render(Lnet/minecraft/client/render/entity/equipment/EquipmentModel$LayerType;Lnet/minecraft/registry/RegistryKey;Lnet/minecraft/client/model/Model;Lnet/minecraft/item/ItemStack;Lnet/minecraft/client/util/math/MatrixStack;Lnet/minecraft/client/render/VertexConsumerProvider;ILnet/minecraft/util/Identifier;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/entity/equipment/EquipmentModelLoader;get(Lnet/minecraft/registry/RegistryKey;)Lnet/minecraft/client/render/entity/equipment/EquipmentModel;"))
- private EquipmentModel patchModelLayers(EquipmentModelLoader instance, RegistryKey<EquipmentAsset> assetKey, Operation<EquipmentModel> original) {
+ @Inject(method = "get", at = @At(value = "HEAD"), cancellable = true)
+ private void patchModelLayers(RegistryKey<EquipmentAsset> assetKey, CallbackInfoReturnable<EquipmentModel> cir) {
var modelOverride = CustomGlobalArmorOverrides.overrideArmorLayer(assetKey.getValue());
- if (modelOverride != null) return modelOverride;
- return original.call(instance, assetKey);
+ if (modelOverride != null)
+ cir.setReturnValue(modelOverride);
}
}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java
new file mode 100644
index 0000000..0fb6bf8
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java
@@ -0,0 +1,37 @@
+package moe.nea.firmament.mixins.custommodels;
+
+
+import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures;
+import moe.nea.firmament.util.MC;
+import net.minecraft.client.render.entity.equipment.EquipmentModel;
+import net.minecraft.util.Identifier;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+@Mixin(EquipmentModel.Layer.class)
+public class PatchLegacyTexturePathsIntoArmorLayers {
+ @Shadow
+ @Final
+ private Identifier textureId;
+
+ @Inject(method = "getFullTextureId", at = @At("HEAD"), cancellable = true)
+ private void replaceWith1201TextureIfExists(EquipmentModel.LayerType layerType, CallbackInfoReturnable<Identifier> cir) {
+ if (!CustomSkyBlockTextures.TConfig.INSTANCE.getEnableLegacyMinecraftCompat())
+ return;
+ var resourceManager = MC.INSTANCE.getResourceManager();
+ // legacy format: "assets/{identifier.namespace}/textures/models/armor/{identifier.path}_layer_{isLegs ? 2 : 1}{suffix}.png"
+ // suffix is sadly not available to us here. this means leather armor will look a bit shite
+ var legacyIdentifier = this.textureId.withPath((textureName) -> {
+ return "textures/models/armor/" + textureName + "_layer_" +
+ (layerType == EquipmentModel.LayerType.HUMANOID_LEGGINGS ? 2 : 1)
+ + ".png";
+ });
+ if (resourceManager.getResource(legacyIdentifier).isPresent()) {
+ cir.setReturnValue(legacyIdentifier);
+ }
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java
index f9a1d0d..95e7dce 100644
--- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockHitSoundPatch.java
@@ -16,7 +16,8 @@ import org.spongepowered.asm.mixin.injection.At;
@Mixin(ClientPlayerInteractionManager.class)
public class ReplaceBlockHitSoundPatch {
- @WrapOperation(method = "updateBlockBreakingProgress", at = @At(value = "NEW", target = "(Lnet/minecraft/sound/SoundEvent;Lnet/minecraft/sound/SoundCategory;FFLnet/minecraft/util/math/random/Random;Lnet/minecraft/util/math/BlockPos;)Lnet/minecraft/client/sound/PositionedSoundInstance;"))
+ @WrapOperation(method = "updateBlockBreakingProgress",
+ at = @At(value = "NEW", target = "(Lnet/minecraft/sound/SoundEvent;Lnet/minecraft/sound/SoundCategory;FFLnet/minecraft/util/math/random/Random;Lnet/minecraft/util/math/BlockPos;)Lnet/minecraft/client/sound/PositionedSoundInstance;"))
private PositionedSoundInstance replaceSound(
SoundEvent sound, SoundCategory category, float volume, float pitch,
Random random, BlockPos pos, Operation<PositionedSoundInstance> original,
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java
index 711b2af..8d2ba38 100644
--- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceBlockRenderManagerBlockModel.java
@@ -5,34 +5,33 @@ import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Local;
import moe.nea.firmament.features.texturepack.CustomBlockTextures;
import net.minecraft.block.BlockState;
-import net.minecraft.client.render.block.BlockModels;
import net.minecraft.client.render.block.BlockRenderManager;
-import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.chunk.SectionBuilder;
+import net.minecraft.client.render.model.BlockStateModel;
import net.minecraft.util.math.BlockPos;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
-@Mixin(BlockRenderManager.class)
+@Mixin(SectionBuilder.class)
public class ReplaceBlockRenderManagerBlockModel {
- @WrapOperation(method = "renderBlock", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockRenderManager;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;"))
- private BakedModel replaceModelInRenderBlock(
- BlockRenderManager instance, BlockState state, Operation<BakedModel> original, @Local(argsOnly = true) BlockPos pos) {
- var replacement = CustomBlockTextures.getReplacementModel(state, pos);
- if (replacement != null) return replacement;
- CustomBlockTextures.enterFallbackCall();
- var fallback = original.call(instance, state);
- CustomBlockTextures.exitFallbackCall();
- return fallback;
- }
-
- @WrapOperation(method = "renderDamage", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockModels;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;"))
- private BakedModel replaceModelInRenderDamage(
- BlockModels instance, BlockState state, Operation<BakedModel> original, @Local(argsOnly = true) BlockPos pos) {
- var replacement = CustomBlockTextures.getReplacementModel(state, pos);
- if (replacement != null) return replacement;
- CustomBlockTextures.enterFallbackCall();
- var fallback = original.call(instance, state);
- CustomBlockTextures.exitFallbackCall();
- return fallback;
- }
+ @WrapOperation(method = "build", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockRenderManager;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BlockStateModel;"))
+ private BlockStateModel replaceModelInRenderBlock(BlockRenderManager instance, BlockState state, Operation<BlockStateModel> original, @Local(ordinal = 2) BlockPos pos) {
+ var replacement = CustomBlockTextures.getReplacementModel(state, pos);
+ if (replacement != null) return replacement;
+ CustomBlockTextures.enterFallbackCall();
+ var fallback = original.call(instance, state);
+ CustomBlockTextures.exitFallbackCall();
+ return fallback;
+ }
+//TODO: cover renderDamage model
+// @WrapOperation(method = "renderDamage", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/render/block/BlockModels;getModel(Lnet/minecraft/block/BlockState;)Lnet/minecraft/client/render/model/BakedModel;"))
+// private BakedModel replaceModelInRenderDamage(
+// BlockModels instance, BlockState state, Operation<BakedModel> original, @Local(argsOnly = true) BlockPos pos) {
+// var replacement = CustomBlockTextures.getReplacementModel(state, pos);
+// if (replacement != null) return replacement;
+// CustomBlockTextures.enterFallbackCall();
+// var fallback = original.call(instance, state);
+// CustomBlockTextures.exitFallbackCall();
+// return fallback;
+// }
}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java
index 53ab74a..455fbf1 100644
--- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceFallbackBlockModel.java
@@ -3,7 +3,7 @@ package moe.nea.firmament.mixins.custommodels;
import moe.nea.firmament.features.texturepack.CustomBlockTextures;
import net.minecraft.block.BlockState;
import net.minecraft.client.render.block.BlockModels;
-import net.minecraft.client.render.model.BakedModel;
+import net.minecraft.client.render.model.BlockStateModel;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
import org.spongepowered.asm.mixin.injection.Inject;
@@ -13,7 +13,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
public class ReplaceFallbackBlockModel {
// TODO: add check to BlockDustParticle
@Inject(method = "getModel", at = @At("HEAD"), cancellable = true)
- private void getModel(BlockState state, CallbackInfoReturnable<BakedModel> cir) {
+ private void getModel(BlockState state, CallbackInfoReturnable<BlockStateModel> cir) {
var replacement = CustomBlockTextures.getReplacementModel(state, null);
if (replacement != null)
cir.setReturnValue(replacement);
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceHeadModel.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceHeadModel.java
new file mode 100644
index 0000000..f445f02
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceHeadModel.java
@@ -0,0 +1,51 @@
+package moe.nea.firmament.mixins.custommodels;
+
+import moe.nea.firmament.features.texturepack.HeadModelChooser;
+import net.minecraft.client.item.ItemModelManager;
+import net.minecraft.client.render.entity.LivingEntityRenderer;
+import net.minecraft.client.render.entity.model.EntityModel;
+import net.minecraft.client.render.entity.state.LivingEntityRenderState;
+import net.minecraft.client.render.item.ItemRenderState;
+import net.minecraft.entity.EquipmentSlot;
+import net.minecraft.entity.LivingEntity;
+import net.minecraft.item.ItemDisplayContext;
+import org.spongepowered.asm.mixin.Final;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.Unique;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(LivingEntityRenderer.class)
+public class ReplaceHeadModel<T extends LivingEntity, S extends LivingEntityRenderState, M extends EntityModel<? super S>> {
+ @Shadow
+ @Final
+ protected ItemModelManager itemModelResolver;
+
+ @Unique
+ private ItemRenderState tempRenderState = new ItemRenderState();
+
+ @Inject(
+ method = "updateRenderState(Lnet/minecraft/entity/LivingEntity;Lnet/minecraft/client/render/entity/state/LivingEntityRenderState;F)V",
+ at = @At("TAIL")
+ )
+ private void replaceHeadModel(
+ T livingEntity, S livingEntityRenderState, float f, CallbackInfo ci
+ ) {
+ var headItemStack = livingEntity.getEquippedStack(EquipmentSlot.HEAD);
+
+ HeadModelChooser.INSTANCE.getIS_CHOOSING_HEAD_MODEL().set(true);
+ tempRenderState.clear();
+ this.itemModelResolver.updateForLivingEntity(tempRenderState, headItemStack, ItemDisplayContext.HEAD, livingEntity);
+ HeadModelChooser.INSTANCE.getIS_CHOOSING_HEAD_MODEL().set(false);
+
+ if (HeadModelChooser.HasExplicitHeadModelMarker.cast(tempRenderState)
+ .isExplicitHeadModel_Firmament()) {
+ livingEntityRenderState.wearingSkullType = null;
+ var temp = livingEntityRenderState.headItemRenderState;
+ livingEntityRenderState.headItemRenderState = tempRenderState;
+ tempRenderState = temp;
+ }
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java
index dfc87a0..f2a7409 100644
--- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java
@@ -4,46 +4,40 @@ package moe.nea.firmament.mixins.custommodels;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import moe.nea.firmament.events.CustomItemModelEvent;
+import moe.nea.firmament.util.mc.IntrospectableItemModelManager;
import net.minecraft.client.item.ItemModelManager;
import net.minecraft.client.render.item.model.ItemModel;
import net.minecraft.client.render.item.model.MissingItemModel;
-import net.minecraft.client.render.model.BakedModelManager;
import net.minecraft.component.ComponentType;
import net.minecraft.item.ItemStack;
import net.minecraft.util.Identifier;
+import org.jetbrains.annotations.NotNull;
import org.spongepowered.asm.mixin.Final;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.Shadow;
-import org.spongepowered.asm.mixin.Unique;
import org.spongepowered.asm.mixin.injection.At;
-import org.spongepowered.asm.mixin.injection.Inject;
-import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
import java.util.function.Function;
@Mixin(ItemModelManager.class)
-public class ReplaceItemModelPatch {
+public class ReplaceItemModelPatch implements IntrospectableItemModelManager {
@Shadow
@Final
private Function<Identifier, ItemModel> modelGetter;
- @Inject(method = "<init>", at = @At("TAIL"))
- private void saveMissingModel(BakedModelManager bakedModelManager, CallbackInfo ci) {
- }
-
- @Unique
- private boolean hasModel(Identifier identifier) {
- return !(modelGetter.apply(identifier) instanceof MissingItemModel);
- }
-
@WrapOperation(
- method = "update(Lnet/minecraft/client/render/item/ItemRenderState;Lnet/minecraft/item/ItemStack;Lnet/minecraft/item/ModelTransformationMode;Lnet/minecraft/world/World;Lnet/minecraft/entity/LivingEntity;I)V",
+ method = "update",
at = @At(value = "INVOKE", target = "Lnet/minecraft/item/ItemStack;get(Lnet/minecraft/component/ComponentType;)Ljava/lang/Object;"))
private Object replaceItemModelByIdentifier(ItemStack instance, ComponentType componentType, Operation<Object> original) {
- var override = CustomItemModelEvent.getModelIdentifier(instance);
- if (override != null && hasModel(override)) {
+ var override = CustomItemModelEvent.getModelIdentifier(instance, this);
+ if (override != null && hasModel_firmament(override)) {
return override;
}
return original.call(instance, componentType);
}
+
+ @Override
+ public boolean hasModel_firmament(@NotNull Identifier identifier) {
+ return !(modelGetter.apply(identifier) instanceof MissingItemModel);
+ }
}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextColorInHandledScreen.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextColorInHandledScreen.java
deleted file mode 100644
index e4834e9..0000000
--- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceTextColorInHandledScreen.java
+++ /dev/null
@@ -1,48 +0,0 @@
-package moe.nea.firmament.mixins.custommodels;
-
-
-import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
-import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
-import moe.nea.firmament.features.texturepack.CustomTextColors;
-import net.minecraft.client.font.TextRenderer;
-import net.minecraft.client.gui.DrawContext;
-import net.minecraft.client.gui.screen.ingame.AnvilScreen;
-import net.minecraft.client.gui.screen.ingame.BeaconScreen;
-import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen;
-import net.minecraft.client.gui.screen.ingame.HandledScreen;
-import net.minecraft.client.gui.screen.ingame.InventoryScreen;
-import net.minecraft.client.gui.screen.ingame.MerchantScreen;
-import net.minecraft.text.Text;
-import org.spongepowered.asm.mixin.Mixin;
-import org.spongepowered.asm.mixin.injection.At;
-
-@Mixin({HandledScreen.class, InventoryScreen.class, CreativeInventoryScreen.class, MerchantScreen.class,
- AnvilScreen.class, BeaconScreen.class})
-public class ReplaceTextColorInHandledScreen {
-
- // To my future self: double check those mixins, but don't be too concerned about errors. Some of the wrapopertions
- // only apply in some of the specified subclasses.
-
- @WrapOperation(
- method = "drawForeground",
- at = @At(
- value = "INVOKE",
- target = "Lnet/minecraft/client/gui/DrawContext;drawText(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;IIIZ)I"),
- expect = 0,
- require = 0)
- private int replaceTextColorWithVariableShadow(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, boolean shadow, Operation<Integer> original) {
- return original.call(instance, textRenderer, text, x, y, CustomTextColors.INSTANCE.mapTextColor(text, color), shadow);
- }
-
- @WrapOperation(
- method = "drawForeground",
- at = @At(
- value = "INVOKE",
- target = "Lnet/minecraft/client/gui/DrawContext;drawTextWithShadow(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;III)I"),
- expect = 0,
- require = 0)
- private int replaceTextColorWithShadow(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, Operation<Integer> original) {
- return original.call(instance, textRenderer, text, x, y, CustomTextColors.INSTANCE.mapTextColor(text, color));
- }
-
-}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java
index 8d3b3f8..75cedf8 100644
--- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java
@@ -4,6 +4,8 @@ import com.google.gson.JsonObject;
import com.llamalad7.mixinextras.injector.ModifyReturnValue;
import com.llamalad7.mixinextras.sugar.Local;
import moe.nea.firmament.Firmament;
+import moe.nea.firmament.features.texturepack.CustomSkyBlockTextures;
+import moe.nea.firmament.features.texturepack.HeadModelChooser;
import moe.nea.firmament.features.texturepack.PredicateModel;
import moe.nea.firmament.util.ErrorUtil;
import net.minecraft.client.item.ItemAsset;
@@ -17,10 +19,7 @@ import net.minecraft.util.Identifier;
import org.spongepowered.asm.mixin.Mixin;
import org.spongepowered.asm.mixin.injection.At;
-import java.io.IOException;
import java.io.InputStreamReader;
-import java.util.ArrayList;
-import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@@ -46,26 +45,28 @@ public class SupplyFakeModelPatch {
}
private static ItemAssetsLoader.Result supplyExtraModels(ResourceManager resourceManager, ItemAssetsLoader.Result oldModels) {
+ if (!CustomSkyBlockTextures.TConfig.INSTANCE.getEnableLegacyMinecraftCompat()) return oldModels;
Map<Identifier, ItemAsset> newModels = new HashMap<>(oldModels.contents());
var resources = resourceManager.findResources(
"models/item",
- id -> id.getNamespace().equals("firmskyblock")
- && id.getPath().endsWith(".json")
- && !id.getPath().substring("models/item/".length()).contains("/"));
+ id -> (id.getNamespace().equals("firmskyblock") || id.getNamespace().equals("cittofirmgenerated"))
+ && id.getPath().endsWith(".json"));
for (Map.Entry<Identifier, Resource> model : resources.entrySet()) {
var resource = model.getValue();
var itemModelId = model.getKey().withPath(it -> it.substring("models/item/".length(), it.length() - ".json".length()));
var genericModelId = itemModelId.withPrefixedPath("item/");
+ var itemAssetId = itemModelId.withPrefixedPath("items/");
// TODO: inject tint indexes based on the json data here
ItemModel.Unbaked unbakedModel = new BasicItemModel.Unbaked(genericModelId, List.of());
// TODO: add a filter using the pack.mcmeta to opt out of this behaviour
try (var is = resource.getInputStream()) {
var jsonObject = Firmament.INSTANCE.getGson().fromJson(new InputStreamReader(is), JsonObject.class);
unbakedModel = PredicateModel.Unbaked.fromLegacyJson(jsonObject, unbakedModel);
+ unbakedModel = HeadModelChooser.Unbaked.fromLegacyJson(jsonObject, unbakedModel);
} catch (Exception e) {
ErrorUtil.INSTANCE.softError("Could not create resource for fake model supplication: " + model.getKey(), e);
}
- if (resourceManager.getResource(itemModelId)
+ if (resourceManager.getResource(itemAssetId.withSuffixedPath(".json"))
.map(Resource::getPack)
.map(it -> isResourcePackNewer(resourceManager, it, resource.getPack()))
.orElse(true)) {
@@ -84,7 +85,7 @@ public class SupplyFakeModelPatch {
var pack = manager.streamResourcePacks()
.filter(it -> it == null_ || it == proposal)
.collect(findLast());
- return pack.orElse(null) == proposal;
+ return pack.orElse(null_) != null_;
}
private static <T> Collector<T, ?, Optional<T>> findLast() {
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ExpandScreenBoundaries.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ExpandScreenBoundaries.java
new file mode 100644
index 0000000..e2cae45
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ExpandScreenBoundaries.java
@@ -0,0 +1,21 @@
+package moe.nea.firmament.mixins.custommodels.screenlayouts;
+
+import moe.nea.firmament.features.texturepack.CustomScreenLayouts;
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.client.gui.screen.ingame.RecipeBookScreen;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable;
+
+@Mixin({HandledScreen.class, RecipeBookScreen.class})
+public class ExpandScreenBoundaries {
+ @Inject(method = "isClickOutsideBounds", at = @At("HEAD"), cancellable = true)
+ private void onClickOutsideBounds(double mouseX, double mouseY, int left, int top, int button, CallbackInfoReturnable<Boolean> cir) {
+ var background = CustomScreenLayouts.getMover(CustomScreenLayouts.CustomScreenLayout::getBackground);
+ if (background == null) return;
+ var x = background.getX() + left;
+ var y = background.getY() + top;
+ cir.setReturnValue(mouseX < (double) x || mouseY < (double) y || mouseX >= (double) (x + background.getWidth()) || mouseY >= (double) (y + background.getHeight()));
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceAnvilScreen.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceAnvilScreen.java
new file mode 100644
index 0000000..7c5dc45
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceAnvilScreen.java
@@ -0,0 +1,55 @@
+package moe.nea.firmament.mixins.custommodels.screenlayouts;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import moe.nea.firmament.features.texturepack.CustomScreenLayouts;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ingame.AnvilScreen;
+import net.minecraft.client.gui.screen.ingame.ForgingScreen;
+import net.minecraft.client.gui.widget.TextFieldWidget;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.screen.AnvilScreenHandler;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.Shadow;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin(AnvilScreen.class)
+public abstract class ReplaceAnvilScreen extends ForgingScreen<AnvilScreenHandler> {
+ @Shadow
+ private TextFieldWidget nameField;
+
+ public ReplaceAnvilScreen(AnvilScreenHandler handler, PlayerInventory playerInventory, Text title, Identifier texture) {
+ super(handler, playerInventory, title, texture);
+ }
+
+ @Inject(method = "setup", at = @At("TAIL"))
+ private void moveNameField(CallbackInfo ci) {
+ var override = CustomScreenLayouts.getMover(CustomScreenLayouts.CustomScreenLayout::getNameField);
+ if (override == null) return;
+ int baseX = (this.width - this.backgroundWidth) / 2;
+ int baseY = (this.height - this.backgroundHeight) / 2;
+ nameField.setX(baseX + override.getX());
+ nameField.setY(baseY + override.getY());
+ if (override.getWidth() != null)
+ nameField.setWidth(override.getWidth());
+ if (override.getHeight() != null)
+ nameField.setHeight(override.getHeight());
+ }
+
+ @WrapOperation(method = "drawForeground",
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTextWithShadow(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;III)I"),
+ allow = 1)
+ private int onDrawRepairCost(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, Operation<Integer> original) {
+ var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getRepairCostTitle);
+ return original.call(instance, textRenderer,
+ textOverride.replaceText(text),
+ textOverride.replaceX(textRenderer, text, x),
+ textOverride.replaceY(y),
+ textOverride.replaceColor(text, color));
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceForgingScreen.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceForgingScreen.java
new file mode 100644
index 0000000..6e9023d
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceForgingScreen.java
@@ -0,0 +1,9 @@
+package moe.nea.firmament.mixins.custommodels.screenlayouts;
+
+import net.minecraft.client.gui.screen.ingame.ForgingScreen;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.Inject;
+
+@Mixin(ForgingScreen.class)
+public class ReplaceForgingScreen {
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceFurnaceBackgrounds.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceFurnaceBackgrounds.java
new file mode 100644
index 0000000..6b076db
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceFurnaceBackgrounds.java
@@ -0,0 +1,31 @@
+package moe.nea.firmament.mixins.custommodels.screenlayouts;
+
+import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
+import moe.nea.firmament.features.texturepack.CustomScreenLayouts;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ingame.AbstractFurnaceScreen;
+import net.minecraft.client.gui.screen.ingame.RecipeBookScreen;
+import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.screen.AbstractFurnaceScreenHandler;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import java.util.function.Function;
+
+@Mixin(AbstractFurnaceScreen.class)
+public abstract class ReplaceFurnaceBackgrounds<T extends AbstractFurnaceScreenHandler> extends RecipeBookScreen<T> {
+ public ReplaceFurnaceBackgrounds(T handler, RecipeBookWidget<?> recipeBook, PlayerInventory inventory, Text title) {
+ super(handler, recipeBook, inventory, title);
+ }
+
+ @WrapWithCondition(method = "drawBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTexture(Ljava/util/function/Function;Lnet/minecraft/util/Identifier;IIFFIIII)V"), allow = 1)
+ private boolean onDrawBackground(DrawContext instance, Function<Identifier, RenderLayer> renderLayers, Identifier sprite, int x, int y, float u, float v, int width, int height, int textureWidth, int textureHeight) {
+ final var override = CustomScreenLayouts.getActiveScreenOverride();
+ if (override == null || override.getBackground() == null) return true;
+ override.getBackground().renderGeneric(instance, this);
+ return false;
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceGenericBackgrounds.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceGenericBackgrounds.java
new file mode 100644
index 0000000..bd12177
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceGenericBackgrounds.java
@@ -0,0 +1,28 @@
+package moe.nea.firmament.mixins.custommodels.screenlayouts;
+
+import moe.nea.firmament.features.texturepack.CustomScreenLayouts;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ingame.*;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.screen.ScreenHandler;
+import net.minecraft.text.Text;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Inject;
+import org.spongepowered.asm.mixin.injection.callback.CallbackInfo;
+
+@Mixin({CraftingScreen.class, CrafterScreen.class, Generic3x3ContainerScreen.class, GenericContainerScreen.class, HopperScreen.class, ShulkerBoxScreen.class,})
+public abstract class ReplaceGenericBackgrounds extends HandledScreen<ScreenHandler> {
+ // TODO: split out screens with special background components like flames, arrows, etc. (maybe arrows deserve generic handling tho)
+ public ReplaceGenericBackgrounds(ScreenHandler handler, PlayerInventory inventory, Text title) {
+ super(handler, inventory, title);
+ }
+
+ @Inject(method = "drawBackground", at = @At("HEAD"), cancellable = true)
+ private void replaceDrawBackground(DrawContext context, float deltaTicks, int mouseX, int mouseY, CallbackInfo ci) {
+ final var override = CustomScreenLayouts.getActiveScreenOverride();
+ if (override == null || override.getBackground() == null) return;
+ override.getBackground().renderGeneric(context, this);
+ ci.cancel();
+ }
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplacePlayerBackgrounds.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplacePlayerBackgrounds.java
new file mode 100644
index 0000000..e02a821
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplacePlayerBackgrounds.java
@@ -0,0 +1,50 @@
+package moe.nea.firmament.mixins.custommodels.screenlayouts;
+
+import com.llamalad7.mixinextras.injector.v2.WrapWithCondition;
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import moe.nea.firmament.features.texturepack.CustomScreenLayouts;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ingame.InventoryScreen;
+import net.minecraft.client.gui.screen.ingame.RecipeBookScreen;
+import net.minecraft.client.gui.screen.recipebook.RecipeBookWidget;
+import net.minecraft.client.render.RenderLayer;
+import net.minecraft.entity.player.PlayerInventory;
+import net.minecraft.screen.PlayerScreenHandler;
+import net.minecraft.text.Text;
+import net.minecraft.util.Identifier;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+
+import java.util.function.Function;
+
+@Mixin(InventoryScreen.class)
+public abstract class ReplacePlayerBackgrounds extends RecipeBookScreen<PlayerScreenHandler> {
+ public ReplacePlayerBackgrounds(PlayerScreenHandler handler, RecipeBookWidget<?> recipeBook, PlayerInventory inventory, Text title) {
+ super(handler, recipeBook, inventory, title);
+ }
+
+
+ @WrapOperation(method = "drawForeground",
+ allow = 1,
+ at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawText(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;IIIZ)I"))
+ private int onDrawForegroundText(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, boolean shadow, Operation<Integer> original) {
+ var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getContainerTitle);
+ return original.call(instance, textRenderer,
+ textOverride.replaceText(text),
+ textOverride.replaceX(textRenderer, text, x),
+ textOverride.replaceY(y),
+ textOverride.replaceColor(text, color),
+ shadow);
+ }
+
+ @WrapWithCondition(method = "drawBackground", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/DrawContext;drawTexture(Ljava/util/function/Function;Lnet/minecraft/util/Identifier;IIFFIIII)V"))
+ private boolean onDrawBackground(DrawContext instance, Function<Identifier, RenderLayer> renderLayers, Identifier sprite, int x, int y, float u, float v, int width, int height, int textureWidth, int textureHeight) {
+ final var override = CustomScreenLayouts.getActiveScreenOverride();
+ if (override == null || override.getBackground() == null) return true;
+ override.getBackground().renderGeneric(instance, this);
+ return false;
+ }
+ // TODO: allow moving the player
+}
diff --git a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceTextColorInHandledScreen.java b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceTextColorInHandledScreen.java
new file mode 100644
index 0000000..4f0905a
--- /dev/null
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/screenlayouts/ReplaceTextColorInHandledScreen.java
@@ -0,0 +1,65 @@
+package moe.nea.firmament.mixins.custommodels.screenlayouts;
+
+import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
+import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
+import moe.nea.firmament.features.texturepack.CustomScreenLayouts;
+import net.minecraft.client.font.TextRenderer;
+import net.minecraft.client.gui.DrawContext;
+import net.minecraft.client.gui.screen.ingame.AnvilScreen;
+import net.minecraft.client.gui.screen.ingame.BeaconScreen;
+import net.minecraft.client.gui.screen.ingame.CreativeInventoryScreen;
+import net.minecraft.client.gui.screen.ingame.HandledScreen;
+import net.minecraft.client.gui.screen.ingame.InventoryScreen;
+import net.minecraft.client.gui.screen.ingame.MerchantScreen;
+import net.minecraft.text.Text;
+import org.objectweb.asm.Opcodes;
+import org.spongepowered.asm.mixin.Mixin;
+import org.spongepowered.asm.mixin.injection.At;
+import org.spongepowered.asm.mixin.injection.Slice;
+
+@Mixin(HandledScreen.class)
+// TODO: MerchantScreen.class, BeaconScreen.class
+public class ReplaceTextColorInHandledScreen {
+
+ @WrapOperation(
+ method = "drawForeground",
+ at = @At(
+ value = "INVOKE",
+ target = "Lnet/minecraft/client/gui/DrawContext;drawText(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;IIIZ)I"),
+ slice = @Slice(
+ from = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;title:Lnet/minecraft/text/Text;", opcode = Opcodes.GETFIELD),
+ to = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;playerInventoryTitle:Lnet/minecraft/text/Text;", opcode = Opcodes.GETFIELD)
+ ),
+ allow = 1,
+ require = 1)
+ private int replaceContainerTitle(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, boolean shadow, Operation<Integer> original) {
+ var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getContainerTitle);
+ return original.call(instance, textRenderer,
+ textOverride.replaceText(text),
+ textOverride.replaceX(textRenderer, text, x),
+ textOverride.replaceY(y),
+ textOverride.replaceColor(text, color),
+ shadow);
+ }
+
+ @WrapOperation(
+ method = "drawForeground",
+ at = @At(
+ value = "INVOKE",
+ target = "Lnet/minecraft/client/gui/DrawContext;drawText(Lnet/minecraft/client/font/TextRenderer;Lnet/minecraft/text/Text;IIIZ)I"),
+ slice = @Slice(
+ from = @At(value = "FIELD", target = "Lnet/minecraft/client/gui/screen/ingame/HandledScreen;playerInventoryTitle:Lnet/minecraft/text/Text;", opcode = Opcodes.GETFIELD),
+ to = @At(value = "TAIL")
+ ),
+ allow = 1,
+ require = 1)
+ private int replacePlayerTitle(DrawContext instance, TextRenderer textRenderer, Text text, int x, int y, int color, boolean shadow, Operation<Integer> original) {
+ var textOverride = CustomScreenLayouts.getTextMover(CustomScreenLayouts.CustomScreenLayout::getPlayerTitle);
+ return original.call(instance, textRenderer,
+ textOverride.replaceText(text),
+ textOverride.replaceX(textRenderer, text, x),
+ textOverride.replaceY(y),
+ textOverride.replaceColor(text, color),
+ shadow);
+ }
+}
diff --git a/symbols/src/main/kotlin/process/CompatMetaProcessor.kt b/symbols/src/main/kotlin/process/CompatMetaProcessor.kt
new file mode 100644
index 0000000..0753e4c
--- /dev/null
+++ b/symbols/src/main/kotlin/process/CompatMetaProcessor.kt
@@ -0,0 +1,63 @@
+package moe.nea.firmament.annotations.process
+
+import com.google.auto.service.AutoService
+import com.google.devtools.ksp.processing.CodeGenerator
+import com.google.devtools.ksp.processing.Dependencies
+import com.google.devtools.ksp.processing.KSPLogger
+import com.google.devtools.ksp.processing.Resolver
+import com.google.devtools.ksp.processing.SymbolProcessor
+import com.google.devtools.ksp.processing.SymbolProcessorEnvironment
+import com.google.devtools.ksp.processing.SymbolProcessorProvider
+import com.google.devtools.ksp.symbol.KSAnnotated
+import com.google.devtools.ksp.symbol.KSClassDeclaration
+import com.google.devtools.ksp.symbol.KSName
+
+class CompatMetaProcessor(val logger: KSPLogger, val codeGenerator: CodeGenerator, val sourceSetName: String) :
+ SymbolProcessor {
+ override fun process(resolver: Resolver): List<KSAnnotated> {
+ val files = resolver.getAllFiles().toList()
+ val packages = files.mapTo(mutableSetOf()) { it.packageName.asString() }
+ packages.add("moe.nea.firmament.annotations.generated.$sourceSetName")
+ val compatMeta = resolver.getSymbolsWithAnnotation("moe.nea.firmament.util.compatloader.CompatMeta")
+ .singleOrNull() as KSClassDeclaration? ?: return listOf()
+ val dependencies = Dependencies(aggregating = true, *files.toTypedArray())
+ val generatedFileName = "GeneratedCompat${sourceSetName.replaceFirstChar { it.uppercaseChar() }}"
+ val compatFile =
+ codeGenerator.createNewFile(dependencies, "moe.nea.firmament.annotations.generated.$sourceSetName", generatedFileName)
+ .bufferedWriter()
+ compatFile.appendLine("// This file is @generated by SubscribeAnnotationProcessor")
+ compatFile.appendLine("// Do not edit")
+ compatFile.appendLine("package moe.nea.firmament.annotations.generated.$sourceSetName")
+ compatFile.appendLine("class $generatedFileName : moe.nea.firmament.util.compatloader.ICompatMetaGen {")
+ compatFile.appendLine("""
+ override fun owns(className: String): Boolean {
+ return moe.nea.firmament.util.compatloader.CompatHelper.isOwnedByPackage(className, ${
+ packages.joinToString { "\"" + it + "\"" }
+ })
+ }
+
+ override val meta: moe.nea.firmament.util.compatloader.ICompatMeta
+ get() = ${compatMeta.qualifiedName!!.asString()}
+""")
+ compatFile.appendLine("}")
+ compatFile.close()
+ val metaInf = codeGenerator.createNewFileByPath(
+ dependencies,
+ "META-INF/services/moe.nea.firmament.util.compatloader.ICompatMetaGen", extensionName = "")
+ .bufferedWriter()
+ metaInf.append("moe.nea.firmament.annotations.generated.$sourceSetName.")
+ metaInf.appendLine(generatedFileName)
+ metaInf.close()
+ return listOf()
+ }
+
+
+ @AutoService(SymbolProcessorProvider::class)
+ class Provider : SymbolProcessorProvider {
+ override fun create(environment: SymbolProcessorEnvironment): SymbolProcessor {
+ return CompatMetaProcessor(environment.logger,
+ environment.codeGenerator,
+ environment.options["firmament.sourceset"] ?: "main")
+ }
+ }
+}
diff --git a/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt b/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt
index d7aaf28..3eaf3d6 100644
--- a/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt
+++ b/symbols/src/main/kotlin/process/SubscribeAnnotationProcessor.kt
@@ -25,22 +25,19 @@ class SubscribeAnnotationProcessor(
override fun finish() {
subscriptions.sort()
if (subscriptions.isEmpty()) return
- val subscriptionSet = subscriptions.mapTo(mutableSetOf()) { it.parent.containingFile!! }
+ val subscriptionSet = subscriptions.mapTo(mutableSetOf()) { it.cf }
val dependencies = Dependencies(
aggregating = true,
*subscriptionSet.toTypedArray())
val generatedFileName = "AllSubscriptions${sourceSetName.replaceFirstChar { it.uppercaseChar() }}"
val subscriptionsFile =
codeGenerator
- .createNewFile(dependencies, "moe.nea.firmament.annotations.generated", generatedFileName)
+ .createNewFile(dependencies, "moe.nea.firmament.annotations.generated.$sourceSetName", generatedFileName)
.bufferedWriter()
subscriptionsFile.apply {
appendLine("// This file is @generated by SubscribeAnnotationProcessor")
appendLine("// Do not edit")
- for (file in subscriptionSet) {
- appendLine("// Dependency: ${file.filePath}")
- }
- appendLine("package moe.nea.firmament.annotations.generated")
+ appendLine("package moe.nea.firmament.annotations.generated.$sourceSetName")
appendLine()
appendLine("import moe.nea.firmament.events.subscription.*")
appendLine()
@@ -48,7 +45,7 @@ class SubscribeAnnotationProcessor(
appendLine("class $generatedFileName : SubscriptionList {")
appendLine(" override fun provideSubscriptions(addSubscription: (Subscription<*>) -> Unit) {")
for (subscription in subscriptions) {
- val owner = subscription.parent.qualifiedName!!.asString()
+ val owner = subscription.pQName.asString()
val method = subscription.child.simpleName.asString()
val type = subscription.type.declaration.qualifiedName!!.asString()
appendLine(" addSubscription(Subscription<$type>(")
@@ -65,7 +62,7 @@ class SubscribeAnnotationProcessor(
dependencies,
"META-INF/services/moe.nea.firmament.events.subscription.SubscriptionList", extensionName = "")
.bufferedWriter()
- metaInf.append("moe.nea.firmament.annotations.generated.")
+ metaInf.append("moe.nea.firmament.annotations.generated.$sourceSetName.")
metaInf.appendLine(generatedFileName)
metaInf.close()
}
@@ -75,13 +72,15 @@ class SubscribeAnnotationProcessor(
val child: KSFunctionDeclaration,
val type: KSType,
) : Comparable<Subscription> {
+ val cf = parent.containingFile!!
+ val pQName = parent.qualifiedName!!
+ val tName = type.declaration.qualifiedName!!
override fun compareTo(other: Subscription): Int {
- var compare = parent.qualifiedName!!.asString().compareTo(other.parent.qualifiedName!!.asString())
+ var compare = pQName.asString().compareTo(other.pQName.asString())
if (compare != 0) return compare
compare = other.child.simpleName.asString().compareTo(child.simpleName.asString())
if (compare != 0) return compare
- compare = other.type.declaration.qualifiedName!!.asString()
- .compareTo(type.declaration.qualifiedName!!.asString())
+ compare = other.tName.asString().compareTo(tName.asString())
if (compare != 0) return compare
return 0
}
diff --git a/translations/en_us.json b/translations/en_us.json
index 82dcd45..3431259 100644
--- a/translations/en_us.json
+++ b/translations/en_us.json
@@ -7,7 +7,8 @@
"firmament.command.waypoint.added": "Added waypoint %s %s %s.",
"firmament.command.waypoint.clear": "Cleared waypoints.",
"firmament.command.waypoint.import": "Imported %s waypoints from clipboard.",
- "firmament.command.waypoint.import.error": "Could not import waypoints from clipboard. Make sure they are on ColeWeight format:\n[{\"x\": 69, \"y\":420, \"z\": 36}]",
+ "firmament.command.waypoint.ordered.toggle.false": "Disabled ordered waypoints",
+ "firmament.command.waypoint.ordered.toggle.true": "Enabled ordered waypoints",
"firmament.command.waypoint.remove": "Removed waypoint %s. Other waypoints may have different indexes now.",
"firmament.command.waypoint.remove.error": "Could not find waypoint with that index to delete.",
"firmament.command.waypoint.skip": "Skipped 1 waypoint",
@@ -23,6 +24,14 @@
"firmament.config.auto-completions.warp-complete.description": "Auto complete warp destinations in chat. This may include warps you have not yet unlocked.",
"firmament.config.auto-completions.warp-is": "Redirect /warp is to /warp island",
"firmament.config.auto-completions.warp-is.description": "Redirects /warp is to /warp island, since hypixel does not recognize /warp is as a warp destination.",
+ "firmament.config.bonemerang-overlay": "Bonemerang Overlay",
+ "firmament.config.bonemerang-overlay.bonemerang-overlay": "Bonemerang Overlay",
+ "firmament.config.bonemerang-overlay.bonemerang-overlay-hud": "Bonemerang Overlay Hud",
+ "firmament.config.bonemerang-overlay.bonemerang-overlay-hud.description": "Shows how many targets your bonemerang will hit",
+ "firmament.config.bonemerang-overlay.bonemerang-overlay.description": "Display an overlay that tells you what block you will warp to.",
+ "firmament.config.bonemerang-overlay.bonemerang-overlay.display": "Bonemerang Targets: %s",
+ "firmament.config.bonemerang-overlay.highlight-hit-entities": "Highlight Target Entities",
+ "firmament.config.bonemerang-overlay.highlight-hit-entities.description": "Highlight entities that will be hit",
"firmament.config.carnival": "Carnival Features",
"firmament.config.carnival.bombs-solver": "Minesweeper Helper",
"firmament.config.carnival.bombs-solver.description": "Display bombs surrounding each block in minesweeper.",
@@ -34,16 +43,23 @@
"firmament.config.category.dev.description": "Settings for texture pack devs and programmers",
"firmament.config.category.events": "Events",
"firmament.config.category.events.description": "Settings for temporary or repeating events",
+ "firmament.config.category.garden": "Garden",
+ "firmament.config.category.garden.description": "Features for the No. 1 Macro Free Island on SkyBlock",
"firmament.config.category.integrations": "Integrations & Textures",
"firmament.config.category.integrations.description": "Integrations with other mods, as well as texture packs",
"firmament.config.category.inventory": "Inventory",
"firmament.config.category.inventory.description": "Features for anything that happens in a chest or inventory",
+ "firmament.config.category.items": "Items",
+ "firmament.config.category.items.description": "Features for items",
"firmament.config.category.meta": "Meta & Firmament",
"firmament.config.category.meta.description": "Settings for Firmament and the item repo",
"firmament.config.category.mining": "Mining",
"firmament.config.category.mining.description": "Mining related features",
"firmament.config.category.misc": "Miscellaneous",
"firmament.config.category.misc.description": "Miscellaneous features that don't fit elsewhere",
+ "firmament.config.centuryraffle": "Century Raffle",
+ "firmament.config.centuryraffle.highlight-cake-players": "Highlight Players in Team",
+ "firmament.config.centuryraffle.highlight-cake-players.description": "Highlight the players you can feed while holding a Slice of Century Cake",
"firmament.config.chat-links": "Chat Links",
"firmament.config.chat-links.allow-all-hosts": "Allow all Image Hosts",
"firmament.config.chat-links.allow-all-hosts.description": "Allow displaying images no matter where it is hosted.",
@@ -63,11 +79,19 @@
"firmament.config.compatibility.explosion-enabled.description": "Redirect explosion particles to be rendered by enhanced explosions.",
"firmament.config.compatibility.explosion-power": "Enhanced Explosion Power",
"firmament.config.compatibility.explosion-power.description": "Choose how big explosions will be rendered by enhanced explosions",
+ "firmament.config.composter": "Composter",
+ "firmament.config.composter.no-more-noises": "Mute Composter",
+ "firmament.config.composter.no-more-noises.description": "Muffle all noises and sounds made by the composter",
"firmament.config.configconfig": "Firmaments Config",
"firmament.config.configconfig.enable-moulconfig": "Use MoulConfig",
"firmament.config.configconfig.enable-moulconfig.description": "Uses the MoulConfig config UI. Turn off to fall back to the built in config.",
"firmament.config.configconfig.enable-yacl": "Use YACL Config",
"firmament.config.configconfig.enable-yacl.description": "Uses the YACL config UI. Turn off to fall back to the built in config. Needs YACL to be installed separately.",
+ "firmament.config.configconfig.wide-moulconfig": "Wide MoulConfig",
+ "firmament.config.configconfig.wide-moulconfig.description": "Use a wider editor for MoulConfig",
+ "firmament.config.copy-chat": "Copy Chat",
+ "firmament.config.copy-chat.copy-chat": "Copy Chat",
+ "firmament.config.copy-chat.copy-chat.description": "Right click a message to copy",
"firmament.config.custom-skyblock-textures": "Custom SkyBlock Item Textures",
"firmament.config.custom-skyblock-textures.armor-overrides": "Enable Armor re-texturing",
"firmament.config.custom-skyblock-textures.armor-overrides.description": "Allows texture pack authors to re-texture (but not re-model) SkyBlock armors.",
@@ -81,13 +105,20 @@
"firmament.config.custom-skyblock-textures.enabled.description": "Allow replacing items for texture packs. Turning this off does not disable custom predicates",
"firmament.config.custom-skyblock-textures.legacy-cit": "Enable legacy CIT Resewn compat",
"firmament.config.custom-skyblock-textures.legacy-cit.description": "Allow CIT resewn texture packs written for 1.20.4 to be loaded on newer versions.",
+ "firmament.config.custom-skyblock-textures.legacy-minecraft-path-support": "Enable Legacy Paths",
+ "firmament.config.custom-skyblock-textures.legacy-minecraft-path-support.description": "Allow texture packs to load textures from some legacy paths. I.e.: Allows loading 1.21.0 armor textures on 1.21.4.",
"firmament.config.custom-skyblock-textures.model-overrides": "Enable model overrides/predicates",
"firmament.config.custom-skyblock-textures.model-overrides.description": "Enable Firmament's model predicates. This will apply to vanilla models as well, if that vanilla model has Firmament predicates.",
"firmament.config.custom-skyblock-textures.recolor-text": "Allow packs to recolor text",
"firmament.config.custom-skyblock-textures.recolor-text.description": "Allows texture packs to recolor UI texts.",
+ "firmament.config.custom-skyblock-textures.screen-layouts": "Allow packs screen relayouts",
+ "firmament.config.custom-skyblock-textures.screen-layouts.description": "Allows texture packs to move UI elements like slots around, as well as replace the background of screens.",
"firmament.config.custom-skyblock-textures.skulls-enabled": "Enable Custom Placed Skull Textures",
"firmament.config.custom-skyblock-textures.skulls-enabled.description": "Allow replacing the textures of placed skulls.",
"firmament.config.developer": "Developer Settings",
+ "firmament.config.developer-capes": "Developer Capes",
+ "firmament.config.developer-capes.show-cape": "Show Developer Capes",
+ "firmament.config.developer-capes.show-cape.description": "Allows you to see the developer capes.",
"firmament.config.developer.auto-rebuild": "Automatically rebuild resources",
"firmament.config.developer.auto-rebuild.description": "Executes ./gradlew processResources before F3+T is executed.",
"firmament.config.diana": "Diana",
@@ -97,6 +128,13 @@
"firmament.config.diana.ancestral-teleport.description": "Click to teleport near the guessed burrow.",
"firmament.config.diana.nearby-waypoints": "Nearby Waypoints Highlighter",
"firmament.config.diana.nearby-waypoints.description": "Highlight nearby diana burrows.",
+ "firmament.config.etherwarp-overlay": "Etherwarp Overlay",
+ "firmament.config.etherwarp-overlay.cube": "Cube",
+ "firmament.config.etherwarp-overlay.cube.description": "Displays a full cube on the block",
+ "firmament.config.etherwarp-overlay.etherwarp-overlay": "Etherwarp Overlay",
+ "firmament.config.etherwarp-overlay.etherwarp-overlay.description": "Display an overlay that tells you what block you will warp to.",
+ "firmament.config.etherwarp-overlay.wireframe": "Outline",
+ "firmament.config.etherwarp-overlay.wireframe.description": "Displays a full outline on the block",
"firmament.config.fairy-souls": "Fairy Souls",
"firmament.config.fairy-souls.reset": "Reset Collected Fairy Souls",
"firmament.config.fairy-souls.reset.description": "Reset all collected fairy souls, allowing you to restart from null.",
@@ -111,16 +149,46 @@
"firmament.config.fixes.auto-sprint-hud.description": "Show your current sprint state on your screen. Only visible if no auto sprint keybind is set.",
"firmament.config.fixes.auto-sprint-keybinding": "Auto Sprint KeyBinding",
"firmament.config.fixes.auto-sprint-keybinding.description": "Toggle auto sprint via this keybinding.",
+ "firmament.config.fixes.auto-sprint-underwater": "Sprint Under Water",
+ "firmament.config.fixes.auto-sprint-underwater.description": "Also Toggle Sprint under water. Sprinting under water puts you in the swimming animation which changes your camera and hitbox, which can be confusing if you stop and start moving a lot.",
"firmament.config.fixes.auto-sprint.description": "This is different from vanilla sprint in the way that it only marks the keybinding pressed for the first tick of walking.",
+ "firmament.config.fixes.disable-hurt-cam": "No Hurt Cam",
+ "firmament.config.fixes.disable-hurt-cam.description": "Disable the damage screen shake animation.",
"firmament.config.fixes.hide-mob-effects": "Hide Potion Effects",
"firmament.config.fixes.hide-mob-effects.description": "Hide Potion effects on the right side of your player inventory.",
+ "firmament.config.fixes.hide-potion-effects-hud": "Hide Potion Effects HUD",
+ "firmament.config.fixes.hide-potion-effects-hud.description": "Hides the potion effects HUd in the top right.",
+ "firmament.config.fixes.hide-recipe-book": "No Recipe Book",
+ "firmament.config.fixes.hide-recipe-book.description": "Remove the recipe book from your inventory",
+ "firmament.config.fixes.hide-slot-highlights": "Hide Slot Highlights",
+ "firmament.config.fixes.hide-slot-highlights.description": "Hide slot highlights for items with disabled tooltip. This makes /sbmenu look nicer with smooth texture packs.",
"firmament.config.fixes.peek-chat": "Peek Chat",
"firmament.config.fixes.peek-chat.description": "Hold this keybinding to view the chat as if you have it opened, but while still being able to control your character.",
"firmament.config.fixes.player-skins": "Fix unsigned Player Skins",
"firmament.config.fixes.player-skins.description": "Mark all player skins as signed, preventing console spam, and some rendering issues.",
- "firmament.config.inventory-buttons": "Inventory buttons",
- "firmament.config.inventory-buttons.open-editor": "Open Editor",
- "firmament.config.inventory-buttons.open-editor.description": "Click anywhere to create a new inventory button or to edit one. Hold SHIFT to grid align.",
+ "firmament.config.hud": "Hud",
+ "firmament.config.hud.day-count": "Day Count",
+ "firmament.config.hud.day-count-hud": "Day Count Hud",
+ "firmament.config.hud.day-count-hud.description": "Shows day.",
+ "firmament.config.hud.day-count-hud.display": "Day: %s",
+ "firmament.config.hud.day-count.description": "A HUD showing current day.",
+ "firmament.config.hud.fps-count": "FPS Count",
+ "firmament.config.hud.fps-count-hud": "FPS Count Hud",
+ "firmament.config.hud.fps-count-hud.description": "Shows FPS.",
+ "firmament.config.hud.fps-count-hud.display": "FPS: %s",
+ "firmament.config.hud.fps-count.description": "A HUD showing current FPS.",
+ "firmament.config.hud.ping-count": "Ping Count",
+ "firmament.config.hud.ping-count-hud": "Ping Count Hud",
+ "firmament.config.hud.ping-count-hud.description": "Shows Ping.",
+ "firmament.config.hud.ping-count-hud.display": "Ping %s",
+ "firmament.config.hud.ping-count.description": "A HUD showing current Ping.",
+ "firmament.config.inventory-buttons-config": "Inventory Buttons",
+ "firmament.config.inventory-buttons-config.hover-text": "Hover Tooltip",
+ "firmament.config.inventory-buttons-config.hover-text.description": "Hovering over inventory buttons will show the command they run.",
+ "firmament.config.inventory-buttons-config.only-inv": "Inventory Only",
+ "firmament.config.inventory-buttons-config.only-inv.description": "Only shows buttons while in the inventory",
+ "firmament.config.inventory-buttons-config.open-editor": "Open Editor",
+ "firmament.config.inventory-buttons-config.open-editor.description": "Click anywhere to create a new inventory button or to edit one. Hold SHIFT to grid align.",
"firmament.config.item-hotkeys": "Item Hotkeys",
"firmament.config.item-hotkeys.global-trade-interface": "Search on Bazaar/AH",
"firmament.config.item-hotkeys.global-trade-interface.description": "Press this button to search the hovered item on the bazaar or auction house.",
@@ -129,7 +197,12 @@
"firmament.config.item-rarity-cosmetics.background-hotbar": "Hotbar Background Rarity",
"firmament.config.item-rarity-cosmetics.background-hotbar.description": "Show item rarity background in the hotbar.",
"firmament.config.item-rarity-cosmetics.background.description": "Show a background behind each item, depending on its rarity.",
- "firmament.config.lore-timers": "Lore Timers",
+ "firmament.config.jade-integration": "Jade / WAILA",
+ "firmament.config.jade-integration.blocks": "Enable Custom Blocks",
+ "firmament.config.jade-integration.blocks.description": "Show custom block descriptions and hardness levels in Jade.",
+ "firmament.config.jade-integration.progress": "Enable Custom Mining Progress",
+ "firmament.config.jade-integration.progress.description": "Show the custom mining progress in Jade, when in a world with mining fatigue.",
+ "firmament.config.lore-timers": "Item Timestamps",
"firmament.config.lore-timers.format": "Time Format",
"firmament.config.lore-timers.format.choice.american": "§9Ame§cri§fcan",
"firmament.config.lore-timers.format.choice.local": "System Time Format",
@@ -137,6 +210,8 @@
"firmament.config.lore-timers.format.choice.socialist": "European-ish",
"firmament.config.lore-timers.format.description": "Choose the time format in which resolved timers are displayed.",
"firmament.config.lore-timers.show": "Show Lore Timers",
+ "firmament.config.lore-timers.show-creation": "Show Creation",
+ "firmament.config.lore-timers.show-creation.description": "Shows the creation or craft timestamp of the item. Sometimes this timestamp is retained when upgrading an item, so it isn't necessarily the craft time of this specific item, but rather one of its components.",
"firmament.config.lore-timers.show.description": "Shows when a timer in a lore (such as interest, auction duration) would end.",
"firmament.config.party-commands": "Party Commands",
"firmament.config.party-commands.cooldown": "Cooldown",
@@ -148,8 +223,14 @@
"firmament.config.pets": "Pets",
"firmament.config.pets.highlight-pet": "Highlight active pet",
"firmament.config.pets.highlight-pet.description": "Highlight your currently selected pet in the /pets menu.",
+ "firmament.config.pets.pet-overlay": "Pet Info",
+ "firmament.config.pets.pet-overlay-hud": "Pet Info Hud",
+ "firmament.config.pets.pet-overlay-hud.description": "A HUD showing current active pet and the pet exp.",
+ "firmament.config.pets.pet-overlay.description": "Shows current active pet and pet exp on screen.",
"firmament.config.pickaxe-info": "Pickaxes & Drills",
"firmament.config.pickaxe-info.ability-cooldown": "Pickaxe Ability Cooldown",
+ "firmament.config.pickaxe-info.ability-cooldown-toast": "Pickaxe Ability Ready Toast",
+ "firmament.config.pickaxe-info.ability-cooldown-toast.description": "Shows a toast when your pickaxe ability is ready.",
"firmament.config.pickaxe-info.ability-cooldown.description": "Show a cooldown on your cross-hair for your pickaxe ability.",
"firmament.config.pickaxe-info.ability-scale": "Ability Cooldown Scale",
"firmament.config.pickaxe-info.ability-scale.description": "Resize the cooldown around your cross-hair for your pickaxe ability.",
@@ -173,15 +254,31 @@
"firmament.config.power-user.copy-skull-texture.description": "Copy the texture location that can be used to re-texture the skull under your cross-hair.",
"firmament.config.power-user.copy-texture-pack-id": "Copy Texture Pack Id",
"firmament.config.power-user.copy-texture-pack-id.description": "Copy the texture pack id that is used for the item stack under your cursor.",
+ "firmament.config.power-user.copy-title": "Copy Inventory Title",
+ "firmament.config.power-user.copy-title.description": "Copies Inventory and Screen Titles",
"firmament.config.power-user.entity-data": "Show Entity Data",
"firmament.config.power-user.entity-data.description": "Print out information about the entity under your cross-hair.",
+ "firmament.config.power-user.export-item-stack": "Export Item Stack",
+ "firmament.config.power-user.export-item-stack.description": "Exports the hovered item to the repo data folder",
+ "firmament.config.power-user.export-npc-location": "Export NPC Location",
+ "firmament.config.power-user.export-npc-location.description": "Export the NPC's location to the repo data",
+ "firmament.config.power-user.export-recipe": "Export Recipe Data",
+ "firmament.config.power-user.export-recipe.description": "Export Recipe Data to the repo data",
"firmament.config.power-user.show-item-id": "Show SkyBlock Ids",
"firmament.config.power-user.show-item-id.description": "Show the SkyBlock id of items underneath them.",
- "firmament.config.price-data": "Price data",
+ "firmament.config.price-data": "Price Data",
+ "firmament.config.price-data.avg-lowest-bin-days": "AVG Lowest Bin Days",
+ "firmament.config.price-data.avg-lowest-bin-days.choice.off": "Off",
+ "firmament.config.price-data.avg-lowest-bin-days.choice.onedayavglowestbin": "1 Day",
+ "firmament.config.price-data.avg-lowest-bin-days.choice.sevendayavglowestbin": "7 Days",
+ "firmament.config.price-data.avg-lowest-bin-days.choice.threedayavglowestbin": "3 Days",
+ "firmament.config.price-data.avg-lowest-bin-days.description": "Select if and for how long the AVG Lowest BIN should show.",
"firmament.config.price-data.enable-always": "Enable Item Price",
"firmament.config.price-data.enable-always.description": "Show item auction/bazaar prices on SkyBlock items",
"firmament.config.price-data.enable-keybind": "Enable only with Keybinding",
"firmament.config.price-data.enable-keybind.description": "Only show auction/bazaar prices when holding this keybinding. Unbind to always show.",
+ "firmament.config.price-data.stack-size-keybind": "Stack Size Multiplier Keybinding",
+ "firmament.config.price-data.stack-size-keybind.description": "Press this key while hovering over an item to show its price multiplied by the number of items you have.",
"firmament.config.pristine-profit": "Pristine Profit Tracker",
"firmament.config.pristine-profit.fine-gemstones": "Use Fine Gemstones",
"firmament.config.pristine-profit.fine-gemstones.description": "Use the (more stable) price of fine gemstones, instead of flawed gemstones.",
@@ -204,6 +301,11 @@
"firmament.config.repo.disable-item-groups.description": "Disabling item groups can increase performance, but will no longer collect similar items (like minions, enchantments) together.",
"firmament.config.repo.enable-super-craft": "Always use Super Craft",
"firmament.config.repo.enable-super-craft.description": "Always use super craft when clicking the craft button in REI, instead of just when holding shift.",
+ "firmament.config.repo.perfect-renders": "Perfect Render",
+ "firmament.config.repo.perfect-renders.choice.nothing": "Broken (Fastest)",
+ "firmament.config.repo.perfect-renders.choice.render": "Fixed Visual (Fast)",
+ "firmament.config.repo.perfect-renders.choice.render_and_text": "Perfect (Slowest)",
+ "firmament.config.repo.perfect-renders.description": "Speed up item list loading by allowing items to be loaded in partially incorrectly at first. They will be corrected down the line when the background reload completes.",
"firmament.config.repo.redownload": "Redownload Item List",
"firmament.config.repo.redownload.description": "Force re-download the item list. This is automatically done on restart.",
"firmament.config.repo.reload": "Reload Item List",
@@ -231,28 +333,65 @@
"firmament.config.slot-locking.bind-render.choice.only_boxes": "Only boxes",
"firmament.config.slot-locking.bind-render.description": "Disable rendering of the slot binding lines (or all of the slot binding rendering), unless the relevant slot is being hovered.",
"firmament.config.slot-locking.bind.description": "Bind a hotbar slot to another slot. This allows quick switching between the slots by shift clicking on either slot.",
+ "firmament.config.slot-locking.drop-in-dungeons": "Allow Dungeon Abilities",
+ "firmament.config.slot-locking.drop-in-dungeons.description": "Allow dropping items in dungeons, to use your dungeon ultimate abilities.",
"firmament.config.slot-locking.lock": "Lock Slot",
"firmament.config.slot-locking.lock-uuid": "Lock UUID (Lock Item)",
"firmament.config.slot-locking.lock-uuid.description": "Lock a SkyBlock item by it's UUID. This blocks a specific item from being dropped/sold, but still allows moving it around.",
"firmament.config.slot-locking.lock.description": "Lock a slot, preventing any item from being moved from, dropped from, or placed into this slot.",
+ "firmament.config.slot-locking.multi-bind": "Allow Multi Binding",
+ "firmament.config.slot-locking.multi-bind.description": "Allow binding the same hotbar slot to multiple inventory slots.",
"firmament.config.slot-locking.require-quick-move": "Require Shift-Click for Bound Slots",
"firmament.config.slot-locking.require-quick-move.description": "If turned off, allows to switch between bound slots without holding shift.",
"firmament.config.storage-overlay": "Storage Overlay",
"firmament.config.storage-overlay.always-replace": "Always Open Overlay",
"firmament.config.storage-overlay.always-replace.description": "Always replace the ender chest with Firmament's storage overlay.",
+ "firmament.config.storage-overlay.block-item-scrolling": "Block Scrolling on Items",
+ "firmament.config.storage-overlay.block-item-scrolling.description": "Disables scrolling the storage overlay screen while you are hovering over an item. Useful if you have a tooltip scrolling mod.",
"firmament.config.storage-overlay.height": "Storage Height",
"firmament.config.storage-overlay.height.description": "The height of the scrollable storage panel.",
"firmament.config.storage-overlay.inverse-scroll": "Invert Scroll",
"firmament.config.storage-overlay.inverse-scroll.description": "Invert the mouse wheel scrolling in Firmament's storage overlay.",
"firmament.config.storage-overlay.margin": "Margin",
"firmament.config.storage-overlay.margin.description": "Margin inside of the storage overview.",
+ "firmament.config.storage-overlay.outline-active-page": "Outline Active Page",
+ "firmament.config.storage-overlay.outline-active-page.description": "Put a border around the selected storage page in the storage overlay.",
"firmament.config.storage-overlay.padding": "Padding",
"firmament.config.storage-overlay.padding.description": "Padding inside of the storage overview.",
"firmament.config.storage-overlay.rows": "Columns",
"firmament.config.storage-overlay.rows.description": "Max columns used by the storage overlay and overview.",
"firmament.config.storage-overlay.scroll-speed": "Scroll Speed",
"firmament.config.storage-overlay.scroll-speed.description": "Scroll speed inside of the storage overlay and overview.",
+ "firmament.config.wardrobe-keybinds": "Wardrobe Keybinds",
+ "firmament.config.wardrobe-keybinds.change-page": "Change Page",
+ "firmament.config.wardrobe-keybinds.change-page.description": "Changes the active page",
+ "firmament.config.wardrobe-keybinds.next-page": "Next Page",
+ "firmament.config.wardrobe-keybinds.next-page.description": "Goes to the next page",
+ "firmament.config.wardrobe-keybinds.previous-page": "Previous Page",
+ "firmament.config.wardrobe-keybinds.previous-page.description": "Goes to the previous page",
+ "firmament.config.wardrobe-keybinds.slot-1": "Slot 1",
+ "firmament.config.wardrobe-keybinds.slot-1.description": "Keybind to toggle the first set in your wardrobe",
+ "firmament.config.wardrobe-keybinds.slot-2": "Slot 2",
+ "firmament.config.wardrobe-keybinds.slot-2.description": "Keybind to toggle the second set in your wardrobe",
+ "firmament.config.wardrobe-keybinds.slot-3": "Slot 3",
+ "firmament.config.wardrobe-keybinds.slot-3.description": "Keybind to toggle the third set in your wardrobe",
+ "firmament.config.wardrobe-keybinds.slot-4": "Slot 4",
+ "firmament.config.wardrobe-keybinds.slot-4.description": "Keybind to toggle the fourth set in your wardrobe",
+ "firmament.config.wardrobe-keybinds.slot-5": "Slot 5",
+ "firmament.config.wardrobe-keybinds.slot-5.description": "Keybind to toggle the fifth set in your wardrobe",
+ "firmament.config.wardrobe-keybinds.slot-6": "Slot 6",
+ "firmament.config.wardrobe-keybinds.slot-6.description": "Keybind to toggle the sixth set in your wardrobe",
+ "firmament.config.wardrobe-keybinds.slot-7": "Slot 7",
+ "firmament.config.wardrobe-keybinds.slot-7.description": "Keybind to toggle the seventh set in your wardrobe",
+ "firmament.config.wardrobe-keybinds.slot-8": "Slot 8",
+ "firmament.config.wardrobe-keybinds.slot-8.description": "Keybind to toggle the eighth set in your wardrobe",
+ "firmament.config.wardrobe-keybinds.slot-9": "Slot 9",
+ "firmament.config.wardrobe-keybinds.slot-9.description": "Keybind to toggle the ninth set in your wardrobe",
+ "firmament.config.wardrobe-keybinds.wardrobe-keybinds": "Keybinds for your wardrobe",
+ "firmament.config.wardrobe-keybinds.wardrobe-keybinds.description": "Lets you use your number keys to quickly change your wardrobe",
"firmament.config.waypoints": "Waypoints",
+ "firmament.config.waypoints.reset-order-on-swap": "Reset Ordered Waypoints On Hop",
+ "firmament.config.waypoints.reset-order-on-swap.description": "Resets Ordered Waypoint progress after swapping to another world.",
"firmament.config.waypoints.show-index": "Show ordered waypoint indexes",
"firmament.config.waypoints.show-index.description": "Show the number of an ordered waypoint in the world.",
"firmament.config.waypoints.skip-to-nearest": "Allow skipping waypoints",
@@ -275,12 +414,16 @@
"firmament.hotmpreset.scrolled": "Just scrolled. Waiting on server to update items.",
"firmament.hotmpreset.scrollprompt": "We need to scroll! Please click anywhere to continue.",
"firmament.hud.edit": "Edit %s",
+ "firmament.inventory-buttons.all-warps-preset": "All Warps Preset",
+ "firmament.inventory-buttons.delete": "Hold L-CTRL and click to delete",
"firmament.inventory-buttons.import-failed": "One of your buttons could only be imported partially",
+ "firmament.inventory-buttons.info": "Hold SHIFT to grid align",
"firmament.inventory-buttons.load-preset": "Load Preset",
+ "firmament.inventory-buttons.reset": "Reset buttons",
"firmament.inventory-buttons.save-preset": "Save Preset",
+ "firmament.inventory-buttons.simple-preset": "Simple Preset",
"firmament.key.category": "Firmament",
"firmament.keybinding.external": "%s",
- "firmament.mixins.start": "Applied firmament mixins:",
"firmament.modapi.event": "Received mod API event: %s",
"firmament.poweruser.entity.armor": "Entity Armor:",
"firmament.poweruser.entity.armor.item": " - %s",
@@ -299,7 +442,6 @@
"firmament.price.lowestbin": "Lowest BIN: %s",
"firmament.pristine-profit.collection": "Collection: %s/h",
"firmament.pristine-profit.money": "Money: %s/h",
- "firmament.protectitem": "Firmament protected your item: ",
"firmament.pv.lookingup": "Looking up %s",
"firmament.pv.noplayer": "%s is not a Minecraft player",
"firmament.pv.noprofile": "%s has no SkyBlock profiles",
@@ -331,8 +473,6 @@
"firmament.recipe.mobs.name": "§8[§7Lv %d§8] §c%s",
"firmament.recipe.mobs.name.nolevel": "§c%s",
"firmament.recipe.novanilla": "Hypixel cannot super craft vanilla recipes",
- "firmament.recipecategory.reforge": "Reforge",
- "firmament.recipecategory.reforge.basic": "This is a basic reforge, available at the Blacksmith.",
"firmament.reiwarning": "Firmament needs RoughlyEnoughItems to display its item list!",
"firmament.reiwarning.disable": "Click here to disable this warning",
"firmament.reiwarning.disabled": "Disabled the RoughlyEnoughItems warning. Keep in mind that you will not have an item list without REI.",
@@ -348,14 +488,15 @@
"firmament.sbinfo.server": "Locraw Server: %s",
"firmament.toggle.false": "Off",
"firmament.toggle.true": "On",
- "firmament.tooltip.ah.lowestbin": "Lowest BIN: %d",
- "firmament.tooltip.bazaar.buy-order": "Bazaar Buy Order: %s",
- "firmament.tooltip.bazaar.sell-order": "Bazaar Sell Order: %s",
"firmament.tooltip.copied.lore": "Copied Name and Lore",
"firmament.tooltip.copied.modelid": "Copied Texture Id: %s",
"firmament.tooltip.copied.modelid.fail": "Failed to copy Texture Id",
"firmament.tooltip.copied.nbt": "Copied NBT data",
"firmament.tooltip.copied.skull": "Copied Skull Id: %s",
+ "firmament.tooltip.copied.skull-id": "Copied Skull Id: %s",
+ "firmament.tooltip.copied.skull-id.fail.no-profile": "Skull has no profile",
+ "firmament.tooltip.copied.skull-id.fail.no-skull": "That isn't a skull",
+ "firmament.tooltip.copied.skull-id.fail.no-texture": "Skull has no texture",
"firmament.tooltip.copied.skull.fail": "Failed to copy skull id.",
"firmament.tooltip.copied.skyblockid": "Copied SkyBlock Id: %s",
"firmament.tooltip.copied.skyblockid.fail": "Failed to copy SkyBlock Id",
@@ -369,5 +510,5 @@
"firmament.warp-util.mark-excluded": "Firmament: Tried to warp to %s, but it was not unlocked. I will avoid warping there again.",
"firmament.warp-util.no-warp-found": "Could not find an unlocked warp in %s",
"firmament.waypoint.temporary": "Temporary Waypoint: %s",
- "firmanent.config.edit": "Edit"
+ "zzzzzzzzz.lastentry": "Here so every real firmament entry has a trailing ,"
}
diff --git a/translations/extra.json b/translations/extra.json
new file mode 100644
index 0000000..cb21fc9
--- /dev/null
+++ b/translations/extra.json
@@ -0,0 +1,6 @@
+{
+ // These are require by jade, but i don't think they are actually rendered in game.
+ // Jade throws exceptions if they are not present however.
+ "config.jade.plugin_firmament.toolprovider": "Firmament Tool Provider",
+ "config.jade.plugin_firmament.custom_mining_hardness": "Firmament Mining Hardness"
+}
diff --git a/web/src/pages/docs/_texture-pack-format.md b/web/src/pages/docs/_texture-pack-format.md
index da66043..2f84777 100644
--- a/web/src/pages/docs/_texture-pack-format.md
+++ b/web/src/pages/docs/_texture-pack-format.md
@@ -139,11 +139,31 @@ Filter by item type:
"firmament:item": "minecraft:clock"
```
+#### Skulls
+
+You can match skulls using the skull textures and other properties using the skull predicate. If there are no properties specified this is equivalent to checking if the item is a `minecraft:player_head`.
+
+```json
+"firmament:skull": {
+ "profileId": "cca2d452-c6d3-39cb-b695-5ec92b2d6729",
+ "textureProfileId": "1d5233d388624bafb00e3150a7aa3a89",
+ "skinUrl": "http://textures.minecraft.net/texture/7bf01c198f6e16965e230235cd22a5a9f4a40e40941234478948ff9a56e51775",
+ "textureValue": "ewogICJ0aW1lc3RhbXAiIDogMTYxODUyMTY2MzY1NCwKICAicHJvZmlsZUlkIiA6ICIxZDUyMzNkMzg4NjI0YmFmYjAwZTMxNTBhN2FhM2E4OSIsCiAgInByb2ZpbGVOYW1lIiA6ICIwMDAwMDAwMDAwMDAwMDBKIiwKICAic2lnbmF0dXJlUmVxdWlyZWQiIDogdHJ1ZSwKICAidGV4dHVyZXMiIDogewogICAgIlNLSU4iIDogewogICAgICAidXJsIiA6ICJodHRwOi8vdGV4dHVyZXMubWluZWNyYWZ0Lm5ldC90ZXh0dXJlLzdiZjAxYzE5OGY2ZTE2OTY1ZTIzMDIzNWNkMjJhNWE5ZjRhNDBlNDA5NDEyMzQ0Nzg5NDhmZjlhNTZlNTE3NzUiLAogICAgICAibWV0YWRhdGEiIDogewogICAgICAgICJtb2RlbCIgOiAic2xpbSIKICAgICAgfQogICAgfQogIH0KfQ"
+}
+```
+
+| Name | Type | Description |
+|--------------------|---------------------------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
+| `profileId` | UUID | Match the uuid of the profile component directly. |
+| `textureProfileId` | UUID | Match the uuid of the skin owner in the encoded texture value. This is more expensive, but can deviate from the profile id of the profile owner. |
+| `skinUrl` | [string](#string-matcher) | Match the texture url of the skin. This starts with `http://`, not with `https:/` in most cases. |
+| `textureValue` | [string](#string-matcher) | Match the texture value. This is the encoded base64 string of the texture url along with metadata. It is faster to query than the `skinUrl`, but it can out of changed without causing any semantic changes, and is less readable than the skinUrl. |
+
#### Extra attributes
Filter by extra attribute NBT data:
-Specify a `path` to look at, separating sub elements with a `.`. You can use a `*` to check any child.
+Specify a `path` (using an [nbt prism](#nbt-prism)) to look at, separating sub elements with a `.`. You can use a `*` to check any child.
Then either specify a `match` sub-object or directly inline that object in the format of an [nbt matcher](#nbt-matcher).
@@ -167,6 +187,32 @@ Sub object match:
}
```
+#### Components
+
+You can match generic components similarly to [extra attributes](#extra-attributes). If you want to match an extra
+attribute match directly using that, for better performance.
+
+You can specify a `path` (using an [nbt prism](#nbt-prism)) and match similar to extra attributes, but in addition you can also specify a `component`. This
+variable is the identifier of a component type that will then be encoded to nbt and matched according to the `match`
+using a [nbt matcher](#nbt-matcher).
+
+```json5
+"firmament:component": {
+ "path": "rgb",
+ "component": "minecraft:dyed_color",
+ "int": 255
+}
+// Alternatively
+"firmament:component": {
+ "path": "rgb",
+ "component": "minecraft:dyed_color",
+ "match": {
+ "int": 255
+ }
+}
+```
+
+
#### Pet Data
Filter by pet information. While you can already filter by the skyblock id for pet type and tier, this allows you to
@@ -310,6 +356,58 @@ compare your number:
This example would match if the level is less than fifty. The available operators are `<`, `>`, `<=` and `>=`. The
operator needs to be specified on the left. The versions of the operator with `=` also allow the number to be equal.
+### Nbt Prism
+
+An nbt prism (or path) is used to specify where in a complex nbt construct to look for a value. A basic prism just looks
+like a dot-separated path (`parent.child.grandchild`), but more complex paths can be constructed.
+
+First the specified path is split into dot separated chunks: `"a.b.c"` -> `["a", "b", "c"]`. You can also directly
+specify the list if you would like. Any entry in that list not starting with a `*` is treated as an attribute name or
+an index:
+
+```json
+{
+ "propA": {
+ "propB": {
+ "propC": 100,
+ "propD": 1000
+ }
+ },
+ "someOtherProp": "hello",
+ "someThirdProp": "{\"innerProp\": true}",
+ "someFourthProp": "aGlkZGVuIHZhbHVl"
+}
+```
+
+In this example json (which is supposed to represent a corresponding nbt object), you can use a path like
+`propA.propB.propC` to directly extract the value `100`.
+
+If you want to extract all of the innermost values of `propB`
+(for example if `propB` was an array instead), you could use `propA.propB.*`. You can use the `*` at any level:
+`*.*.*` for example extracts all properties that are exactly at the third level. In that case you would try to match any
+of the values of `[100, 1000]` to your match object.
+
+Sometimes values are encoded in a non-nbt format inside a string. For those you can use other star based directives like
+`*base64` or `*json` to decode those entries.
+
+`*base64` turns a base64 encoded string into the base64 decoded counterpart. `*json` decodes a string into the json
+object represented by that string. Note that json to nbt conversion isn't always straightforwards and the types can
+end up being mangled (for example what could have been a byte ends up an int).
+
+| Path | Result |
+|---------------------------------|---------------------------------|
+| `propA.propB` | `{"propC": 100, "propD": 1000}` |
+| `propA.propB.propC` | `100` |
+| `propA.*.propC` | `100` |
+| `propA.propB.*` | `100`, `1000` |
+| `someOtherProp` | `"hello"` |
+| `someThirdProp` | "{\"innerProp\": true}" |
+| `someThirdProp.*json` | {"innerProp": true} |
+| `someThirdProp.*json.innerProp` | true |
+| `someFourthProp` | `"aGlkZGVuIHZhbHVl"` |
+| `someFourthProp.*base64` | `"hidden value"` |
+
+
### Nbt Matcher
This matches a single nbt element.
@@ -477,6 +575,152 @@ not screens from other mods. You can also target specific texts via a [string ma
| `overrides.predicate` | true | This is a [string matcher](#string-matcher) that allows you to match on the text you are replacing |
| `overrides.override` | true | This is the replacement color that will be used if the predicate matches. |
+## Screen Layout Replacement
+
+You can change the layout of an entire screen by using screen layout overrides. These get placed in `firmskyblock:overrides/screen_layout/*.json`, with one file per screen. You can match on the title of a screen, the type of screen, replace the background texture (including extending the background canvas further than vanilla allows you) and move slots around.
+
+### Selecting a screen
+
+```json
+{
+ "predicates": {
+ "label": {
+ "regex": "Hyper Furnace"
+ },
+ "screenType": "minecraft:furnace"
+ }
+}
+```
+
+The `label` property is a regular [string matcher](#string-matcher) and matches against the screens title (typically the chest title, or "Crafting" for the players inventory).
+
+The `screenType` property is an optional namespaced identifier that allows matching to a [screen type](https://minecraft.wiki/w/Java_Edition_protocol/Inventory#Types).
+
+### Changing the background
+
+```json
+{
+ "predicates": {
+ "label": {
+ "regex": "Hyper Furnace"
+ }
+ },
+ "background": {
+ "texture": "firmskyblock:textures/furnace.png",
+ "x": -21,
+ "y": -30,
+ "width": 197,
+ "height": 196
+ }
+}
+```
+
+You need to specify an x and y offset relative to where the regular screen would render. This means you just check where the upper left corner of the UI texture would be in your texture (and turn it into a negative number). You also need to specify a width and height of your texture. This is the width in pixels rendered. If you want a higher or lower resolution texture, you can scale the actual texture up (tho it is expected to meet the same aspect ratio as the one defined here).
+
+### Moving slots around
+
+```json
+{
+ "predicates": {
+ "label": {
+ "regex": "Hyper Furnace"
+ }
+ },
+ "slots": [
+ {
+ "index": 10,
+ "x": -5000,
+ "y": -5000
+ }
+ ]
+}
+```
+
+You can move slots around by a specific index. This is not the index in the inventory, but rather the index in the screen (so if you have a chest screen then all the player inventory slots would be a higher index since the chest slots move them down the list). The x and y are relative to where the regular screen top left would be. Set to large values to effectively "delete" a slot by moving it offscreen.
+
+### Moving text around
+
+```json
+{
+ "predicates": {
+ "label": {
+ "regex": "Hyper Furnace"
+ }
+ },
+ "playerTitle": {
+ "x": 0,
+ "y": 0,
+ "align": "left",
+ "replace": "a"
+ }
+}
+```
+
+You can move the window title around. The x and y are relative to the top left of the regular screen (like slots). Set to large values to effectively "delete" a slot by moving it offscreen.
+
+The align only specifies the direction the text grows in, it does not the actual anchor point, so if you want right aligned text you will also need to move the origin of the text to the right (or it will just grow out of the left side of your screen).
+
+You can replace the text with another text to render instead.
+
+Available titles are
+
+- `containerTitle` for the title of the open container, typically at the very top.
+- `playerTitle` for the players inventory title. Note that in the player inventory without a chest or something open, the `containerTitle` is also used for the "Crafting" text.
+- `repairCostTitle` for the repair cost label in anvils.
+
+### Moving components around
+
+```json
+{
+ "predicates": {
+ "label": {
+ "regex": "Hyper Furnace"
+ }
+ },
+ "nameField": {
+ "x": 10,
+ "y": 10,
+ "width": 100,
+ "height": 12
+ }
+}
+```
+
+Some other components can also be moved. These components might be buttons, text inputs or other things not fitting into any category. They can have a x, y (relative to the top left of the screen), as well as sometimes a width, height, and other properties. This is more of a wild card category, and which options work depends on the type of object.
+
+Available options
+
+- `nameField`: x, y, width & height are all available to move the field to set the name of the item in an anvil.
+
+### All together
+
+| Field | Required | Description |
+|---------------------------|----------|--------------------------------------------------------------------------------------------------------------------------|
+| `predicates` | true | A list of predicates that need to match in order to change the layout of a screen |
+| `predicates.label` | true | A [string matcher](#string-matcher) for the screen title |
+| `background` | false | Allows replacing the background texture |
+| `background.texture` | true | The texture of the background as an identifier |
+| `background.x` | true | The x offset of the background relative to where the regular background would be rendered. |
+| `background.y` | true | The y offset of the background relative to where the regular background would be rendered. |
+| `background.width` | true | The width of the background texture. |
+| `background.height` | true | The height of the background texture. |
+| `slots` | false | An array of slots to move around. |
+| `slots[*].index` | true | The index in the array of all slots on the screen (not inventory). |
+| `slots[*].x` | true | The x coordinate of the slot relative to the top left of the screen |
+| `slots[*].y` | true | The y coordinate of the slot relative to the top left of the screen |
+| `<element>Title` | false | The title mover (see above for valid options) |
+| `<element>Title.x` | false | The x coordinate of text relative to the top left of the screen |
+| `<element>Title.y` | false | The y coordinate of text relative to the top left of the screen |
+| `<element>Title.align` | false | How you want the text to align. "left", "center" or "right". This only changes the text direction, not its anchor point. |
+| `<element>Title.replace` | false | Replace the text with your own text |
+| `<extraComponent>` | false | Allows you to move button components and similar around |
+| `<extraComponent>.x` | true | The new x coordinate of the component relative to the top left of the screen |
+| `<extraComponent>.x` | true | The new y coordinate of the component relative to the top left of the screen |
+| `<extraComponent>.width` | false | The new width of the component |
+| `<extraComponent>.height` | false | The new height of the component |
+
+
+
## Global Item Texture Replacement
Most texture replacement is done based on the SkyBlock id of the item. However, some items you might want to re-texture