aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.editorconfig1
-rw-r--r--README.md21
-rw-r--r--REUSE.toml4
-rw-r--r--build.gradle.kts118
-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.toml63
-rw-r--r--gradle/wrapper/gradle-wrapper.properties2
-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/DrillToolProvider.kt5
-rw-r--r--src/compat/moulconfig/java/MCConfigEditorIntegration.kt2
-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.kt49
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/NEUItemEntryRenderer.kt54
-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.kt16
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockCraftingRecipeDynamicGenerator.kt22
-rw-r--r--src/compat/rei/java/moe/nea/firmament/compat/rei/SkyblockItemIdFocusedStackProvider.kt1
-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.kt62
-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.kt7
-rw-r--r--src/compat/wildfireGender/java/moe/nea/firmament/compat/gender/Compat.kt13
-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/DispatchMouseInputEventsPatch.java17
-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/MinecraftInitLevelListener.java26
-rw-r--r--src/main/java/moe/nea/firmament/mixins/MixinRecipeBookScreen.java16
-rw-r--r--src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java2
-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/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/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/kotlin/Compat.kt11
-rw-r--r--src/main/kotlin/Firmament.kt21
-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.kt44
-rw-r--r--src/main/kotlin/events/CustomItemModelEvent.kt34
-rw-r--r--src/main/kotlin/events/EntityUpdateEvent.kt24
-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.kt25
-rw-r--r--src/main/kotlin/features/chat/ChatLinks.kt18
-rw-r--r--src/main/kotlin/features/debug/AnimatedClothingScanner.kt182
-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.kt47
-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/carnival/MinesweeperHelper.kt4
-rw-r--r--src/main/kotlin/features/fixes/Fixes.kt33
-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.kt57
-rw-r--r--src/main/kotlin/features/inventory/PriceData.kt148
-rw-r--r--src/main/kotlin/features/inventory/REIDependencyWarner.kt4
-rw-r--r--src/main/kotlin/features/inventory/SlotLocking.kt7
-rw-r--r--src/main/kotlin/features/inventory/TimerInLore.kt32
-rw-r--r--src/main/kotlin/features/inventory/WardrobeKeybinds.kt56
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButton.kt129
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt68
-rw-r--r--src/main/kotlin/features/inventory/buttons/InventoryButtons.kt44
-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.kt199
-rw-r--r--src/main/kotlin/features/inventory/storageoverlay/VirtualInventory.kt7
-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/PickaxeAbility.kt16
-rw-r--r--src/main/kotlin/features/misc/CustomCapes.kt180
-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/FirmWaypointManager.kt168
-rw-r--r--src/main/kotlin/features/world/FirmWaypoints.kt37
-rw-r--r--src/main/kotlin/features/world/TemporaryWaypoints.kt73
-rw-r--r--src/main/kotlin/features/world/Waypoints.kt345
-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.kt2
-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/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.kt117
-rw-r--r--src/main/kotlin/repo/MiningRepoData.kt2
-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.kt26
-rw-r--r--src/main/kotlin/repo/RepoModResourcePack.kt2
-rw-r--r--src/main/kotlin/repo/SBItemStack.kt54
-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.kt8
-rw-r--r--src/main/kotlin/util/HoveredItemStack.kt2
-rw-r--r--src/main/kotlin/util/LegacyTagWriter.kt103
-rw-r--r--src/main/kotlin/util/MC.kt27
-rw-r--r--src/main/kotlin/util/MoulConfigUtils.kt67
-rw-r--r--src/main/kotlin/util/SkyblockId.kt135
-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/InitLevel.kt25
-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.kt7
-rw-r--r--src/main/kotlin/util/render/CustomRenderLayers.kt104
-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.kt61
-rw-r--r--src/main/kotlin/util/render/TintedOverlayTexture.kt11
-rw-r--r--src/main/kotlin/util/skyblock/SackUtil.kt2
-rw-r--r--src/main/kotlin/util/skyblock/SkyBlockItems.kt1
-rw-r--r--src/main/kotlin/util/skyblock/TabListAPI.kt41
-rw-r--r--src/main/kotlin/util/textutil.kt53
-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/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.json2
-rw-r--r--src/main/resources/firmament.accesswidener9
-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.png (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/player_inventory.png)bin639 -> 639 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png)bin147 -> 147 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/scroll_bar_background.png.mcmeta)0
-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.mcmeta (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png.mcmeta)0
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png)bin203 -> 203 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_row.png.mcmeta)0
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png)bin795 -> 795 bytes
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta (renamed from src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/upper_background.png.mcmeta)0
-rw-r--r--src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta (renamed from src/main/resources/resourcepacks/transparent_storage/pack.mcmeta)2
-rw-r--r--src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.pngbin624 -> 0 bytes
-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.snbt3
-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.snbt3
-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.kt21
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomGlobalTextures.kt1
-rw-r--r--src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomModelOverrideParser.kt8
-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.kt3
-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.kt2
-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.kt11
-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/CastPredicate.kt2
-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/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.java1
-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.java2
-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.java2
-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.json113
-rw-r--r--translations/extra.json6
-rw-r--r--web/src/pages/docs/_texture-pack-format.md246
288 files changed, 15121 insertions, 2933 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/README.md b/README.md
index 94f501f..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/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 4b5a351..3a72ed0 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -7,7 +7,7 @@
*/
import com.google.common.hash.Hashing
-import com.google.devtools.ksp.gradle.KspTaskJvm
+import com.google.devtools.ksp.gradle.KspAATask
import com.google.gson.Gson
import com.google.gson.JsonObject
import moe.nea.licenseextractificator.LicenseDiscoveryTask
@@ -15,10 +15,9 @@ 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.Base64
+import java.util.*
plugins {
java
@@ -29,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())
@@ -127,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)
}
@@ -139,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
@@ -163,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)
@@ -181,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
}
@@ -192,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")
@@ -219,8 +228,10 @@ 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")
@@ -254,14 +265,14 @@ dependencies {
include(libs.hypixelmodapi.fabric)
compileOnly(projects.javaplugin)
annotationProcessor(projects.javaplugin)
- implementation("com.google.auto.service:auto-service-annotations:1.1.1")
+ 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)
@@ -269,7 +280,6 @@ dependencies {
modCompileOnly(libs.fabric.api)
modRuntimeOnly(libs.fabric.api.deprecated)
- modApi(libs.architectury)
modCompileOnly(libs.jarvis.api)
include(libs.jarvis.fabric)
@@ -286,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()}"
@@ -312,8 +320,8 @@ 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(projects.symbols)
ksp(projects.symbols)
@@ -327,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)
@@ -364,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)
}
}
@@ -383,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")
@@ -402,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")
@@ -457,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 {
@@ -499,16 +523,20 @@ fun patchRenderDoc(
if (!fileF.exists()) {
fileF.parentFile.mkdirs()
if (isWindows) {
- fileF.writeText("""
+ fileF.writeText(
+ """
setlocal enableextensions
start "" renderdoccmd.exe capture --opt-hook-children --wait-for-exit --working-dir . "$wrappedJavaExecutable" %*
endlocal
- """.trimIndent())
+ """.trimIndent()
+ )
} else {
- fileF.writeText("""
+ fileF.writeText(
+ """
#!/usr/bin/env bash
exec renderdoccmd capture --opt-hook-children --wait-for-exit --working-dir . "$wrappedJavaExecutable" "$@"
- """.trimIndent())
+ """.trimIndent()
+ )
fileF.setExecutable(true)
}
}
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 dc1f36d..9062f7b 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.10"
+kotlin = "2.1.20"
# Update from https://github.com/google/ksp/releases
-kotlin_ksp = "2.1.10-1.0.30"
+kotlin_ksp = "2.1.20-2.0.0"
# Update from https://linkie.shedaniel.me/dependencies?loader=fabric
-fabric_loader = "0.16.10"
-fabric_api = "0.117.0+1.21.4"
-yarn = "1.21.4+build.8"
-modmenu = "13.0.2"
-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.1+kotlin.2.1.10"
+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,36 +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 = "17.2.2+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.7.0"
+neurepoparser = "1.8.0"
# Update from https://github.com/HotswapProjects/HotswapAgent/releases
# TODO: bump to 2.0.1
@@ -70,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.8.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"
@@ -84,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" }
@@ -103,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" }
@@ -139,7 +143,7 @@ asm = { module = "org.ow2.asm:asm", version.ref = "asm" }
[bundles]
runtime_required = [
- "rei_fabric",
+ # "rei_fabric",
# "notenoughanimations",
"hypixelmodapi_fabric",
]
@@ -147,8 +151,8 @@ runtime_optional = [
"devauth",
# "freecammod",
# "sodium",
- # "qolify",
- "ncr",
+ # "qolify",
+ # "ncr",
# "citresewn",
]
@@ -159,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/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/DrillToolProvider.kt b/src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt
index ab45e7c..10bff1b 100644
--- a/src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt
+++ b/src/compat/jade/java/moe/nea/firmament/compat/jade/DrillToolProvider.kt
@@ -13,18 +13,17 @@ 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.item.ItemStack
-import net.minecraft.item.Items
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.ItemCache.asItemStack
+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,
diff --git a/src/compat/moulconfig/java/MCConfigEditorIntegration.kt b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt
index dec2559..46b95de 100644
--- a/src/compat/moulconfig/java/MCConfigEditorIntegration.kt
+++ b/src/compat/moulconfig/java/MCConfigEditorIntegration.kt
@@ -313,7 +313,7 @@ class MCConfigEditorIntegration : FirmamentConfigScreenProvider {
}
override fun getTitle(): String {
- return "Firmament"
+ return "Firmament ${Firmament.version.friendlyString}"
}
@Deprecated("Deprecated in java")
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 aade59e..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,18 +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
@@ -41,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())
}
@@ -48,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
@@ -75,13 +85,17 @@ 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)
}
@@ -91,17 +105,14 @@ 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(
@@ -110,10 +121,6 @@ class FirmamentReiPlugin : REIClientPlugin {
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 35a1e1b..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,10 +17,14 @@ 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 net.minecraft.text.Text
-import moe.nea.firmament.compat.rei.FirmamentReiPlugin.Companion.asItemEntry
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
@@ -31,6 +35,7 @@ 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,
@@ -39,15 +44,25 @@ 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)
- val item = entry.asItemEntry().value
- context.drawItemWithoutEntity(item, -8, -8)
- context.drawStackOverlay(minecraft.textRenderer, item, -8, -8,
- if (entry.value.getStackSize() > 1000) FirmFormatters.shortFormat(entry.value.getStackSize()
- .toDouble())
- else null
+ 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()
}
@@ -55,7 +70,18 @@ object NEUItemEntryRenderer : EntryRenderer<SBItemStack> {
val minecraft = MinecraftClient.getInstance()
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)
@@ -70,12 +96,14 @@ object NEUItemEntryRenderer : EntryRenderer<SBItemStack> {
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())
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 2b1700d..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,6 +15,7 @@ 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.ExpensiveItemCacheApi
import moe.nea.firmament.repo.RepoManager
import moe.nea.firmament.repo.SBItemStack
import moe.nea.firmament.util.SkyblockId
@@ -24,6 +25,7 @@ object SBItemEntryDefinition : EntryDefinition<SBItemStack> {
return o1.skyblockId == o2.skyblockId && o1.getStackSize() == o2.getStackSize()
}
+ @OptIn(ExpensiveItemCacheApi::class)
override fun cheatsAs(entry: EntryStack<SBItemStack>?, value: SBItemStack): ItemStack {
return value.asCopiedItemStack()
}
@@ -41,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 {
@@ -51,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(), reforge = null)
+ 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 {
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 e80840f..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,6 +1,5 @@
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
@@ -11,9 +10,6 @@ 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
@@ -22,33 +18,27 @@ 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) }
val SkyblockShopRecipeDynamicGenerator =
neuDisplayGenerator<SBShopRecipe, NEUNpcShopRecipe> { SBShopRecipe(it) }
val SkyblockKatRecipeDynamicGenerator =
neuDisplayGenerator<SBKatRecipe, NEUKatUpgradeRecipe> { SBKatRecipe(it) }
-val SkyblockEssenceRecipeDynamicGenerator =
- neuDisplayGeneratorWithItem<SBEssenceUpgradeRecipe, EssenceRecipeProvider.EssenceUpgradeRecipe> { item, recipe ->
- SBEssenceUpgradeRecipe(recipe, item)
- }
inline fun <D : Display, reified T : NEURecipe> neuDisplayGenerator(crossinline mapper: (T) -> D) =
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>()
+ val craftingRecipes = recipes.filterIsInstance<T>(filter)
return Optional.of(craftingRecipes.map { mapper(item, it) })
}
@@ -60,7 +50,7 @@ inline fun <D : Display, reified T : NEURecipe> neuDisplayGeneratorWithItem(cros
if (entry.type != SBItemEntryDefinition.type) return Optional.empty()
val item = entry.castValue<SBItemStack>()
val recipes = RepoManager.getUsagesFor(item.skyblockId)
- val craftingRecipes = recipes.filterIsInstance<T>()
+ 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 518f7b4..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<*>> {
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 c02e078..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, "crafting_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 e0a3784..0000000
--- a/src/compat/rei/java/moe/nea/firmament/compat/rei/recipes/SBEssenceUpgradeRecipe.kt
+++ /dev/null
@@ -1,62 +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,
- val sourceItem: SBItemStack) : 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(display.sourceItem.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(display.sourceItem.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 7a0ec78..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 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)
- add(Widgets.createBurningFire(ingredientsCenter).animationDurationTicks(25.0))
- 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 b8313a6..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
@@ -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
),
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/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/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/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/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/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 b20c223..f07604e 100644
--- a/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java
+++ b/src/main/java/moe/nea/firmament/mixins/PlayerDropEventPatch.java
@@ -20,7 +20,7 @@ public abstract class PlayerDropEventPatch extends PlayerEntity {
@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);
+ 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/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/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/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/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 0191036..b00546a 100644
--- a/src/main/kotlin/Firmament.kt
+++ b/src/main/kotlin/Firmament.kt
@@ -51,6 +51,7 @@ 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 {
@@ -66,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
@@ -73,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
}
@@ -119,6 +135,7 @@ object Firmament {
@JvmStatic
fun onClientInitialize() {
+ InitLevel.bump(InitLevel.MC_INIT)
FeatureManager.subscribeEvents()
ClientTickEvents.END_CLIENT_TICK.register(ClientTickEvents.EndTick { instance ->
TickEvent.publish(TickEvent(MC.currentTick++))
@@ -148,9 +165,9 @@ object Firmament {
})
ClientInitEvent.publish(ClientInitEvent())
ResourceManagerHelper.registerBuiltinResourcePack(
- identifier("transparent_storage"),
+ identifier("transparent_overlay"),
modContainer,
- tr("firmament.resourcepack.transparentstorage", "Transparent Firmament Storage Overlay"),
+ 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 8ae34f6..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,6 +12,7 @@ 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
@@ -33,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
@@ -130,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 {
@@ -149,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(
@@ -192,7 +204,7 @@ fun firmamentCommand() = literal("firmament") {
}
}
}
- thenLiteral("dev") {
+ thenLiteral(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
thenLiteral("simulate") {
thenArgument("message", RestArgumentType) { message ->
thenExecute {
@@ -218,6 +230,15 @@ 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())
@@ -252,7 +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)}"))
+ source.sendFeedback(tr("firmament.sbinfo.custommining",
+ "Custom Mining: ${formatBool(locrawInfo.skyblockLocation?.hasCustomMining ?: false)}"))
}
}
}
@@ -303,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 21ee326..7b86980 100644
--- a/src/main/kotlin/events/CustomItemModelEvent.kt
+++ b/src/main/kotlin/events/CustomItemModelEvent.kt
@@ -1,10 +1,13 @@
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
@@ -14,7 +17,36 @@ data class CustomItemModelEvent(
var overrideModel: Identifier? = null,
) : FirmamentEvent() {
companion object : FirmamentEventBus<CustomItemModelEvent>() {
- val cache = WeakCache.memoize("ItemModelIdentifier", ::getModelIdentifier0)
+ 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?, itemModelManager: IntrospectableItemModelManager): Identifier? {
diff --git a/src/main/kotlin/events/EntityUpdateEvent.kt b/src/main/kotlin/events/EntityUpdateEvent.kt
index 27a90f9..fec2fa5 100644
--- a/src/main/kotlin/events/EntityUpdateEvent.kt
+++ b/src/main/kotlin/events/EntityUpdateEvent.kt
@@ -7,6 +7,8 @@ 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.
@@ -15,7 +17,27 @@ 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
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 1b39d4e..85a9784 100644
--- a/src/main/kotlin/features/FeatureManager.kt
+++ b/src/main/kotlin/features/FeatureManager.kt
@@ -25,12 +25,15 @@ 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.mining.PickaxeAbility
import moe.nea.firmament.features.mining.PristineProfitTracker
+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) {
@@ -59,7 +62,6 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
loadFeature(PowerUserTools)
loadFeature(Waypoints)
loadFeature(ChatLinks)
- loadFeature(InventoryButtons)
loadFeature(CompatibliltyFeatures)
loadFeature(AnniversaryFeatures)
loadFeature(QuickCommands)
@@ -67,6 +69,8 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
loadFeature(SaveCursorPosition)
loadFeature(PriceData)
loadFeature(Fixes)
+ loadFeature(Hud)
+ loadFeature(WardrobeKeybinds)
loadFeature(DianaWaypoints)
loadFeature(ItemRarityCosmetics)
loadFeature(PickaxeAbility)
@@ -83,17 +87,18 @@ object FeatureManager : DataHolder<FeatureManager.Config>(serializer(), "feature
fun subscribeEvents() {
SubscriptionList.allLists.forEach { list ->
- runCatching {
- list.provideSubscriptions {
- it.owner.javaClass.classes.forEach {
- runCatching { it.getDeclaredField("INSTANCE").get(null) }
+ if (ICompatMeta.shouldLoad(list.javaClass.name))
+ runCatching {
+ list.provideSubscriptions {
+ it.owner.javaClass.classes.forEach {
+ runCatching { it.getDeclaredField("INSTANCE").get(null) }
+ }
+ subscribeSingleEvent(it)
}
- 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)
}
- }.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)
- }
}
}
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/debug/AnimatedClothingScanner.kt b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
index 11b47a9..4edccfb 100644
--- a/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
+++ b/src/main/kotlin/features/debug/AnimatedClothingScanner.kt
@@ -1,51 +1,193 @@
package moe.nea.firmament.features.debug
-import net.minecraft.component.DataComponentTypes
+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.skyBlockId
+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 {
- var observedEntity: Entity? = null
+ 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) {
- if (event.entity != observedEntity) return
+ val s = subject ?: return
+ if (event.entity != s) return
+ val l = lens ?: return
if (event is EntityUpdateEvent.EquipmentUpdate) {
event.newEquipment.forEach {
- val id = it.second.skyBlockId?.neuItem
- val colour = it.second.get(DataComponentTypes.DYED_COLOR)
- ?.rgb?.toHexString(HexFormat.UpperCase)
- ?.let { " #$it" } ?: ""
- MC.sendChat(tr("firmament.fitstealer.update",
- "[FIT CHECK][${MC.currentTick}] ${it.first.asString()} => ${id}${colour}"))
+ 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("dev") {
+ event.subcommand(DeveloperFeatures.DEVELOPER_SUBCOMMAND) {
thenLiteral("stealthisfit") {
- thenExecute {
- observedEntity =
- if (observedEntity == null) MC.instance.targetedEntity else null
-
- MC.sendChat(
- observedEntity?.let {
- tr("firmament.fitstealer.targeted", "Observing the equipment of ${it.name}.")
- } ?: tr("firmament.fitstealer.targetlost", "No longer logging equipment."),
- )
+ 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 8be5d5d..7c1df3f 100644
--- a/src/main/kotlin/features/debug/PowerUserTools.kt
+++ b/src/main/kotlin/features/debug/PowerUserTools.kt
@@ -1,31 +1,25 @@
package moe.nea.firmament.features.debug
-import com.mojang.serialization.Codec
-import com.mojang.serialization.DynamicOps
import com.mojang.serialization.JsonOps
-import com.mojang.serialization.codecs.RecordCodecBuilder
import kotlin.jvm.optionals.getOrNull
import net.minecraft.block.SkullBlock
import net.minecraft.block.entity.SkullBlockEntity
import net.minecraft.component.DataComponentTypes
import net.minecraft.component.type.ProfileComponent
import net.minecraft.entity.Entity
-import net.minecraft.entity.EntityType
import net.minecraft.entity.LivingEntity
import net.minecraft.item.ItemStack
import net.minecraft.item.Items
-import net.minecraft.nbt.NbtCompound
+import net.minecraft.nbt.NbtList
import net.minecraft.nbt.NbtOps
-import net.minecraft.nbt.NbtString
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
-import net.minecraft.util.math.Position
-import net.minecraft.util.math.Vec3d
import moe.nea.firmament.annotations.Subscribe
import moe.nea.firmament.events.CustomItemModelEvent
import moe.nea.firmament.events.HandledScreenKeyPressedEvent
@@ -43,8 +37,10 @@ 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
@@ -59,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
@@ -67,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
@@ -108,7 +107,7 @@ object PowerUserTools : FirmamentFeature {
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)))
}
}
@@ -179,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"))
}
}
@@ -223,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/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 3dae233..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,10 +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
@@ -33,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
@@ -43,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..bb39fbc 100644
--- a/src/main/kotlin/features/inventory/PetFeatures.kt
+++ b/src/main/kotlin/features/inventory/PetFeatures.kt
@@ -1,14 +1,24 @@
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.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 +29,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 +42,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) 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..2e854b7 100644
--- a/src/main/kotlin/features/inventory/PriceData.kt
+++ b/src/main/kotlin/features/inventory/PriceData.kt
@@ -1,51 +1,121 @@
-
-
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 && !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 0083c40..d3348a2 100644
--- a/src/main/kotlin/features/inventory/SlotLocking.kt
+++ b/src/main/kotlin/features/inventory/SlotLocking.kt
@@ -4,6 +4,7 @@ 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
@@ -17,6 +18,9 @@ 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
@@ -45,7 +49,6 @@ 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
@@ -445,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 309ea61..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 }
}
@@ -81,6 +83,9 @@ object TimerInLore {
CHOCOLATEFACTORY("Next Charge", "Available at"),
STONKSAUCTION("Auction ends in", "Ends at"),
LIZSTONKREDEMPTION("Resets in:", "Resets at"),
+ TIMEREMAININGS("Time Remaining:", "Ends at"),
+ COOLDOWN("Cooldown:", "Come back at"),
+ ONCOOLDOWN("On cooldown:", "Available at"),
EVENTENDING("Event ends in:", "Ends at");
}
@@ -88,6 +93,14 @@ object TimerInLore {
"(?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
@@ -108,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
@@ -120,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..d797600
--- /dev/null
+++ b/src/main/kotlin/features/inventory/WardrobeKeybinds.kt
@@ -0,0 +1,56 @@
+package moe.nea.firmament.features.inventory
+
+import org.lwjgl.glfw.GLFW
+import net.minecraft.item.Items
+import net.minecraft.screen.slot.SlotActionType
+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
+import moe.nea.firmament.util.mc.SlotUtils.clickMiddleMouseButton
+
+object WardrobeKeybinds : FirmamentFeature {
+ override val identifier: String
+ get() = "wardrobe-keybinds"
+
+ object TConfig : ManagedConfig(identifier, Category.INVENTORY) {
+ val wardrobeKeybinds by toggle("wardrobe-keybinds") { false }
+ 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
+
+ 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..74a986a 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButtonEditor.kt
@@ -1,11 +1,14 @@
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.util.InputUtil
@@ -55,6 +58,14 @@ 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(
@@ -83,14 +94,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 +116,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 +133,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)
@@ -180,14 +211,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 +219,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..15f57d9 100644
--- a/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
+++ b/src/main/kotlin/features/inventory/buttons/InventoryButtons.kt
@@ -5,28 +5,33 @@ 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.MinecraftClient
+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.FirmHoverComponent
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 }
}
- object DConfig : DataHolder<Data>(serializer(), identifier, ::Data)
+ object DConfig : DataHolder<Data>(serializer(), "inventory-buttons", ::Data)
@Serializable
data class Data(
@@ -34,9 +39,6 @@ object InventoryButtons : FirmamentFeature {
)
- override val config: ManagedConfig
- get() = TConfig
-
fun getValidButtons() = DConfig.data.buttons.asSequence().filter { it.isValid() }
@Subscribe
@@ -60,16 +62,36 @@ object InventoryButtons : FirmamentFeature {
}
}
+ var lastHoveredComponent: InventoryButton? = null
+ var lastMouseMove = TimeMark.farPast()
+
@Subscribe
fun onRenderForeground(it: HandledScreenForegroundEvent) {
val bounds = it.screen.getRectangle()
+
+ var hoveredComponent: InventoryButton? = null
for (button in getValidButtons()) {
val buttonBounds = button.getBounds(bounds)
it.context.matrices.push()
it.context.matrices.translate(buttonBounds.minX.toFloat(), buttonBounds.minY.toFloat(), 0F)
button.render(it.context)
it.context.matrices.pop()
+
+ 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 +100,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 633a8fe..f1cbea7 100644
--- a/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
+++ b/src/main/kotlin/features/inventory/storageoverlay/StorageOverlayScreen.kt
@@ -47,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
@@ -68,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
}
@@ -100,6 +104,7 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
coerceScroll(StorageOverlay.adjustScrollSpeed(verticalAmount).toFloat())
return true
}
+
fun coerceScroll(offset: Float) {
scroll = (scroll + offset)
.coerceAtMost(getMaxScroll())
@@ -159,11 +164,16 @@ class StorageOverlayScreen : Screen(Text.literal("")) {
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,
@@ -186,25 +196,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> {
@@ -218,7 +234,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)
@@ -227,17 +243,21 @@ 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) {
@@ -257,12 +277,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()
@@ -282,11 +303,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)
}
@@ -322,11 +345,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
}
@@ -357,6 +382,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,
@@ -416,7 +445,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(
@@ -448,22 +477,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))
@@ -476,22 +524,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/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/PickaxeAbility.kt b/src/main/kotlin/features/mining/PickaxeAbility.kt
index 1737969..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(),
@@ -170,6 +175,11 @@ object PickaxeAbility : FirmamentFeature {
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!"))
+ )
}
}
diff --git a/src/main/kotlin/features/misc/CustomCapes.kt b/src/main/kotlin/features/misc/CustomCapes.kt
new file mode 100644
index 0000000..a20707e
--- /dev/null
+++ b/src/main/kotlin/features/misc/CustomCapes.kt
@@ -0,0 +1,180 @@
+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.util.MC
+import moe.nea.firmament.util.TimeMark
+
+object CustomCapes {
+ 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 = byUuid[player.uuid]
+ 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/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..b36c49d
--- /dev/null
+++ b/src/main/kotlin/features/world/TemporaryWaypoints.kt
@@ -0,0 +1,73 @@
+package moe.nea.firmament.features.world
+
+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, 0xFFFFFF00.toInt())
+ }
+ temporaryPlayerWaypointList.forEach { (player, waypoint) ->
+ val skin =
+ MC.networkHandler?.listedPlayerListEntries?.find { it.profile.name == player }?.skinTextures?.texture
+ withFacingThePlayer(waypoint.pos.toCenterPos()) {
+ waypoint(waypoint.pos, Text.stringifiedTranslatable("firmament.waypoint.temporary", player))
+ if (skin != null) {
+ matrixStack.translate(0F, -20F, 0F)
+ // Head front
+ texture(
+ skin, 16, 16,
+ 1 / 8f, 1 / 8f,
+ 2 / 8f, 2 / 8f,
+ )
+ // Head overlay
+ texture(
+ skin, 16, 16,
+ 5 / 8f, 1 / 8f,
+ 6 / 8f, 2 / 8f,
+ )
+ }
+ }
+ }
+ }
+ }
+
+ @Subscribe
+ fun onWorldReady(event: WorldReadyEvent) {
+ temporaryPlayerWaypointList.clear()
+ }
+
+}
diff --git a/src/main/kotlin/features/world/Waypoints.kt b/src/main/kotlin/features/world/Waypoints.kt
index 2e4cb70..b5c2b66 100644
--- a/src/main/kotlin/features/world/Waypoints.kt
+++ b/src/main/kotlin/features/world/Waypoints.kt
@@ -2,36 +2,24 @@ 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 kotlinx.serialization.encodeToString
-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
@@ -43,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, 0x800050A0.toInt())
+ 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
}
}
@@ -144,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"))
@@ -189,118 +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("export") {
- thenExecute {
- val data = Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints.map {
- ColeWeightWaypoint(it.x,
- it.y,
- it.z)
- })
- ClipboardUtils.setTextContent(data)
- source.sendFeedback(tr("firmament.command.waypoint.export",
- "Copied ${waypoints.size} waypoints to clipboard"))
- }
- }
- thenLiteral("exportrelative") {
- thenExecute {
- val playerPos = MC.player!!.blockPos
- val x = playerPos.x
- val y = playerPos.y
- val z = playerPos.z
- val data = Firmament.tightJson.encodeToString<List<ColeWeightWaypoint>>(waypoints.map {
- ColeWeightWaypoint(it.x - x,
- it.y - y,
- it.z - z)
- })
- ClipboardUtils.setTextContent(data)
- source.sendFeedback(tr("firmament.command.waypoint.export.relative",
- "Copied ${waypoints.size} relative waypoints to clipboard. Make sure to stand in the same position when importing."))
-
- }
- }
- thenLiteral("import") {
- thenExecute {
- source.sendFeedback(
- importRelative(BlockPos.ORIGIN)
- ?: Text.stringifiedTranslatable("firmament.command.waypoint.import", waypoints.size),
- )
- }
- }
- thenLiteral("importrelative") {
- thenExecute {
- source.sendFeedback(
- importRelative(MC.player!!.blockPos)
- ?: tr("firmament.command.waypoint.import.relative",
- "Imported ${waypoints.size} 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 importRelative(pos: BlockPos): Text? {
- val contents = ClipboardUtils.getTextContents()
- val data = try {
- Firmament.tightJson.decodeFromString<List<ColeWeightWaypoint>>(contents)
- } catch (ex: Exception) {
- Firmament.logger.error("Could not load waypoints from clipboard", ex)
- return (Text.translatable("firmament.command.waypoint.import.error"))
- }
- waypoints.clear()
- data.mapTo(waypoints) { BlockPos(it.x + pos.x, it.y + pos.y, it.z + pos.z) }
- return null
- }
-
- @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> {
@@ -313,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/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..125eaab 100644
--- a/src/main/kotlin/gui/config/ManagedConfig.kt
+++ b/src/main/kotlin/gui/config/ManagedConfig.kt
@@ -39,6 +39,7 @@ abstract class ManagedConfig(
CHAT,
INVENTORY,
MINING,
+ GARDEN,
EVENTS,
INTEGRATIONS,
META,
@@ -69,6 +70,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/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 0967ad1..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,17 +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
@@ -45,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 {
@@ -61,14 +68,18 @@ object ItemCache : IReloadable {
putShort("Damage", damage.toShort())
}
+ @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, nbtComponent),
-1,
- SharedConstants.getGameVersion().saveVersion.id
+ currentSaveVersion
).value as NbtCompound
} catch (e: Exception) {
isFlawless = false
@@ -131,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()
@@ -150,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]
@@ -183,22 +229,49 @@ object ItemCache : IReloadable {
}
}
- var job: Job? = null
+ var itemRecacheScope: CoroutineScope? = null
- override fun reload(repository: NEURepository) {
- val j = job
- if (j != null && j.isActive) {
- j.cancel()
+ private var recacheSoonSubmitted = mutableSetOf<SkyblockId>()
+
+ @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 = itemRecacheScope
+ j?.cancel("New reload invoked")
cache.clear()
isFlawless = true
if (TestUtil.isInTest) return
- job = Firmament.coroutineScope.launch {
- val items = repository.items?.items ?: return@launch
- items.values.forEach {
- it.asItemStack() // Rebuild cache
- }
+ 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
index e40292d..e96a241 100644
--- a/src/main/kotlin/repo/MiningRepoData.kt
+++ b/src/main/kotlin/repo/MiningRepoData.kt
@@ -81,6 +81,7 @@ class MiningRepoData : IReloadable {
) {
@Transient
val dropItem = baseDrop?.let(::SBItemStack)
+ @OptIn(ExpensiveItemCacheApi::class)
private val labeledStack by lazy {
dropItem?.asCopiedItemStack()?.also(::markItemStack)
}
@@ -110,6 +111,7 @@ class MiningRepoData : IReloadable {
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 {
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 e50a131..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
@@ -55,9 +66,11 @@ 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)
@@ -102,6 +115,13 @@ 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 {
RepoDownloadManager.downloadUpdate(force)
@@ -128,8 +148,10 @@ object RepoManager {
} 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."
+ )
)
}
}
diff --git a/src/main/kotlin/repo/RepoModResourcePack.kt b/src/main/kotlin/repo/RepoModResourcePack.kt
index 2fdf710..4fec14a 100644
--- a/src/main/kotlin/repo/RepoModResourcePack.kt
+++ b/src/main/kotlin/repo/RepoModResourcePack.kt
@@ -24,7 +24,7 @@ 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 {
diff --git a/src/main/kotlin/repo/SBItemStack.kt b/src/main/kotlin/repo/SBItemStack.kt
index 3690866..01d1c4d 100644
--- a/src/main/kotlin/repo/SBItemStack.kt
+++ b/src/main/kotlin/repo/SBItemStack.kt
@@ -225,14 +225,21 @@ data class SBItemStack constructor(
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)
@@ -256,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
@@ -346,7 +353,9 @@ data class SBItemStack constructor(
}
// 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
@@ -357,6 +366,7 @@ data class SBItemStack constructor(
group("power").toInt()
} ?: 0
+ @ExpensiveItemCacheApi
private val itemStack: ItemStack
get() {
val itemStack = itemStack_ ?: run {
@@ -413,19 +423,35 @@ data class SBItemStack constructor(
.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)
+ 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 acb7102..03dafc5 100644
--- a/src/main/kotlin/util/FirmFormatters.kt
+++ b/src/main/kotlin/util/FirmFormatters.kt
@@ -13,6 +13,7 @@ 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 {
@@ -131,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/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 c1a5e65..e85b119 100644
--- a/src/main/kotlin/util/MC.kt
+++ b/src/main/kotlin/util/MC.kt
@@ -1,7 +1,9 @@
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
@@ -16,10 +18,14 @@ 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
@@ -99,7 +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?.inventory?.mainHandStack ?: ItemStack.EMPTY
+ 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
@@ -120,6 +126,25 @@ object MC {
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 362a4d9..51ff340 100644
--- a/src/main/kotlin/util/MoulConfigUtils.kt
+++ b/src/main/kotlin/util/MoulConfigUtils.kt
@@ -35,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) {
@@ -81,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),
)
}
@@ -179,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)
)
}
@@ -196,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)
}
})
}
@@ -210,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 })
@@ -288,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/SkyblockId.kt b/src/main/kotlin/util/SkyblockId.kt
index a31255c..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,28 +26,32 @@ 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) : 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
@@ -53,7 +62,7 @@ value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> {
}
/**
- * 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.
@@ -61,11 +70,10 @@ value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> {
@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(":", "-"))
}
}
@@ -84,7 +92,9 @@ value class SkyblockId(val neuItem: String) : Comparable<SkyblockId> {
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()
@@ -103,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 }
@@ -130,13 +142,38 @@ fun ItemStack.modifyExtraAttributes(block: (NbtCompound) -> Unit) {
}
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)
@@ -145,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
}
@@ -155,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?
@@ -169,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
}
@@ -180,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/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/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 f239810..be6bcfb 100644
--- a/src/main/kotlin/util/regex.kt
+++ b/src/main/kotlin/util/regex.kt
@@ -26,6 +26,13 @@ inline fun <T> Pattern.useMatch(string: String?, block: Matcher.() -> T): T? {
?.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..3d9e598
--- /dev/null
+++ b/src/main/kotlin/util/render/CustomRenderLayers.kt
@@ -0,0 +1,104 @@
+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)
+ .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..98b10ca 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
@@ -27,47 +24,12 @@ 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 +44,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 +117,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 +144,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 +164,6 @@ class RenderInWorldContext private constructor(
.next()
}
- RenderLayers.LINES.draw(buffer.end())
}
// TODO: put the favourite icons in front of items again
@@ -281,16 +241,15 @@ class RenderInWorldContext private constructor(
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 +261,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
index a02eccc..0677846 100644
--- a/src/main/kotlin/util/render/TintedOverlayTexture.kt
+++ b/src/main/kotlin/util/render/TintedOverlayTexture.kt
@@ -1,7 +1,5 @@
package moe.nea.firmament.util.render
-import com.mojang.blaze3d.platform.GlConst
-import com.mojang.blaze3d.systems.RenderSystem
import me.shedaniel.math.Color
import net.minecraft.client.render.OverlayTexture
import net.minecraft.util.math.ColorHelper
@@ -29,16 +27,9 @@ class TintedOverlayTexture : OverlayTexture() {
}
}
- RenderSystem.activeTexture(GlConst.GL_TEXTURE1)
- texture.bindTexture()
texture.setFilter(false, false)
texture.setClamp(true)
- image.upload(0,
- 0, 0,
- 0, 0,
- image.width, image.height,
- false)
- RenderSystem.activeTexture(GlConst.GL_TEXTURE0)
+ texture.upload()
return this
}
}
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 ca2b17b..74e1327 100644
--- a/src/main/kotlin/util/skyblock/SkyBlockItems.kt
+++ b/src/main/kotlin/util/skyblock/SkyBlockItems.kt
@@ -3,6 +3,7 @@ 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")
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 806f61e..cfda2e9 100644
--- a/src/main/kotlin/util/textutil.kt
+++ b/src/main/kotlin/util/textutil.kt
@@ -56,6 +56,7 @@ fun OrderedText.reconstitute(): MutableText {
return base
}
+
fun StringVisitable.reconstitute(): MutableText {
val base = Text.literal("")
base.setStyle(Style.EMPTY.withItalic(false))
@@ -82,15 +83,47 @@ val Text.unformattedString: String
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
@@ -127,13 +160,13 @@ 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(HoverEvent.Action.SHOW_TEXT, text))}
+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))
}
}
@@ -164,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/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 02c11ee..115778f 100644
--- a/src/main/resources/fabric.mod.json
+++ b/src/main/resources/fabric.mod.json
@@ -51,7 +51,7 @@
"firmament.mixins.json"
],
"depends": {
- "fabric": ">=${fabric_api_version}",
+ "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 fd79cb5..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
@@ -26,3 +27,5 @@ accessible method net/minecraft/client/render/RenderPhase$Texture getId ()Ljava/
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_storage/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
index 1831ef3..1831ef3 100644
--- a/src/main/resources/resourcepacks/transparent_storage/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
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_storage/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
index 5b774b2..5b774b2 100644
--- a/src/main/resources/resourcepacks/transparent_storage/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
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_storage/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
index 94b9a1d..94b9a1d 100644
--- a/src/main/resources/resourcepacks/transparent_storage/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
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_storage/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
index 5964a6f..5964a6f 100644
--- a/src/main/resources/resourcepacks/transparent_storage/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
diff --git a/src/main/resources/resourcepacks/transparent_storage/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
index 61e9ee5..61e9ee5 100644
--- a/src/main/resources/resourcepacks/transparent_storage/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
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_storage/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
index cd2857e..cd2857e 100644
--- a/src/main/resources/resourcepacks/transparent_storage/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
diff --git a/src/main/resources/resourcepacks/transparent_storage/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
index 653a99e..653a99e 100644
--- a/src/main/resources/resourcepacks/transparent_storage/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
Binary files differ
diff --git a/src/main/resources/resourcepacks/transparent_storage/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
index a29299d..a29299d 100644
--- a/src/main/resources/resourcepacks/transparent_storage/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
diff --git a/src/main/resources/resourcepacks/transparent_storage/pack.mcmeta b/src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta
index c37df06..035feaa 100644
--- a/src/main/resources/resourcepacks/transparent_storage/pack.mcmeta
+++ b/src/main/resources/resourcepacks/transparent_overlay/pack.mcmeta
@@ -5,6 +5,6 @@
"min_inclusive": 15,
"max_inclusive": 2147483647
},
- "description": "Adds a more transparent storage overlay for /firm storage"
+ "description": "Adds a more transparent overlay for Firmament"
}
}
diff --git a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png b/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
deleted file mode 100644
index d4852d8..0000000
--- a/src/main/resources/resourcepacks/transparent_storage/assets/firmament/textures/gui/sprites/storageoverlay/storage_controls.png
+++ /dev/null
Binary files differ
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
index 1de4632..4a0b7c6 100644
--- a/src/test/resources/testdata/items/books/feather_falling.snbt
+++ b/src/test/resources/testdata/items/books/feather_falling.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
components: {
"minecraft:attribute_modifiers": {
modifiers: [
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
index 35f8cf0..fd740ce 100644
--- a/src/test/resources/testdata/items/necron-boots.snbt
+++ b/src/test/resources/testdata/items/necron-boots.snbt
@@ -1,4 +1,7 @@
{
+ source: {
+ dataVersion: 4189,
+ },
components: {
"minecraft:attribute_modifiers": {
modifiers: [
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 aafc85a..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()
},
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 20f1fb6..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
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 4529d1d..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,12 +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 {
@@ -63,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(
@@ -110,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 bef52d2..18949ff 100644
--- a/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt
+++ b/src/texturePacks/java/moe/nea/firmament/features/texturepack/CustomSkyBlockTextures.kt
@@ -36,6 +36,7 @@ object CustomSkyBlockTextures : FirmamentFeature {
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
@@ -45,7 +46,7 @@ object CustomSkyBlockTextures : FirmamentFeature {
listOf(
skullTextureCache.cache,
CustomItemModelEvent.cache.cache,
- CustomGlobalArmorOverrides.overrideCache.cache
+ // TODO: re-add this once i figure out how to make the cache useful again CustomGlobalArmorOverrides.overrideCache.cache
)
}
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 6cef4ca..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,10 @@
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, 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 0edad4c..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,10 +28,10 @@ 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
@@ -42,7 +41,7 @@ class PredicateModel {
.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/CastPredicate.kt b/src/texturePacks/java/moe/nea/firmament/features/texturepack/predicates/CastPredicate.kt
index 2b79c1a..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
@@ -16,7 +16,7 @@ class CastPredicate : FirmamentModelPredicate {
}
override fun test(stack: ItemStack, holder: LivingEntity?): Boolean {
- return (holder as? PlayerEntity)?.fishHook != null && holder.activeItem === stack
+ return (holder as? PlayerEntity)?.fishHook != null && holder.mainHandStack === stack
}
override fun test(stack: ItemStack): Boolean {
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/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
index f829da0..0fb6bf8 100644
--- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/PatchLegacyTexturePathsIntoArmorLayers.java
@@ -26,7 +26,6 @@ public class PatchLegacyTexturePathsIntoArmorLayers {
// 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) -> {
- String var10000 = layerType.asString();
return "textures/models/armor/" + textureName + "_layer_" +
(layerType == EquipmentModel.LayerType.HUMANOID_LEGGINGS ? 2 : 1)
+ ".png";
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 97abd1f..f2a7409 100644
--- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/ReplaceItemModelPatch.java
@@ -26,7 +26,7 @@ public class ReplaceItemModelPatch implements IntrospectableItemModelManager {
private Function<Identifier, ItemModel> modelGetter;
@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, this);
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 850ea53..75cedf8 100644
--- a/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java
+++ b/src/texturePacks/java/moe/nea/firmament/mixins/custommodels/SupplyFakeModelPatch.java
@@ -5,6 +5,7 @@ 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;
@@ -61,6 +62,7 @@ public class SupplyFakeModelPatch {
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);
}
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 3baab79..a5a612c 100644
--- a/translations/en_us.json
+++ b/translations/en_us.json
@@ -7,7 +7,6 @@
"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.",
@@ -36,6 +35,8 @@
"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",
@@ -68,6 +69,9 @@
"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.",
@@ -92,6 +96,8 @@
"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",
@@ -118,18 +124,44 @@
"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.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.",
@@ -143,7 +175,7 @@
"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": "Lore Timers",
+ "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",
@@ -151,6 +183,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",
@@ -162,8 +196,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.",
@@ -187,15 +227,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.",
@@ -218,6 +274,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",
@@ -258,19 +319,46 @@
"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.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",
@@ -296,10 +384,8 @@
"firmament.inventory-buttons.import-failed": "One of your buttons could only be imported partially",
"firmament.inventory-buttons.load-preset": "Load Preset",
"firmament.inventory-buttons.save-preset": "Save Preset",
- "firmament.jade.breaking_power": "Required Breaking Power: %s",
"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",
@@ -349,8 +435,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.",
@@ -366,14 +450,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",
@@ -387,5 +472,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