From 0be50d077af828ff14bfc45c276f84fede5b0deb Mon Sep 17 00:00:00 2001 From: KernelPanic Date: Mon, 10 Aug 2026 16:08:55 -0400 Subject: [PATCH 1/9] Stand up archie-core as a standalone Architectury Loom project Phase 1 of the Archie modularization plan: port today's common+fabric+ neoforge into a new archie-core (Archie-Core/core/{common,fabric,neoforge}), with the hard gametest/datagen dependency inverted into a ServiceLoader-based ArchieExtension registration hook instead of Archie.kt reaching into them directly. Directory layout (nested /, flattened to hyphenated Gradle project names) matches terrarium-earth/Common-Storage-Lib's settings.gradle.kts convention, so archie-datagen/archie-gametest (and eventually the test mod) can join later as more includeModule(...) calls without another restructure. This also closes out a from-scratch Cloche migration spike (fully reverted here): Cloche got archie-core's Fabric target to a clean, fully verified build (compile + full jar/remap pipeline), but NeoForge hit a confirmed structural gap in Cloche 0.19.13 - FabricTargetImpl has a dedicated remapCommon pipeline for intermediary-mapped "common" mod-library dependencies that ForgeLikeTargetImpl/NeoForgeTargetImpl simply doesn't have yet, so a real, load-bearing common dependency (Common Storage Lib) can never be remapped correctly for NeoForge under today's Cloche. Revisiting Cloche later, once that gap is fixed upstream, remains straightforward given this module boundary already exists. Verified: :archie-core-common, :archie-core-fabric, and :archie-core-neoforge all build cleanly end to end (compile, remapJar, shadowJar, assemble). Co-Authored-By: Claude Sonnet 5 --- Archie-Core/build.gradle.kts | 131 ++ Archie-Core/core/common/build.gradle.kts | 101 + .../gui/access/SlotLayerDepthContext.java | 52 + .../AbstractContainerScreenDepthMixin.java | 27 + .../gui/AbstractContainerScreenMixin.java | 112 ++ .../mixin/client/gui/GuiGraphicsMixin.java | 40 + .../archie/APlatform.common.kt | 8 + .../net/kernelpanicsoft/archie/Archie.kt | 353 ++++ .../kernelpanicsoft/archie/ArchieExtension.kt | 21 + .../archie/block/entity/NBTBlockEntity.kt | 60 + .../archie/config/CategorySpec.kt | 31 + .../archie/config/ClientConfigContainer.kt | 48 + .../archie/config/ClientConfigSpec.kt | 43 + .../archie/config/ClientDataSpec.kt | 1265 +++++++++++++ .../archie/config/CommonKeyCode.kt | 84 + .../archie/config/ConfigContainer.kt | 60 + .../archie/config/ConfigSpec.kt | 313 +++ .../kernelpanicsoft/archie/config/DataSpec.kt | 1673 +++++++++++++++++ .../archie/config/FieldType.kt | 188 ++ .../archie/config/IConfigSerializer.kt | 77 + .../archie/config/builder/ColorListBuilder.kt | 38 + .../archie/config/builder/ColorMapBuilder.kt | 40 + .../config/builder/ConfigFieldBuilder.kt | 40 + .../archie/config/builder/DoubleMapBuilder.kt | 26 + .../config/builder/DropdownFieldBuilder.kt | 46 + .../archie/config/builder/FloatMapBuilder.kt | 26 + .../config/builder/IntegerMapBuilder.kt | 26 + .../config/builder/KeycodeListBuilder.kt | 61 + .../config/builder/KeycodeMapBuilder.kt | 56 + .../archie/config/builder/ListFieldBuilder.kt | 105 ++ .../archie/config/builder/LongMapBuilder.kt | 26 + .../archie/config/builder/MapFieldBuilder.kt | 181 ++ .../config/builder/RegistryFieldBuilder.kt | 56 + .../config/builder/RegistryListBuilder.kt | 34 + .../config/builder/RegistryMapBuilder.kt | 36 + .../archie/config/builder/SpecFieldBuilder.kt | 56 + .../archie/config/builder/SpecListBuilder.kt | 30 + .../archie/config/builder/SpecMapBuilder.kt | 31 + .../archie/config/builder/StringMapBuilder.kt | 26 + .../archie/config/builder/extensions.kt | 259 +++ .../archie/config/entry/ConfigSpecEntry.kt | 111 ++ .../archie/config/extensions.kt | 22 + .../serializer/Json5ConfigSerializer.kt | 31 + .../config/serializer/JsonConfigSerializer.kt | 32 + .../config/serializer/NullConfigSerializer.kt | 32 + .../config/serializer/TomlConfigSerializer.kt | 32 + .../archie/data/ADataGenerator.kt | 60 + .../data/ADataGeneratorPlatform.common.kt | 12 + .../data/common/conditions/AAndCondition.kt | 35 + .../common/conditions/ABuiltinConditions.kt | 30 + .../conditions/AConditionsPlatform.common.kt | 22 + .../common/conditions/AEqualsCondition.kt | 36 + .../data/common/conditions/AFalseCondition.kt | 26 + .../data/common/conditions/AGroupCondition.kt | 16 + .../common/conditions/AModLoadedCondition.kt | 46 + .../data/common/conditions/ANotCondition.kt | 34 + .../data/common/conditions/AOrCondition.kt | 35 + .../common/conditions/APlatformCondition.kt | 47 + .../common/conditions/ARegistryCondition.kt | 49 + .../data/common/conditions/ATrueCondition.kt | 25 + .../data/common/conditions/AXorCondition.kt | 35 + .../data/common/conditions/IACondition.kt | 84 + .../crafting/ingredients/AAllIngredient.kt | 60 + .../crafting/ingredients/AAnyIngredient.kt | 51 + .../ingredients/ABuiltinIngredients.kt | 15 + .../ingredients/ACombinedIngredient.kt | 64 + .../ingredients/AComponentsIngredient.kt | 120 ++ .../ingredients/ACustomDataIngredient.kt | 122 ++ .../ACustomIngredientPlatform.common.kt | 9 + ...stomIngredientSerializerPlatform.common.kt | 8 + .../ingredients/IACustomIngredient.kt | 59 + .../ingredients/IACustomIngredientHolder.kt | 11 + .../IACustomIngredientSerializer.kt | 43 + .../archie/data/common/tags/ACommonTags.kt | 1090 +++++++++++ .../archie/events/ABasicEventObject.kt | 25 + .../archie/events/AEventObject.kt | 47 + .../kernelpanicsoft/archie/events/AEvents.kt | 185 ++ .../ADedicatedServerPlatform.common.kt | 25 + .../gametest/AGameTestPlatform.common.kt | 78 + .../archie/gametest/ThreadingImpl.kt | 457 +++++ .../archie/gui/AUIScopeManager.kt | 17 + .../archie/gui/ComposeBlockContainerMenu.kt | 74 + .../archie/gui/ComposeContainerMenuBase.kt | 483 +++++ .../archie/gui/ComposeContainerScreen.kt | 482 +++++ .../archie/gui/ComposeScreen.kt | 335 ++++ .../net/kernelpanicsoft/archie/gui/Slot.kt | 295 +++ .../gui/access/SlotHighlightClipProvider.kt | 17 + .../gui/access/SlotLayerDepthProvider.kt | 12 + .../archie/gui/animation/Animation.kt | 94 + .../BlockEntityStateComposables.kt | 44 + .../blockentity/BlockEntityStateContainer.kt | 171 ++ .../blockentity/BlockEntityStateManager.kt | 162 ++ .../gui/blockentity/BlockEntityStatePacket.kt | 102 + .../BlockEntityStatePacketRegistry.kt | 118 ++ .../blockentity/BlockEntityUpdatePacket.kt | 37 + .../blockentity/ComposeBlockEntityState.kt | 148 ++ .../archie/gui/composables/basic/Divider.kt | 54 + .../archie/gui/composables/basic/EnergyBar.kt | 45 + .../archie/gui/composables/basic/FluidTank.kt | 91 + .../archie/gui/composables/basic/Icon.kt | 43 + .../gui/composables/basic/ProgressBar.kt | 108 ++ .../archie/gui/composables/basic/Spacer.kt | 37 + .../archie/gui/composables/basic/Text.kt | 99 + .../archie/gui/composables/basic/Texture.kt | 55 + .../gui/composables/containers/Collapsible.kt | 166 ++ .../composables/containers/ContainerPanel.kt | 102 + .../gui/composables/containers/Panel.kt | 43 + .../composables/containers/RootContainer.kt | 36 + .../gui/composables/containers/Scrollable.kt | 267 +++ .../gui/composables/containers/Surface.kt | 77 + .../composables/containers/TabContainer.kt | 410 ++++ .../archie/gui/composables/input/Button.kt | 145 ++ .../archie/gui/composables/input/Checkbox.kt | 129 ++ .../archie/gui/composables/input/Clickable.kt | 92 + .../gui/composables/input/ColorPicker.kt | 179 ++ .../archie/gui/composables/input/Radio.kt | 159 ++ .../archie/gui/composables/input/Slider.kt | 203 ++ .../archie/gui/composables/input/Switch.kt | 133 ++ .../composables/input/textfield/TextField.kt | 221 +++ .../input/textfield/TextFieldCore.kt | 313 +++ .../input/textfield/TextFieldValue.kt | 61 + .../gui/composables/modal/ConfirmDialog.kt | 108 ++ .../gui/composables/modal/DialogPrimitives.kt | 202 ++ .../gui/composables/theme/TextureStates.kt | 26 + .../gui/composables/theme/WidgetState.kt | 84 + .../gui/item/ComposeItemContainerMenu.kt | 147 ++ .../archie/gui/item/ComposeItemState.kt | 145 ++ .../archie/gui/item/ItemContainerAccess.kt | 43 + .../archie/gui/item/ItemStateComposables.kt | 49 + .../archie/gui/item/ItemStateManager.kt | 53 + .../archie/gui/item/ItemStatePacket.kt | 38 + .../gui/item/ItemStatePacketRegistry.kt | 42 + .../archie/gui/item/ItemUpdatePacket.kt | 30 + .../archie/gui/item/SyncedItemHolder.kt | 33 + .../kernelpanicsoft/archie/gui/layer/Layer.kt | 53 + .../archie/gui/layer/LayerStackManager.kt | 382 ++++ .../archie/gui/layout/Alignment.kt | 275 +++ .../archie/gui/layout/Arrangement.kt | 689 +++++++ .../kernelpanicsoft/archie/gui/layout/Box.kt | 60 + .../archie/gui/layout/Column.kt | 87 + .../archie/gui/layout/Helpers.kt | 13 + .../archie/gui/layout/IntCoordinates.kt | 55 + .../archie/gui/layout/IntRect.kt | 44 + .../archie/gui/layout/Layout.kt | 60 + .../archie/gui/layout/LayoutDirection.kt | 19 + .../archie/gui/layout/LayoutNode.kt | 401 ++++ .../archie/gui/layout/MeasurePolicy.kt | 123 ++ .../kernelpanicsoft/archie/gui/layout/Row.kt | 80 + .../gui/layout/RowColumnMeasurePolicy.kt | 71 + .../kernelpanicsoft/archie/gui/layout/Size.kt | 16 + .../archie/gui/modifiers/Constraints.kt | 80 + .../archie/gui/modifiers/DebugModifier.kt | 55 + .../archie/gui/modifiers/DrawModifier.kt | 51 + .../gui/modifiers/LayoutChangingModifier.kt | 47 + .../archie/gui/modifiers/Modifier.kt | 153 ++ .../modifiers/OnGloballyPositionedModifier.kt | 33 + .../gui/modifiers/OnSizeChangedModifier.kt | 28 + .../archie/gui/modifiers/SizeModifier.kt | 132 ++ .../appearance/BackgroundModifier.kt | 97 + .../modifiers/appearance/BorderModifier.kt | 53 + .../modifiers/appearance/TextureModifier.kt | 26 + .../modifiers/appearance/TooltipModifier.kt | 29 + .../archie/gui/modifiers/input/InputEvent.kt | 112 ++ .../modifiers/input/OnCharTypedModifier.kt | 34 + .../gui/modifiers/input/OnKeyEventModifier.kt | 36 + .../modifiers/input/OnPointerEventModifier.kt | 150 ++ .../gui/modifiers/position/MarginModifier.kt | 90 + .../gui/modifiers/position/OffsetModifier.kt | 34 + .../gui/modifiers/position/PaddingModifier.kt | 96 + .../archie/gui/modifiers/position/ZIndex.kt | 33 + .../archie/gui/nodes/LayoutNodeApplier.kt | 42 + .../archie/gui/nodes/UINode.kt | 57 + .../gui/render/AFluidRenderPlatform.common.kt | 21 + .../archie/gui/theme/ComposableTheme.kt | 287 +++ .../kernelpanicsoft/archie/gui/theme/Theme.kt | 193 ++ .../archie/gui/util/HsvColor.kt | 55 + .../kernelpanicsoft/archie/gui/util/KColor.kt | 135 ++ .../archie/gui/util/extension/GuiGraphics.kt | 164 ++ .../archie/gui/util/extension/Screen.kt | 177 ++ .../gui/util/extension/VertexConsumer.kt | 20 + .../archie/networking/ArchieNetworkChannel.kt | 27 + .../archie/networking/IPacketContext.kt | 35 + .../archie/networking/NetworkChannel.kt | 486 +++++ .../AClientRegistrationPlatform.common.kt | 24 + .../archie/registries/ACreativeTabRegistry.kt | 11 + .../registries/ADeferredRegistryHolder.kt | 89 + .../archie/registries/BlockRegistryHelper.kt | 63 + .../registries/CreativeTabRegistryHelper.kt | 45 + .../archie/registries/RegistrarHelper.kt | 42 + .../archie/registries/RegistryHelper.kt | 66 + .../archie/registries/extensions.kt | 16 + .../SerializationReloadListener.kt | 88 + .../serialization/ArchieDataAttachmentImpl.kt | 24 + .../serialization/AttachmentRegistry.kt | 102 + .../archie/serialization/DataAttachment.kt | 106 ++ .../serialization/FluidStackNBTHolderImpl.kt | 244 +++ .../serialization/ItemStackNBTHolderImpl.kt | 348 ++++ .../archie/serialization/KOps.kt | 674 +++++++ .../archie/serialization/NBT.kt | 270 +++ .../archie/serialization/NBTHolder.kt | 119 ++ .../archie/serialization/NBTHolderImpl.kt | 304 +++ .../archie/serialization/ObservableList.kt | 98 + .../archie/serialization/ObservableMap.kt | 84 + .../serialization/SerializationManager.kt | 391 ++++ .../archie/serialization/Sync.kt | 13 + .../archie/serialization/Utils.kt | 292 +++ .../serializers/BuiltinSerializers.kt | 134 ++ .../serializers/MinecraftSerializers.kt | 363 ++++ .../transfer/ArchieCapabilityExposure.kt | 124 ++ .../archie/transfer/ArchieEnergyStorage.kt | 153 ++ .../archie/transfer/ArchieFluidSlot.kt | 192 ++ .../archie/transfer/ArchieFluidStorage.kt | 126 ++ .../archie/transfer/ArchieItemMenuSlot.kt | 64 + .../archie/transfer/ArchieItemSlot.kt | 175 ++ .../archie/transfer/ArchieItemStorage.kt | 102 + .../archie/transfer/VanillaMenuSlot.kt | 69 + .../net/kernelpanicsoft/archie/util/Array.kt | 22 + .../kernelpanicsoft/archie/util/Component.kt | 244 +++ .../net/kernelpanicsoft/archie/util/Env.kt | 39 + .../archie/util/MutableEntry.kt | 12 + .../kernelpanicsoft/archie/util/Properties.kt | 35 + .../kernelpanicsoft/archie/util/Reflect.kt | 39 + .../archie/util/ResourceLocation.kt | 19 + .../net/kernelpanicsoft/archie/util/Tile.kt | 18 + .../main/resources/archie-common.mixins.json | 16 + .../src/main/resources/archie.accesswidener | 336 ++++ .../src/main/resources/archie.common.json | 3 + .../archie/archie_themes/java.theme.json | 8 + .../archie/archie_themes/java/button.json | 19 + .../archie/archie_themes/java/checkbox.json | 22 + .../archie_themes/java/dark/surface.json | 21 + .../archie/archie_themes/java/energy_bar.json | 13 + .../archie/archie_themes/java/fluid_tank.json | 13 + .../archie_themes/java/progress_bar.json | 13 + .../archie/archie_themes/java/radio.json | 25 + .../archie/archie_themes/java/slider.json | 19 + .../archie_themes/java/slider_handle.json | 19 + .../archie/archie_themes/java/slot.json | 15 + .../archie_themes/java/small_checkbox.json | 16 + .../archie/archie_themes/java/surface.json | 20 + .../archie_themes/java/switch_thumb.json | 16 + .../archie_themes/java/switch_track.json | 25 + .../archie/archie_themes/java/tab_game.json | 25 + .../archie/archie_themes/java/tab_menu.json | 25 + .../archie/archie_themes/java/text_field.json | 16 + .../resources/assets/archie/atlases/java.json | 9 + .../main/resources/assets/archie/banner.png | Bin 0 -> 32385 bytes .../src/main/resources/assets/archie/icon.png | Bin 0 -> 68643 bytes .../textures/gui/sprites/java/button.png | Bin 0 -> 1432 bytes .../gui/sprites/java/button.png.mcmeta | 10 + .../gui/sprites/java/button_disabled.png | Bin 0 -> 1223 bytes .../sprites/java/button_disabled.png.mcmeta | 10 + .../gui/sprites/java/button_highlighted.png | Bin 0 -> 1448 bytes .../java/button_highlighted.png.mcmeta | 10 + .../textures/gui/sprites/java/checkbox.png | Bin 0 -> 408 bytes .../gui/sprites/java/checkbox_clicked.png | Bin 0 -> 482 bytes .../java/checkbox_clicked_and_hovered.png | Bin 0 -> 478 bytes .../gui/sprites/java/checkbox_hovered.png | Bin 0 -> 407 bytes .../textures/gui/sprites/java/energy_bar.png | Bin 0 -> 136 bytes .../gui/sprites/java/energy_bar.png.mcmeta | 10 + .../textures/gui/sprites/java/fluid_tank.png | Bin 0 -> 131 bytes .../gui/sprites/java/fluid_tank.png.mcmeta | 10 + .../gui/sprites/java/progress_bar.png | Bin 0 -> 115 bytes .../gui/sprites/java/progress_bar.png.mcmeta | 10 + .../textures/gui/sprites/java/radio.png | Bin 0 -> 469 bytes .../gui/sprites/java/radio_clicked.png | Bin 0 -> 468 bytes .../java/radio_clicked_and_hovered.png | Bin 0 -> 463 bytes .../gui/sprites/java/radio_disabled.png | Bin 0 -> 424 bytes .../gui/sprites/java/radio_hovered.png | Bin 0 -> 468 bytes .../textures/gui/sprites/java/slider.png | Bin 0 -> 1158 bytes .../gui/sprites/java/slider.png.mcmeta | 10 + .../gui/sprites/java/slider_handle.png | Bin 0 -> 242 bytes .../gui/sprites/java/slider_handle.png.mcmeta | 15 + .../java/slider_handle_highlighted.png | Bin 0 -> 237 bytes .../java/slider_handle_highlighted.png.mcmeta | 15 + .../gui/sprites/java/slider_highlighted.png | Bin 0 -> 1165 bytes .../java/slider_highlighted.png.mcmeta | 10 + .../archie/textures/gui/sprites/java/slot.png | Bin 0 -> 507 bytes .../gui/sprites/java/small_checkbox.png | Bin 0 -> 239 bytes .../sprites/java/small_checkbox_clicked.png | Bin 0 -> 320 bytes .../textures/gui/sprites/java/surface.png | Bin 0 -> 166 bytes .../gui/sprites/java/surface.png.mcmeta | 10 + .../gui/sprites/java/surface_dark.png | Bin 0 -> 173 bytes .../gui/sprites/java/surface_dark.png.mcmeta | 10 + .../gui/sprites/java/surface_inset.png | Bin 0 -> 366 bytes .../gui/sprites/java/surface_inset.png.mcmeta | 10 + .../gui/sprites/java/surface_inset_dark.png | Bin 0 -> 372 bytes .../java/surface_inset_dark.png.mcmeta | 10 + .../gui/sprites/java/switch_thumb.png | Bin 0 -> 126 bytes .../sprites/java/switch_thumb_disabled.png | Bin 0 -> 127 bytes .../gui/sprites/java/switch_track.png | Bin 0 -> 639 bytes .../gui/sprites/java/switch_track_clicked.png | Bin 0 -> 965 bytes .../java/switch_track_clicked_and_hovered.png | Bin 0 -> 964 bytes .../sprites/java/switch_track_disabled.png | Bin 0 -> 645 bytes .../gui/sprites/java/switch_track_hovered.png | Bin 0 -> 637 bytes .../textures/gui/sprites/java/tab_game.png | Bin 0 -> 147 bytes .../gui/sprites/java/tab_game.png.mcmeta | 15 + .../gui/sprites/java/tab_game_clicked.png | Bin 0 -> 163 bytes .../sprites/java/tab_game_clicked.png.mcmeta | 15 + .../java/tab_game_clicked_and_hovered.png | Bin 0 -> 162 bytes .../tab_game_clicked_and_hovered.png.mcmeta | 15 + .../gui/sprites/java/tab_game_disabled.png | Bin 0 -> 159 bytes .../sprites/java/tab_game_disabled.png.mcmeta | 15 + .../gui/sprites/java/tab_game_hovered.png | Bin 0 -> 148 bytes .../sprites/java/tab_game_hovered.png.mcmeta | 15 + .../gui/sprites/java/tab_game_selected.png | Bin 0 -> 163 bytes .../sprites/java/tab_game_selected.png.mcmeta | 15 + .../java/tab_game_selected_highlighted.png | Bin 0 -> 162 bytes .../tab_game_selected_highlighted.png.mcmeta | 15 + .../textures/gui/sprites/java/tab_menu.png | Bin 0 -> 178 bytes .../gui/sprites/java/tab_menu.png.mcmeta | 10 + .../gui/sprites/java/tab_menu_clicked.png | Bin 0 -> 192 bytes .../sprites/java/tab_menu_clicked.png.mcmeta | 10 + .../java/tab_menu_clicked_and_hovered.png | Bin 0 -> 186 bytes .../tab_menu_clicked_and_hovered.png.mcmeta | 10 + .../gui/sprites/java/tab_menu_disabled.png | Bin 0 -> 178 bytes .../sprites/java/tab_menu_disabled.png.mcmeta | 10 + .../gui/sprites/java/tab_menu_hovered.png | Bin 0 -> 184 bytes .../sprites/java/tab_menu_hovered.png.mcmeta | 10 + .../gui/sprites/java/tab_menu_selected.png | Bin 0 -> 192 bytes .../sprites/java/tab_menu_selected.png.mcmeta | 10 + .../java/tab_menu_selected_highlighted.png | Bin 0 -> 186 bytes .../tab_menu_selected_highlighted.png.mcmeta | 10 + .../textures/gui/sprites/java/text_field.png | Bin 0 -> 111 bytes .../gui/sprites/java/text_field.png.mcmeta | 10 + .../sprites/java/text_field_highlighted.png | Bin 0 -> 104 bytes .../java/text_field_highlighted.png.mcmeta | 10 + .../data/archie/structure/gametest/empty.nbt | Bin 0 -> 123 bytes Archie-Core/core/fabric/build.gradle.kts | 131 ++ .../mixin/fabric/ArchieMixinPlugin.java | 57 + .../threading/MinecraftClientMixin.java | 44 + .../mixin/fabric/threading/ServerMixin.java | 33 + .../net/kernelpanicsoft/archie/APlatform.kt | 11 + .../kernelpanicsoft/archie/ArchieFabric.kt | 25 + .../archie/data/ADataGeneratorPlatform.kt | 10 + .../common/conditions/AConditionsPlatform.kt | 93 + .../ingredients/ACustomIngredientPlatform.kt | 48 + .../ACustomIngredientSerializerPlatform.kt | 69 + .../gametest/ADedicatedServerPlatform.kt | 105 ++ .../ADedicatedServerPlatformInternal.kt | 46 + .../archie/gametest/AGameTestPlatform.kt | 43 + .../gametest/AGameTestPlatformInternal.kt | 18 + .../archie/gui/render/AFluidRenderPlatform.kt | 21 + .../registries/AClientRegistrationPlatform.kt | 6 + .../src/main/resources/archie.mixins.json | 16 + .../fabric/src/main/resources/fabric.mod.json | 55 + Archie-Core/core/neoforge/build.gradle.kts | 150 ++ Archie-Core/core/neoforge/gradle.properties | 1 + .../threading/MinecraftClientMixin.java | 44 + .../mixin/neoforge/threading/ServerMixin.java | 33 + .../net/kernelpanicsoft/archie/APlatform.kt | 10 + .../kernelpanicsoft/archie/ArchieNeoForge.kt | 31 + .../archie/data/ADataGeneratorPlatform.kt | 10 + .../common/conditions/AConditionsPlatform.kt | 113 ++ .../ingredients/ACustomIngredientPlatform.kt | 37 + .../ACustomIngredientSerializerPlatform.kt | 57 + .../gametest/ADedicatedServerPlatform.kt | 105 ++ .../ADedicatedServerPlatformInternal.kt | 46 + .../archie/gametest/AGameTestPlatform.kt | 42 + .../gametest/AGameTestPlatformInternal.kt | 16 + .../archie/gui/render/AFluidRenderPlatform.kt | 19 + .../registries/AClientRegistrationPlatform.kt | 18 + .../resources/META-INF/neoforge.mods.toml | 46 + .../src/main/resources/archie.mixins.json | 15 + Archie-Core/gradle.properties | 12 + Archie-Core/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 43583 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 + Archie-Core/gradlew | 251 +++ Archie-Core/gradlew.bat | 94 + Archie-Core/settings.gradle.kts | 50 + 370 files changed, 30082 insertions(+) create mode 100644 Archie-Core/build.gradle.kts create mode 100644 Archie-Core/core/common/build.gradle.kts create mode 100644 Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java create mode 100644 Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenDepthMixin.java create mode 100644 Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java create mode 100644 Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/GuiGraphicsMixin.java create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.common.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/block/entity/NBTBlockEntity.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CategorySpec.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigContainer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigSpec.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientDataSpec.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CommonKeyCode.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigContainer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigSpec.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/DataSpec.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/FieldType.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/IConfigSerializer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorListBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorMapBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ConfigFieldBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DoubleMapBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DropdownFieldBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/FloatMapBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/IntegerMapBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeListBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeMapBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ListFieldBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/LongMapBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/MapFieldBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryFieldBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryListBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryMapBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecFieldBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecListBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecMapBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/StringMapBuilder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/extensions.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/entry/ConfigSpecEntry.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/extensions.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/Json5ConfigSerializer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/JsonConfigSerializer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/NullConfigSerializer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/TomlConfigSerializer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AAndCondition.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ABuiltinConditions.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AEqualsCondition.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AFalseCondition.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AGroupCondition.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AModLoadedCondition.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ANotCondition.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AOrCondition.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/APlatformCondition.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ARegistryCondition.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ATrueCondition.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AXorCondition.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAllIngredient.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAnyIngredient.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ABuiltinIngredients.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACombinedIngredient.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AComponentsIngredient.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomDataIngredient.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.common.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.common.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredient.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientHolder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientSerializer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEvents.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/AUIScopeManager.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeBlockContainerMenu.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerMenuBase.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerScreen.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/Slot.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotHighlightClipProvider.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthProvider.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/animation/Animation.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateComposables.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateContainer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateManager.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacket.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityUpdatePacket.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/ComposeBlockEntityState.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Divider.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Icon.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Spacer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Text.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Texture.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Collapsible.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/ContainerPanel.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Panel.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/RootContainer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Scrollable.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Surface.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/TabContainer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Button.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Checkbox.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Clickable.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/ColorPicker.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Radio.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Slider.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Switch.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextField.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldCore.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldValue.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/ConfirmDialog.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/DialogPrimitives.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/TextureStates.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/WidgetState.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemContainerMenu.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemState.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemContainerAccess.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateComposables.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateManager.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacket.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacketRegistry.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemUpdatePacket.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/SyncedItemHolder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/Layer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/LayerStackManager.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Alignment.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Arrangement.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Box.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Column.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Helpers.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntCoordinates.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntRect.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Layout.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutDirection.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutNode.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/MeasurePolicy.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Row.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/RowColumnMeasurePolicy.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Size.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Constraints.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DebugModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DrawModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/LayoutChangingModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Modifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnGloballyPositionedModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnSizeChangedModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/SizeModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BackgroundModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BorderModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TextureModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TooltipModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/InputEvent.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnCharTypedModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnKeyEventModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnPointerEventModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/MarginModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/OffsetModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/PaddingModifier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/ZIndex.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/LayoutNodeApplier.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/UINode.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/ComposableTheme.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/Theme.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/HsvColor.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/KColor.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/GuiGraphics.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/Screen.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/VertexConsumer.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/IPacketContext.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.common.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ACreativeTabRegistry.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ADeferredRegistryHolder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/BlockRegistryHelper.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/CreativeTabRegistryHelper.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistrarHelper.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistryHelper.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/extensions.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/resourcepacks/SerializationReloadListener.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ArchieDataAttachmentImpl.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/AttachmentRegistry.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/DataAttachment.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/KOps.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBT.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableList.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableMap.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/SerializationManager.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Sync.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Utils.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/BuiltinSerializers.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/MinecraftSerializers.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemMenuSlot.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemSlot.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemStorage.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/VanillaMenuSlot.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Array.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Component.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Env.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/MutableEntry.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Properties.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Reflect.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt create mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Tile.kt create mode 100644 Archie-Core/core/common/src/main/resources/archie-common.mixins.json create mode 100644 Archie-Core/core/common/src/main/resources/archie.accesswidener create mode 100644 Archie-Core/core/common/src/main/resources/archie.common.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java.theme.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/button.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/checkbox.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/dark/surface.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/radio.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slider.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slider_handle.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slot.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/small_checkbox.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/surface.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/switch_thumb.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/switch_track.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/tab_game.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/tab_menu.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/text_field.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/atlases/java.json create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/banner.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/icon.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked_and_hovered.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_hovered.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked_and_hovered.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_disabled.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_hovered.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slot.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox_clicked.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb_disabled.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked_and_hovered.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_disabled.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_hovered.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png create mode 100644 Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png.mcmeta create mode 100644 Archie-Core/core/common/src/main/resources/data/archie/structure/gametest/empty.nbt create mode 100644 Archie-Core/core/fabric/build.gradle.kts create mode 100644 Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/ArchieMixinPlugin.java create mode 100644 Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java create mode 100644 Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java create mode 100644 Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt create mode 100644 Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt create mode 100644 Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.kt create mode 100644 Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt create mode 100644 Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.kt create mode 100644 Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.kt create mode 100644 Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt create mode 100644 Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt create mode 100644 Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt create mode 100644 Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt create mode 100644 Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt create mode 100644 Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt create mode 100644 Archie-Core/core/fabric/src/main/resources/archie.mixins.json create mode 100644 Archie-Core/core/fabric/src/main/resources/fabric.mod.json create mode 100644 Archie-Core/core/neoforge/build.gradle.kts create mode 100644 Archie-Core/core/neoforge/gradle.properties create mode 100644 Archie-Core/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java create mode 100644 Archie-Core/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java create mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt create mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt create mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.kt create mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt create mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.kt create mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.kt create mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt create mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt create mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt create mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt create mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt create mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt create mode 100644 Archie-Core/core/neoforge/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 Archie-Core/core/neoforge/src/main/resources/archie.mixins.json create mode 100644 Archie-Core/gradle.properties create mode 100644 Archie-Core/gradle/wrapper/gradle-wrapper.jar create mode 100644 Archie-Core/gradle/wrapper/gradle-wrapper.properties create mode 100755 Archie-Core/gradlew create mode 100644 Archie-Core/gradlew.bat create mode 100644 Archie-Core/settings.gradle.kts diff --git a/Archie-Core/build.gradle.kts b/Archie-Core/build.gradle.kts new file mode 100644 index 000000000..c8ac21c3c --- /dev/null +++ b/Archie-Core/build.gradle.kts @@ -0,0 +1,131 @@ +import net.fabricmc.loom.api.LoomGradleExtensionAPI +import org.jetbrains.kotlin.konan.properties.loadProperties + +plugins { + java + alias(libs.plugins.architectury) + id("net.kernelpanicsoft.actualizer") version "0.1.0" apply false + alias(libs.plugins.architectury.loom) apply false + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.compose) +} + +architectury.minecraft = libs.versions.minecraft.get() + +val sharedProperties = kotlin.runCatching { + val localPropsFile = rootDir.resolve("gradle.properties") + val sharedPropsFile = rootDir.resolve("../gradle.properties") + when { + localPropsFile.exists() -> loadProperties(localPropsFile.path) + sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) + else -> null + } +}.getOrNull() + +val String.prop: String? + get() = sharedProperties?.get(this)?.toString() + +val String.localOrEnv: String? + get() = System.getenv(this.uppercase()) + +subprojects { + apply(plugin = "dev.architectury.loom") + apply(plugin = "net.kernelpanicsoft.actualizer") + + val loom = project.extensions.getByName("loom") + + configure { + silentMojangMappingsLicense() + } + + repositories { + val githubUsername = "github_actor".localOrEnv + val githubToken = "github_token".localOrEnv + mavenCentral() + mavenLocal() + google { + content { + includeGroupByRegex("androidx\\..*") + includeGroupByRegex("com\\.android.*") + } + } + maven { + name = "kernelpanic releases" + url = uri("https://maven.kernelpanicsoft.net/releases") + } + maven { + name = "kernelpanic snapshots" + url = uri("https://maven.kernelpanicsoft.net/snapshots") + } + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://maven.parchmentmc.org") + maven("https://maven.fabricmc.net/") + maven("https://maven.neoforged.net/releases/") + maven("https://maven.terraformersmc.com/releases/") + maven("https://repo.nyon.dev/releases") + maven("https://maven.isxander.dev/releases") { + name = "Xander Maven" + } + maven("https://maven.resourcefulbees.com/repository/maven-public/") { + content { + includeGroup("earth.terrarium.common_storage_lib") + } + } + maven { + url = uri("https://maven.pkg.github.com/MrCrayfish/Maven") + credentials { + username = githubUsername + password = githubToken + } + } + maven { + url = uri("https://www.cursemaven.com") + content { + includeGroup("curse.maven") + } + } + } + + @Suppress("UnstableApiUsage") + dependencies { + "minecraft"(rootProject.libs.minecraft) + "mappings"(loom.layered { + officialMojangMappings() + parchment(rootProject.libs.parchment) + }) + + compileOnly("org.jetbrains:annotations:24.1.0") + } +} + +allprojects { + apply(plugin = "java") + apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "org.jetbrains.kotlin.plugin.serialization") + apply(plugin = "org.jetbrains.kotlin.plugin.compose") + apply(plugin = "org.jetbrains.compose") + apply(plugin = "architectury-plugin") + + version = "mod_version".prop ?: "0.0.1-SNAPSHOT" + group = "mod_group".prop ?: "net.kernelpanicsoft" + base.archivesName = "archie-core" + + tasks.withType().configureEach { + options.encoding = "UTF-8" + options.release.set(21) + } + + kotlin { + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + + architectury { + compileOnly() + } + + java.withSourcesJar() +} diff --git a/Archie-Core/core/common/build.gradle.kts b/Archie-Core/core/common/build.gradle.kts new file mode 100644 index 000000000..c453eeb18 --- /dev/null +++ b/Archie-Core/core/common/build.gradle.kts @@ -0,0 +1,101 @@ +import org.jetbrains.kotlin.konan.properties.loadProperties + +architectury { + common("fabric", "neoforge") +} + +actualizer { + stubUnfulfilledExpects() +} + +val sharedProperties = kotlin.runCatching { + val localPropsFile = rootDir.resolve("gradle.properties") + val sharedPropsFile = rootDir.resolve("../gradle.properties") + when { + localPropsFile.exists() -> loadProperties(localPropsFile.path) + sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) + else -> null + } +}.getOrNull() + +val String.prop: String? + get() = sharedProperties?.get(this)?.toString() + +loom { + accessWidenerPath = file("src/main/resources/${"mod_id".prop}.accesswidener") +} + +dependencies { + compileOnly(kotlin("reflect")) + implementation(libs.junit.jupiter.api) + // Used by the client GameTest harness (archie-gametest) only, to give ComposeScreen a virtual + // clock/dispatcher during tests - never on a real player's classpath. compileOnly deliberately. + compileOnly(libs.kotlinx.coroutines.test) + testImplementation(libs.junit.jupiter.api) + testImplementation(kotlin("reflect")) + testRuntimeOnly(libs.junit.jupiter.engine) + api(libs.kotlinx.serialization) + api(libs.kotlinx.serialization.json) + api(libs.kotlinx.serialization.nbt) { isTransitive = false } + api(libs.kotlinx.serialization.toml) { isTransitive = false } + api(libs.kotlinx.serialization.json5) { isTransitive = false } + api(libs.kotlinx.serialization.cbor) { isTransitive = false } + api(compose.runtime) + // Used only for the fabric @Environment annotations + mixin deps. Do NOT use other classes + // from fabric loader from common code. + modImplementation(libs.fabric.loader) + + modApi(libs.rei.common) + // catalogue.common deliberately omitted - common source never references it directly. + modCompileOnly(libs.clothConfig.common) + // yacl.common deliberately omitted too - unused, and it's actually a Fabric-only build (its + // version coordinate ends in "-fabric"). + modApi(libs.architectury.common) + modApi(libs.storage.common) + modApi(libs.storage.resources.common) +} + +tasks { + base.archivesName.set(base.archivesName.get() + "-common") + + val verifyGuiSpriteAssets by registering { + group = "verification" + description = "Verifies GUI sprite metadata files have matching PNG assets." + + doLast { + val spritesDir = file("src/main/resources/assets/archie/textures/gui/sprites") + if (!spritesDir.exists()) return@doLast + + val missingPng = spritesDir + .walkTopDown() + .filter { it.isFile && it.name.endsWith(".png.mcmeta") } + .map { it to file(it.path.removeSuffix(".mcmeta")) } + .filter { (_, png) -> !png.exists() } + .map { (meta, _) -> meta.relativeTo(projectDir).invariantSeparatorsPath } + .toList() + + if (missingPng.isNotEmpty()) { + val details = missingPng.joinToString(separator = "\n") { " - $it" } + throw GradleException( + "Found GUI sprite metadata files without matching PNGs:\n$details" + ) + } + } + } + + named("check") { + dependsOn(verifyGuiSpriteAssets) + } + + // Keep stubUnfulfilledExpects()'s generated throwing-actual stubs out of what gets published - + // a consumer with both this jar and a real actual on its classpath must only ever see the + // real one, or Kotlin's actual-resolution can end up preferring the stub. + jar { + from(sourceSets.main.get().output) + exclude("**/*StubKt.class") + } + + sourcesJar { + exclude("**/*Stub.kt") + } +} diff --git a/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java b/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java new file mode 100644 index 000000000..30e433a7a --- /dev/null +++ b/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java @@ -0,0 +1,52 @@ +package net.kernelpanicsoft.archie.gui.access; + +import java.util.ArrayDeque; +import java.util.Deque; +import net.kernelpanicsoft.archie.Archie; + +/** + * Tracks when slot rendering overrides the default GUI depth so downstream + * render calls (like GuiGraphics#renderItem) can adjust their transforms. + */ +public final class SlotLayerDepthContext +{ + private static final ThreadLocal> DEPTHS = ThreadLocal.withInitial(ArrayDeque::new); + + private SlotLayerDepthContext() + { + } + + public static void push(float depth) + { + Deque depths = DEPTHS.get(); + depths.push(depth); + Archie.LOGGER.debug("Slot depth push -> {} (stack size={})", depth, depths.size()); + } + + public static void pop() + { + Deque depths = DEPTHS.get(); + if (depths.isEmpty()) + { + DEPTHS.remove(); + return; + } + Float removed = depths.pop(); + Archie.LOGGER.debug("Slot depth pop -> {} (remaining={})", removed, depths.size()); + if (depths.isEmpty()) + { + DEPTHS.remove(); + } + } + + public static boolean isActive() + { + Deque depths = DEPTHS.get(); + return !depths.isEmpty(); + } + + public static Float currentDepth() + { + return DEPTHS.get().peek(); + } +} diff --git a/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenDepthMixin.java b/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenDepthMixin.java new file mode 100644 index 000000000..34adb0021 --- /dev/null +++ b/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenDepthMixin.java @@ -0,0 +1,27 @@ +package net.kernelpanicsoft.archie.mixin.client.gui; + +import com.mojang.blaze3d.systems.RenderSystem; +import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthProvider; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import org.lwjgl.opengl.GL11; +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(AbstractContainerScreen.class) +public abstract class AbstractContainerScreenDepthMixin +{ + @Inject(method = "render", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;disableDepthTest()V", shift = At.Shift.AFTER)) + private void archie$restoreDepth(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick, CallbackInfo ci) + { + if (this instanceof SlotLayerDepthProvider) + { + RenderSystem.enableDepthTest(); + RenderSystem.depthMask(true); + RenderSystem.depthFunc(GL11.GL_LEQUAL); + } + } +} + diff --git a/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java b/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java new file mode 100644 index 000000000..14c7664d7 --- /dev/null +++ b/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java @@ -0,0 +1,112 @@ +package net.kernelpanicsoft.archie.mixin.client.gui; + +import net.kernelpanicsoft.archie.Archie; +import net.kernelpanicsoft.archie.gui.access.SlotHighlightClipProvider; +import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthContext; +import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthProvider; +import net.kernelpanicsoft.archie.gui.layout.IntRect; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.network.chat.Component; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Debug; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArgs; +import org.spongepowered.asm.mixin.injection.Redirect; +import org.spongepowered.asm.mixin.injection.invoke.arg.Args; + +@Debug(export = true) +@Mixin(AbstractContainerScreen.class) +public abstract class AbstractContainerScreenMixin extends Screen implements MenuAccess +{ + @Unique + private Float archie$slotDepthOverride; + + protected AbstractContainerScreenMixin(Component title) + { + super(title); + } + + @ModifyArgs(method = "renderSlot", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;translate(FFF)V")) + private void archie$adjustSlotLayer(Args args, GuiGraphics guiGraphics, Slot slot) + { + float originalZ = args.get(2); + float adjustedZ = originalZ; + archie$slotDepthOverride = null; + if (this instanceof SlotLayerDepthProvider provider) + { + Float custom = provider.slotRenderLayerOffset(slot); + archie$slotDepthOverride = custom; + if (custom != null) + { + adjustedZ = custom; + Archie.LOGGER.debug("Adjusting slot layer depth for {} to {}", slot, custom); + } + } + args.set(2, adjustedZ); + } + + @Redirect(method = "renderSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;renderItem(Lnet/minecraft/world/item/ItemStack;III)V")) + private void archie$wrapSlotItemRender(GuiGraphics guiGraphics, ItemStack stack, int x, int y, int seed) + { + boolean pushed = false; + if (archie$slotDepthOverride != null) + { + SlotLayerDepthContext.push(archie$slotDepthOverride); + pushed = true; + } + try + { + guiGraphics.renderItem(stack, x, y, seed); + } + finally + { + if (pushed) + { + SlotLayerDepthContext.pop(); + } + archie$slotDepthOverride = null; + } + } + + /** + * Vanilla's per-slot hover highlight is drawn via a static helper that only takes the + * slot's raw x/y/blitOffset - there's no per-slot instance override point to clip it the + * way {@link #archie$adjustSlotLayer} clips the item icon, so this redirects the call + * site directly instead. + */ + @Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;renderSlotHighlight(Lnet/minecraft/client/gui/GuiGraphics;III)V")) + private void archie$clipSlotHighlight(GuiGraphics guiGraphics, int x, int y, int blitOffset) + { + IntRect clip = null; + if (this instanceof SlotHighlightClipProvider provider) + { + clip = provider.slotHighlightClipRect(x, y); + if (clip == null) + { + return; + } + } + if (clip != null) + { + guiGraphics.enableScissor(clip.getMinX(), clip.getMinY(), clip.getMaxX(), clip.getMaxY()); + } + try + { + AbstractContainerScreen.renderSlotHighlight(guiGraphics, x, y, blitOffset); + } + finally + { + if (clip != null) + { + guiGraphics.disableScissor(); + } + } + } +} diff --git a/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/GuiGraphicsMixin.java b/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/GuiGraphicsMixin.java new file mode 100644 index 000000000..d054a103d --- /dev/null +++ b/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/GuiGraphicsMixin.java @@ -0,0 +1,40 @@ +package net.kernelpanicsoft.archie.mixin.client.gui; + +import net.kernelpanicsoft.archie.Archie; +import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthContext; +import net.minecraft.client.gui.GuiGraphics; +import org.spongepowered.asm.mixin.Debug; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.ModifyArgs; +import org.spongepowered.asm.mixin.injection.invoke.arg.Args; + +@Debug(export = true) +@Mixin(GuiGraphics.class) +public abstract class GuiGraphicsMixin +{ + @Unique + private static final float ITEM_TRANSLATE_Z = 150.0F; + + @ModifyArgs( + method = "renderItem(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;IIII)V", + at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;translate(FFF)V") + ) + private void archie$flattenSlotItemDepth(Args args) + { + if (!SlotLayerDepthContext.isActive()) + { + return; + } + float originalZ = args.get(2); + float adjusted = originalZ - ITEM_TRANSLATE_Z; + Archie.LOGGER.debug( + "Slot depth context active: target={}, translate={} -> {}", + SlotLayerDepthContext.currentDepth(), + originalZ, + adjusted + ); + args.set(2, adjusted); + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.common.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.common.kt new file mode 100644 index 000000000..24e218198 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.common.kt @@ -0,0 +1,8 @@ +package net.kernelpanicsoft.archie + +/** Cross-loader platform identification, backed by an `actual` per mod loader. */ +expect object APlatform +{ + /** The current mod loader's short id: `"fabric"` on Fabric, `"neoforge"` on NeoForge. */ + val platform: String +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt new file mode 100644 index 000000000..fbf99e603 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt @@ -0,0 +1,353 @@ +package net.kernelpanicsoft.archie + +import com.mojang.logging.LogUtils +import dev.architectury.event.events.client.ClientTickEvent +import dev.architectury.platform.Mod +import dev.architectury.platform.Platform +import dev.architectury.registry.ReloadListenerRegistry +import kotlinx.serialization.Serializable +import net.kernelpanicsoft.archie.config.CategorySpec +import net.kernelpanicsoft.archie.config.ConfigContainer +import net.kernelpanicsoft.archie.config.ConfigSpec +import net.kernelpanicsoft.archie.config.DataSpec +import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform +import net.kernelpanicsoft.archie.data.common.conditions.ABuiltinConditions +import net.kernelpanicsoft.archie.data.common.crafting.ingredients.ABuiltinIngredients +import net.kernelpanicsoft.archie.data.common.tags.ACommonTags +import net.kernelpanicsoft.archie.events.AEvents +import net.kernelpanicsoft.archie.gametest.AGameTestPlatform +import net.kernelpanicsoft.archie.gametest.AGameTestSide +import net.kernelpanicsoft.archie.gametest.ThreadingImpl +import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager +import net.kernelpanicsoft.archie.gui.item.ItemStateManager +import net.kernelpanicsoft.archie.gui.theme.ThemeManifestResourceListener +import net.kernelpanicsoft.archie.gui.theme.ThemeResourceListener +import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel +import net.kernelpanicsoft.archie.util.buildArray +import net.kernelpanicsoft.archie.util.onClient +import net.kernelpanicsoft.archie.util.rem +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.network.chat.Component +import net.minecraft.server.packs.PackType +import net.minecraft.world.item.BlockItem +import net.minecraft.world.item.Items +import net.minecraft.world.level.block.entity.BlockEntityType +import org.slf4j.Logger +import java.util.ServiceLoader + +/** + * Archie's mod object and library entrypoint. + */ +object Archie +{ + /** Archie's own mod id, used as the namespace for its resources and network channel. */ + const val MOD_ID = "archie" + + + /** The Architectury [Mod] descriptor for Archie itself. */ + @JvmField + val MOD: Mod = Platform.getMod(MOD_ID) + + /** Shared SLF4J logger for Archie's own internal logging. */ + @JvmField + val LOGGER: Logger = LogUtils.getLogger() + + /** + * Initializes Archie's shared (loader-independent) systems. + * + * Registers Archie with [AEvents], wires up networking (skipped only for a server-only + * gametest run, since Architectury's networking registration touches client-only classes), + * initializes block entity state syncing, built-in data providers, and Archie's own config, + * and activates the datagen/gametest code paths when running under those tasks. + * + * @throws IllegalStateException if running on LexForge, which is not supported. + */ + @JvmStatic + fun init() + { + + if (Platform.isMinecraftForge()) + error("LexForge is not supported. Switch to NeoForge, or don't use my mods.") + AEvents += MOD + if (!AGameTestPlatform.isGameTest || AGameTestPlatform.side == AGameTestSide.CLIENT) + { + ArchieNetworkChannel.init() + } + BlockEntityStateManager.init() + ItemStateManager.init() + + ABuiltinIngredients.init() + ABuiltinConditions.init() + ACommonTags.init() + Config.init() + + + // Datagen and GameTest code paths are only activated in dedicated run configs, and only + // exist at all when archie-datagen/archie-gametest are present - see ArchieExtension. + if (AGameTestPlatform.isGameTest) + ServiceLoader.load(ArchieExtension::class.java).forEach { it.onGameTest() } + if (ADataGeneratorPlatform.isDataGen) + ServiceLoader.load(ArchieExtension::class.java).forEach { it.onDataGen() } + onClient { + ReloadListenerRegistry.register(PackType.CLIENT_RESOURCES, ThemeManifestResourceListener(), Archie % "theme_manifest") + ReloadListenerRegistry.register(PackType.CLIENT_RESOURCES, ThemeResourceListener(), Archie % "theme") + } + } + + /** + * Reserved for client-only initialization that must run after [init], from a client + * entrypoint. Currently a no-op: Archie's own config screen(s) already register synchronously + * inside [init] via `Config.init()` (see [ConfigContainer.initClient]), so there's nothing left + * to do here. + */ + @JvmStatic + fun initClient() + { + ClientTickEvent.CLIENT_POST.register { + ThreadingImpl.onClientTick() + } + } + + /** + * Reserved for common-side initialization that must run after both [init] and platform + * bootstrap. Currently a no-op. + */ + @JvmStatic + fun initCommon() + { + } + + /** + * Archie's own config, registered under the "Config" title. `General` holds Archie's real + * settings; `Test` is a self-test fixture exercising every [DataSpec] value type + * supported by the config system and is not meant to be user-facing. + */ + object Config : ConfigContainer(MOD) + { + object Common : ConfigSpec.Common(MOD) + { + object General : CategorySpec(Component.literal("General"), "general") + { + val tests by boolean( + title = Component.literal("Tests"), + default = false + ) + } + + @Suppress("unused") + object Test : CategorySpec(Component.literal("Test Category"), "test") + { + override val isEnabled: Boolean + get() = General.tests + + var testBoolean by boolean( + title = Component.literal("Test Boolean"), + comment = Component.literal("Test Comment") + ) + + val testInt by int( + title = Component.literal("Test Int"), + ) + + val testLong by long( + title = Component.literal("Test Long"), + ) + + val testIntSlider by intSlider( + title = Component.literal("Test Int Slider"), + min = Int.MIN_VALUE / 2 + 1, + max = Int.MAX_VALUE / 2 + ) + + val testLongSlider by longSlider( + title = Component.literal("Test Long Slider"), + min = Long.MIN_VALUE / 2 + 1, + max = Long.MAX_VALUE / 2, + ) + + val testFloat by float( + title = Component.literal("Test Float"), + ) + + val testDouble by double( + title = Component.literal("Test Double"), + ) + + val testString by string( + title = Component.literal("Test String"), + ) + + val testSpec by spec( + title = Component.literal("Test Spec"), + default = TestSpec(), + factory = ::TestSpec + ) + + val testRegistry: BlockItem by registry( + title = Component.literal("Test Registry"), + default = Items.COBBLESTONE, + subclass = BlockItem::class, + registry = BuiltInRegistries.ITEM + ) + + val testKeycode by keycode( + title = Component.literal("Test Keycode"), + ) + + val testColor by color( + title = Component.literal("Test Color"), + alpha = true + ) + + val testEnumSelector by enumSelector( + title = Component.literal("Test Enum Selector"), + kclass = TestEnum::class, + default = TestEnum.Foo + ) + + val testSelector by selector( + title = Component.literal("Test Selector"), + kclass = String::class, + default = "foo", + entries = buildArray { + add("foo") + add("bar") + } + ) + + val testIntList by intList( + title = Component.literal("Test Int List"), + ) + + val testLongList by longList( + title = Component.literal("Test Long List"), + ) + + val testFloatList by floatList( + title = Component.literal("Test Float List"), + ) + + val testDoubleList by doubleList( + title = Component.literal("Test Double List"), + ) + + val testStringList by stringList( + title = Component.literal("Test String List"), + ) + + val testSpecList by specList( + title = Component.literal("Test Spec List"), + factory = ::TestSpec + ) + + val testRegistryList: List by registryList( + title = Component.literal("Test Registry List"), + factory = Items::COBBLESTONE, + subclass = BlockItem::class, + registry = BuiltInRegistries.ITEM + ) + + val testKeycodeList by keycodeList( + title = Component.literal("Test Keycode List"), + ) + + val testColorList by colorList( + title = Component.literal("Test Color List"), + ) + + val testIntMap by intMap( + title = Component.literal("Test Int Map"), + ) + + val testLongMap by longMap( + title = Component.literal("Test Long Map"), + ) + + val testFloatMap by floatMap( + title = Component.literal("Test Float Map"), + ) + + val testDoubleMap by doubleMap( + title = Component.literal("Test Double Map"), + ) + + val testStringMap by stringMap( + title = Component.literal("Test String Map"), + ) + + val testSpecMap by specMap( + title = Component.literal("Test Spec Map"), + factory = ::TestSpec + ) + + val testRegistryMap: Map by registryMap( + title = Component.literal("Test Registry Map"), + factory = Items::COBBLESTONE, + subclass = BlockItem::class, + registry = BuiltInRegistries.ITEM + ) + + val testKeycodeMap by keycodeMap( + title = Component.literal("Test Keycode Map"), + ) + + val testColorMap by colorMap( + title = Component.literal("Test Color Map") + ) + + val testNestedSpec by spec( + title = Component.literal("Test Nested Spec"), + default = TestNestedSpec(), + factory = ::TestNestedSpec + ) + + @Serializable + enum class TestEnum + { + Foo, + Bar + } + + class TestSpec : DataSpec(Component.literal("Test Spec")) + { + val test by boolean( + title = Component.literal("Test"), + ) + } + + class TestNestedSpec : DataSpec(Component.literal("Test Nested Spec")) + { + val childrenList by specList( + title = Component.literal("Children List"), + factory = ::TestNestedSpec + ) + + val childrenMap by specMap( + title = Component.literal("Children Map"), + factory = ::TestNestedSpec + ) + } + + object TestSub : CategorySpec(Component.literal("Test Subcategory"), "test_sub") + { + val test by boolean( + title = Component.literal("Test"), + ) + + val testRegistry by registry( + title = Component.literal("Test Registry"), + default = BlockEntityType.CHEST, + registry = BuiltInRegistries.BLOCK_ENTITY_TYPE + ) + + object TestSubSub : CategorySpec(Component.literal("Test Sub Subcategory"), "test_sub_sub") + { + val test by boolean( + title = Component.literal("Test"), + ) + } + } + } + } + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt new file mode 100644 index 000000000..4526a34a0 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt @@ -0,0 +1,21 @@ +package net.kernelpanicsoft.archie + +/** + * Extension point for modules that hook into a dedicated datagen/gametest run without `archie-core` + * needing a compile-time dependency on them. `archie-datagen`/`archie-gametest` each register an + * implementation via `META-INF/services/net.kernelpanicsoft.archie.ArchieExtension` + * ([java.util.ServiceLoader]); [Archie.init] invokes whichever hook applies, and does nothing if + * neither module is on the classpath (the normal case for a production build). + */ +interface ArchieExtension +{ + /** Called from [Archie.init] when running under a datagen task ([net.kernelpanicsoft.archie.data.ADataGeneratorPlatform.isDataGen]). */ + fun onDataGen() + { + } + + /** Called from [Archie.init] when running under a GameTest task ([net.kernelpanicsoft.archie.gametest.AGameTestPlatform.isGameTest]). */ + fun onGameTest() + { + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/block/entity/NBTBlockEntity.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/block/entity/NBTBlockEntity.kt new file mode 100644 index 000000000..e2cdfddc9 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/block/entity/NBTBlockEntity.kt @@ -0,0 +1,60 @@ +package net.kernelpanicsoft.archie.block.entity + +import net.kernelpanicsoft.archie.serialization.NBTHolder +import net.minecraft.core.BlockPos +import net.minecraft.core.HolderLookup +import net.minecraft.nbt.CompoundTag +import net.minecraft.network.protocol.Packet +import net.minecraft.network.protocol.game.ClientGamePacketListener +import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket +import net.minecraft.world.level.block.entity.BlockEntity +import net.minecraft.world.level.block.entity.BlockEntityType +import net.minecraft.world.level.block.state.BlockState + +/** + * A [BlockEntity] base class that automatically persists fields declared with [NBTHolder] + * delegates to and from the block entity's [CompoundTag]. + * + * Subclass this and declare fields using the [NBTHolder] delegation API: + * ```kotlin + * class MyBlockEntity(pos: BlockPos, state: BlockState) + * : NBTBlockEntity(MY_TYPE, pos, state) { + * + * var energy by nbt.intField() + * var label by nbt.stringField { "default" } + * val items by nbt.itemField(9) + * } + * ``` + * + * Saving and loading are handled automatically via [saveAdditional] and [loadAdditional]. + * Call [BlockEntity.setChanged] to push the block entity state to tracking clients. + */ +abstract class NBTBlockEntity(type: BlockEntityType<*>, pos: BlockPos, blockState: BlockState) : BlockEntity( + type, pos, + blockState +), NBTHolder by NBTHolder.create() +{ + override fun loadAdditional(compoundTag: CompoundTag, provider: HolderLookup.Provider) + { + super.loadAdditional(compoundTag, provider) + loadFromTag(compoundTag) + } + + override fun saveAdditional(compoundTag: CompoundTag, provider: HolderLookup.Provider) + { + super.saveAdditional(compoundTag, provider) + saveToTag(compoundTag) + } + + /** Returns the [NBTHolder] sync tag sent to tracking clients; see [NBTHolder.getSyncTag]. */ + override fun getUpdateTag(provider: HolderLookup.Provider): CompoundTag + { + return getSyncTag() + } + + /** Builds the block entity update packet carrying [getUpdateTag]'s data. */ + override fun getUpdatePacket(): Packet? + { + return ClientboundBlockEntityDataPacket.create(this) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CategorySpec.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CategorySpec.kt new file mode 100644 index 000000000..c71c62fbd --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CategorySpec.kt @@ -0,0 +1,31 @@ +package net.kernelpanicsoft.archie.config + +import net.minecraft.network.chat.Component +import kotlin.reflect.KClass +import kotlin.reflect.full.isSubclassOf + +/** + * A top-level section of a [ConfigSpec], or a nested subsection of another [CategorySpec]. + * Declared as a nested singleton `object` inside a [ConfigSpec] or a parent [CategorySpec] - + * [ConfigSpec.categories] and [subcategories] both discover their members by reflecting over + * nested objects, so there's nothing to override or register manually. + */ +abstract class CategorySpec(title: Component, id: String = title.string.toSnakeCase()) : DataSpec(title, id) +{ + /** Nested [CategorySpec] objects declared inside this one, for grouping in the UI. */ + val subcategories: List + get() = this::class.nestedClasses + .filterIsInstance>() + .filter { it.isSubclassOf(CategorySpec::class) } + .mapNotNull { klass -> klass.objectInstance } + + override fun init() + { + super.init() + subcategories.forEach { cat -> + types[cat.id] = FieldType.Category(cat) + if (cat.subcategories.isNotEmpty()) + cat.init() + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigContainer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigContainer.kt new file mode 100644 index 000000000..19eb9b825 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigContainer.kt @@ -0,0 +1,48 @@ +package net.kernelpanicsoft.archie.config + +import me.shedaniel.clothconfig2.api.ConfigBuilder +import net.kernelpanicsoft.archie.config.builder.startConfigField +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.components.Button +import net.minecraft.client.gui.screens.Screen +import net.minecraft.network.chat.Component + +/** + * Client-side mirror of a [ConfigContainer]. If the container holds exactly one [ConfigSpec], + * [buildConfigContainer] opens that spec's own screen directly; with more than one, it builds a + * screen listing an "Edit" entry per spec (via `ConfigFieldBuilder`/`ConfigSpecEntry`) that drills + * into that spec's screen. A [ConfigSpec.Type.SERVER] entry is hidden while not in a world. + */ +class ClientConfigContainer(internal var container: ConfigContainer) +{ + fun buildConfigContainer(parent: Screen): Screen + { + val isWorld = Minecraft.getInstance().level != null + val configs = container.configs + if (configs.size == 1) + { + if (!isWorld && configs.first().type == ConfigSpec.Type.SERVER) + return parent + return configs.first().client.buildConfig(parent) + } + return ConfigBuilder.create().apply { + title = container.title + val category = getOrCreateCategory(container.title) + configs.filter { it.type != ConfigSpec.Type.SERVER || isWorld }.forEach { config -> + val entryBuilder = entryBuilder() + category.addEntry( + entryBuilder.startConfigField(config.title, config) + .build() + ) + } + setFallbackCategory(category) + parentScreen = parent + setAfterInitConsumer { configScreen -> + configScreen.removeWidget(configScreen.children().first { it is Button && it.message == Component.empty() }) + } + }.build() + } + + /** Registers this spec's config screen with the platform's mod-list UI, client-side only. */ + fun initClient() = container.mod.registerConfigurationScreen(::buildConfigContainer) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigSpec.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigSpec.kt new file mode 100644 index 000000000..1fa7eccb4 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigSpec.kt @@ -0,0 +1,43 @@ +package net.kernelpanicsoft.archie.config + +import me.shedaniel.clothconfig2.api.ConfigBuilder +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.screens.Screen + +/** Client-side mirror of a [ConfigSpec], built lazily as [ConfigSpec.client]; builds the Cloth Config UI screen. */ +@Suppress("unused") +class ClientConfigSpec(internal var spec: ConfigSpec) +{ + /** + * Builds a fresh Cloth Config [ConfigBuilder] for [spec]: one category per enabled entry of + * [ConfigSpec.categoriesMap]. Saving writes locally via [ConfigSpec.save] for + * [ConfigSpec.Type.COMMON]/[ConfigSpec.Type.CLIENT]/[ConfigSpec.Type.STARTUP] specs, or sends + * the edited config to the server over [ConfigSpec.channel] for [ConfigSpec.Type.SERVER] specs. + */ + fun buildConfig(parent: Screen): Screen + { + return ConfigBuilder.create().apply { + title = spec.title + savingRunnable = Runnable { + when (spec.type) + { + ConfigSpec.Type.COMMON, + ConfigSpec.Type.CLIENT, + ConfigSpec.Type.STARTUP -> spec.save() + ConfigSpec.Type.SERVER -> spec.channel.toServer(spec) + } + } + spec.categoriesMap.values.forEach { value -> + if (value.isEnabled) + { + val entryBuilder = entryBuilder() + val category = getOrCreateCategory(value.title) + + value.client.buildRoot(category, entryBuilder) + } + } + parentScreen = parent + }.build() + } + +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientDataSpec.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientDataSpec.kt new file mode 100644 index 000000000..2185c3069 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientDataSpec.kt @@ -0,0 +1,1265 @@ +package net.kernelpanicsoft.archie.config + +import me.shedaniel.clothconfig2.api.AbstractConfigListEntry +import me.shedaniel.clothconfig2.api.ConfigCategory +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.SubCategoryListEntry +import me.shedaniel.math.Color +import net.kernelpanicsoft.archie.config.builder.* +import net.kernelpanicsoft.archie.util.toMutableEntry +import net.minecraft.core.Registry +import net.minecraft.network.chat.Component +import java.util.function.Consumer +import java.util.function.Supplier +import kotlin.reflect.KClass + +/** + * Client-side mirror of a [DataSpec], built lazily as [DataSpec.client]. Every `boolean`/ + * `int`/... method here is called by its [DataSpec] counterpart (via [DataSpec.onClient]) + * with matching parameters, and queues a [ConfigEntryBuilder]-based entry that reads from and + * writes back into the same backing maps on [spec]. [buildRoot]/[buildSub] then turn the queued + * entries into an actual Cloth Config [ConfigCategory]/[SubCategoryListEntry]. None of this is + * called directly by mod authors - see [DataSpec] for the public DSL. + */ +@Suppress("unused") +class ClientDataSpec(internal val spec: DataSpec) +{ + /** Queued entry builders, appended to in declaration order by each field-registering method below. */ + internal val builders: MutableList AbstractConfigListEntry<*>> = mutableListOf() + + /** Builds this category's entries plus its subcategories (as nested [SubCategoryListEntry]s) directly into the top-level [category]. */ + internal fun buildRoot(category: ConfigCategory, entryBuilder: ConfigEntryBuilder) + { + builders.forEach { builder -> + category.addEntry(entryBuilder.builder()) + } + if (spec is CategorySpec) + { + spec.subcategories.forEach { subcategory -> + category.addEntry(subcategory.client.buildSub(entryBuilder)) + } + } + } + + /** Builds this category (and its subcategories, recursively) as a single [SubCategoryListEntry]. */ + internal fun buildSub(entryBuilder: ConfigEntryBuilder): SubCategoryListEntry + { + val category = entryBuilder.startSubCategory(spec.title) + + builders.forEach { builder -> + category.add(entryBuilder.builder()) + } + + if (spec is CategorySpec) + { + spec.subcategories.forEach { subcategory -> + category.add(subcategory.client.buildSub(entryBuilder)) + } + } + + return category.build() + } + + /** Queues a read-only description entry showing [text], used to render a field's `comment` above it. */ + internal fun comment(text: Component) + { + builders.add { startTextDescription(text).build() } + } + + internal fun boolean( + id: String, + title: Component, + comment: Component? = null, + default: Boolean = false, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.booleans.getOrPut(id) { default } + val ret = startBooleanToggle(title, spec.booleans.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { + spec.booleans[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + + internal fun int( + id: String, + title: Component, + comment: Component? = null, + default: Int = 0, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.ints.getOrPut(id) { default } + val ret = startIntField(title, spec.ints.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { + spec.ints[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + + + } + + internal fun long( + id: String, + title: Component, + comment: Component? = null, + default: Long = 0, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.longs.getOrPut(id) { default } + val ret = startLongField(title, spec.longs.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { + spec.longs[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + + + } + + internal fun intSlider( + id: String, + title: Component, + comment: Component? = null, + min: Int, + max: Int, + default: Int = min + (max - min) / 2, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.ints.getOrPut(id) { default } + val ret = startIntSlider(title, spec.ints.getOrPut(id) { default }, min, max) + .apply { + saveConsumer = Consumer { + spec.ints[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + + + } + + internal fun longSlider( + id: String, + title: Component, + comment: Component? = null, + min: Long, + max: Long, + default: Long = min + (max - min) / 2, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.longs.getOrPut(id) { default } + val ret = startLongSlider(title, spec.longs.getOrPut(id) { default }, min, max) + .apply { + saveConsumer = Consumer { + spec.longs[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + + + } + + internal fun float( + id: String, + title: Component, + comment: Component? = null, + default: Float = 0.0f, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.floats.getOrPut(id) { default } + val ret = startFloatField(title, spec.floats.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { + spec.floats[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + + + } + + internal fun double( + id: String, + title: Component, + comment: Component? = null, + default: Double = 0.0, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.doubles.getOrPut(id) { default } + val ret = startDoubleField(title, spec.doubles.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { + spec.doubles[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + + + } + + internal fun string( + id: String, + title: Component, + comment: Component? = null, + default: String = "", + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.strings.getOrPut(id) { default } + val ret = startStrField(title, spec.strings.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { + spec.strings[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + + + } + + @Suppress("UNCHECKED_CAST") + internal fun spec( + id: String, + title: Component, + comment: Component? = null, + default: T, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + (spec.specs as MutableMap).getOrPut(id) { default } + val ret = startSpecField( + title, + spec.specs.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { + spec.specs[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + + + } + + internal fun registry( + id: String, + title: Component, + comment: Component? = null, + default: T, + registry: Registry, + resetKey: Component? = null, + subclass: KClass? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + val ret = startRegistryField( + title, + registry.get(spec.registries.getOrPut(id) { + registry.getKey( + default + )!! + }) ?: default, + subclass, + registry + ) + .apply { + setSaveConsumer { + spec.registries[id] = registry.getKey(it)!! + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun keycode( + id: String, + title: Component, + comment: Component? = null, + default: CommonKeyCode = CommonKeyCode.unknown, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + val ret = startModifierKeyCodeField( + title, + spec.keycodes.getOrPut(id) { default }.toClient() + ) + .apply { + setModifierSaveConsumer { + spec.keycodes[id] = it.toCommon() + } + setModifierDefaultValue { + default.toClient() + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun color( + id: String, + title: Component, + comment: Component? = null, + alpha: Boolean = false, + default: Color = if (alpha) Color.ofTransparent(-1) else Color.ofOpaque(-1), + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + val ret = startColorField( + title, + spec.colors.getOrPut(id) { default }.color + ) + .apply { + setSaveConsumer2 { + spec.colors[id] = it + } + setDefaultValue2 { + default + } + alphaMode = alpha + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + @Suppress("UNCHECKED_CAST") + internal fun > enumSelector( + id: String, + title: Component, + comment: Component? = null, + kclass: KClass, + default: T, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.enums.getOrPut(id) { default } + val ret = startEnumSelector(title, kclass.java, spec.enums.getOrPut(id) { default } as T) + .apply { + saveConsumer = Consumer { + spec.enums[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + @Suppress("UNCHECKED_CAST") + internal fun selector( + id: String, + title: Component, + comment: Component? = null, + kclass: KClass, + default: T, + entries: Array, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.selectors.getOrPut(id) { default } + val ret = startSelector(title, entries, spec.selectors.getOrPut(id) { default } as T) + .apply { + saveConsumer = Consumer { + spec.selectors[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun intList( + id: String, + title: Component, + comment: Component? = null, + default: List = listOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.intLists.getOrPut(id) { default } + val ret = startIntList( + title, + spec.intLists.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { + spec.intLists[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun longList( + id: String, + title: Component, + comment: Component? = null, + default: List = listOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.longLists.getOrPut(id) { default } + val ret = startLongList( + title, + spec.longLists.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { + spec.longLists[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun floatList( + id: String, + title: Component, + comment: Component? = null, + default: List = listOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.floatLists.getOrPut(id) { default } + val ret = startFloatList( + title, + spec.floatLists.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { + spec.floatLists[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun doubleList( + id: String, + title: Component, + comment: Component? = null, + default: List = listOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.doubleLists.getOrPut(id) { default } + val ret = startDoubleList( + title, + spec.doubleLists.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { + spec.doubleLists[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun stringList( + id: String, + title: Component, + comment: Component? = null, + default: List = listOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.stringLists.getOrPut(id) { default } + val ret = startStrList( + title, + spec.stringLists.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { + spec.stringLists[id] = it + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + @Suppress("UNCHECKED_CAST") + internal fun specList( + id: String, + title: Component, + comment: Component? = null, + default: List = listOf(), + factory: () -> T, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + (spec.specLists as MutableMap>).getOrPut(id) { default } + val ret = startSpecList( + title, + spec.specLists.getOrPut( + id + ) { default }, + factory + ) + .apply { + saveConsumer = Consumer { + spec.specLists[id] = it as List + } + defaultValue = Supplier { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun registryList( + id: String, + title: Component, + comment: Component? = null, + default: List = listOf(), + factory: () -> T, + registry: Registry, + resetKey: Component? = null, + subclass: KClass? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + val ret = startRegistryList( + title, + spec.registryLists.getOrPut( + id + ) { default.map { registry.getKey(it)!! } } + .map { registry.get(it) ?: factory() }, + factory, + subclass, + registry + ) + .apply { + setSaveConsumer { value -> + spec.registryLists[id] = value.map { registry.getKey(it)!! } + } + + setDefaultValue { + default + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun keycodeList( + id: String, + title: Component, + comment: Component? = null, + default: List = listOf(), + factory: () -> CommonKeyCode = CommonKeyCode.Companion::unknown, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.keycodeLists.getOrPut(id) { default } + val ret = startKeycodeList( + title, + spec.keycodeLists.getOrPut( + id + ) { default }.map { it.toClient() } + ) { factory().toClient() } + .apply { + saveConsumer = Consumer { value -> + spec.keycodeLists[id] = value.map { it.toCommon() } + } + defaultValue = Supplier { + default.map { it.toClient() } + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun colorList( + id: String, + title: Component, + comment: Component? = null, + alpha: Boolean = false, + default: List = listOf(), + factory: () -> Color = { if (alpha) Color.ofTransparent(-1) else Color.ofOpaque(-1) }, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + val ret = startColorList( + title, + spec.colorLists.getOrPut(id) { default }, + factory + ) + .apply { + setSaveConsumer { + spec.colorLists[id] = if (alpha) + it.map(Color::ofTransparent) + else + it.map(Color::ofOpaque) + } + setDefaultValue { + default.map { it.color } + } + alphaMode = alpha + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun intMap( + id: String, + title: Component, + comment: Component? = null, + default: Map = mapOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.intMaps.getOrPut(id) { default } + val ret = startIntMap( + title, + spec.intMaps.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { value -> + spec.intMaps[id] = value.associate { it.toPair() } + } + defaultValue = Supplier { + default.entries.toList().map { it.toMutableEntry() } + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun longMap( + id: String, + title: Component, + comment: Component? = null, + default: Map = mapOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.longMaps.getOrPut(id) { default } + val ret = startLongMap( + title, + spec.longMaps.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { value -> + spec.longMaps[id] = value.associate { it.toPair() } + } + defaultValue = Supplier { + default.entries.toList().map { it.toMutableEntry() } + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun floatMap( + id: String, + title: Component, + comment: Component? = null, + default: Map = mapOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.floatMaps.getOrPut(id) { default } + val ret = startFloatMap( + title, + spec.floatMaps.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { value -> + spec.floatMaps[id] = value.associate { it.toPair() } + } + defaultValue = Supplier { + default.entries.toList().map { it.toMutableEntry() } + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun doubleMap( + id: String, + title: Component, + comment: Component? = null, + default: Map = mapOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.doubleMaps.getOrPut(id) { default } + val ret = startDoubleMap( + title, + spec.doubleMaps.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { value -> + spec.doubleMaps[id] = value.associate { it.toPair() } + } + defaultValue = Supplier { + default.entries.toList().map { it.toMutableEntry() } + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun stringMap( + id: String, + title: Component, + comment: Component? = null, + default: Map = mapOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.stringMaps.getOrPut(id) { default } + val ret = startStrMap( + title, + spec.stringMaps.getOrPut(id) { default }) + .apply { + saveConsumer = Consumer { value -> + spec.stringMaps[id] = value.associate { it.toPair() } + } + defaultValue = Supplier { + default.entries.toList().map { it.toMutableEntry() } + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + @Suppress("UNCHECKED_CAST") + internal fun specMap( + id: String, + title: Component, + comment: Component? = null, + default: Map = mapOf(), + factory: () -> T, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + (spec.specMaps as MutableMap>).getOrPut(id) { default } + val ret = startSpecMap( + title, + spec.specMaps.getOrPut( + id + ) { default }, + factory + ) + .apply { + saveConsumer = Consumer { value -> + spec.specMaps[id] = value.associate { it.toPair() } + } + defaultValue = Supplier { + default.entries.toList().map { it.toMutableEntry() } + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun registryMap( + id: String, + title: Component, + comment: Component? = null, + default: Map = mapOf(), + factory: () -> T, + registry: Registry, + resetKey: Component? = null, + subclass: KClass? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + val ret = startRegistryMap( + title, + spec.registryMaps.getOrPut( + id + ) { default.mapValues { registry.getKey(it.value)!! } } + .mapValues { + registry.get(it.value) ?: factory() + }, + factory, + subclass, + registry + ) + .apply { + setSaveConsumer { value -> + spec.registryMaps[id] = + value.associate { it.toPair() }.mapValues { registry.getKey(it.value)!! } + } + + setDefaultValue { + default.toList().map { it.toMutableEntry() } + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun keycodeMap( + id: String, + title: Component, + comment: Component? = null, + default: Map = mapOf(), + factory: () -> CommonKeyCode = CommonKeyCode.Companion::unknown, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + spec.keycodeMaps.getOrPut(id) { default } + val ret = startKeycodeMap( + title, + spec.keycodeMaps.getOrPut(id) { default }.mapValues { it.value.toClient() } + ) { factory().toClient() } + .apply { + saveConsumer = Consumer { value -> + spec.keycodeMaps[id] = + value.associate { it.toPair() }.mapValues { it.value.toCommon() } + } + defaultValue = Supplier { + default.mapValues { it.value.toClient() }.entries.toList().map { it.toMutableEntry() } + } + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } + + internal fun colorMap( + id: String, + title: Component, + comment: Component? = null, + alpha: Boolean = false, + default: Map = mapOf(), + factory: () -> Color = { if (alpha) Color.ofTransparent(-1) else Color.ofOpaque(-1) }, + resetKey: Component? = null, + needsRestart: Boolean = false + ) + { + if (comment != null) + { + comment(comment) + } + builders.add { + val reset = resetButtonKey + resetButtonKey = resetKey ?: resetButtonKey + val ret = startColorMap( + title, + spec.colorMaps.getOrPut( + id + ) { default }, + factory + ) + .apply { + setSaveConsumer { value -> + spec.colorMaps[id] = if (alpha) + value.associate { it.toPair() }.mapValues { Color.ofTransparent(it.value) } + else + value.associate { it.toPair() }.mapValues { Color.ofOpaque(it.value) } + } + setDefaultValue { + default.mapValues { it.value.color }.toList().map { it.toMutableEntry() } + } + alphaMode = alpha + requireRestart(needsRestart) + } + .build() + resetButtonKey = reset + ret + } + } +} + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CommonKeyCode.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CommonKeyCode.kt new file mode 100644 index 000000000..f4cb133a3 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CommonKeyCode.kt @@ -0,0 +1,84 @@ +package net.kernelpanicsoft.archie.config + +import com.mojang.blaze3d.platform.InputConstants +import kotlinx.serialization.Serializable +import me.shedaniel.clothconfig2.api.Modifier +import me.shedaniel.clothconfig2.api.ModifierKeyCode + +/** + * A serializable, client-independent representation of a keybind, used by [DataSpec.keycode] + * fields so config files don't depend on Cloth Config's [ModifierKeyCode]. Convert to/from the + * client type with [toClient]/[toCommon]. + */ +@Serializable +data class CommonKeyCode(val type: Type, val key: Int, val modifiers: Set) +{ + constructor(type: Type, key: Int, vararg modifiers: Modifier) : this(type, key, modifiers.toSet()) + + /** Which [InputConstants] key space [key] is a code in. */ + @Serializable + enum class Type + { + KEYSYM, + SCANCODE, + MOUSE; + } + + /** A modifier key held alongside the base [key]. */ + @Serializable + enum class Modifier + { + ALT, + CONTROL, + SHIFT + } + + companion object + { + /** Sentinel for "no key bound". */ + val unknown: CommonKeyCode = CommonKeyCode(Type.KEYSYM, -1) + } +} + +private val CommonKeyCode.modifier: Modifier + get() + { + var alt = false + var control = false + var shift = false + modifiers.forEach { + when (it) + { + CommonKeyCode.Modifier.ALT -> alt = true + CommonKeyCode.Modifier.CONTROL -> control = true + CommonKeyCode.Modifier.SHIFT -> shift = true + } + } + return Modifier.of(alt, control, shift) + } + +/** Converts this to Cloth Config's client-side [ModifierKeyCode]. */ +fun CommonKeyCode.toClient(): ModifierKeyCode = when (type) +{ + CommonKeyCode.Type.KEYSYM -> ModifierKeyCode.of(InputConstants.Type.KEYSYM.getOrCreate(key), modifier) + CommonKeyCode.Type.SCANCODE -> ModifierKeyCode.of(InputConstants.Type.SCANCODE.getOrCreate(key), modifier) + CommonKeyCode.Type.MOUSE -> ModifierKeyCode.of(InputConstants.Type.MOUSE.getOrCreate(key), modifier) +} + +private val ModifierKeyCode.modifiers: Set + get() = buildSet { + modifier.apply { + if (hasAlt()) add(CommonKeyCode.Modifier.ALT) + if (hasControl()) add(CommonKeyCode.Modifier.CONTROL) + if (hasShift()) add(CommonKeyCode.Modifier.SHIFT) + } + } + +/** Converts a Cloth Config [ModifierKeyCode] to the serializable [CommonKeyCode]. */ +@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") +fun ModifierKeyCode.toCommon(): CommonKeyCode = when (type) +{ + InputConstants.Type.KEYSYM -> CommonKeyCode(CommonKeyCode.Type.KEYSYM, keyCode.value, modifiers) + InputConstants.Type.SCANCODE -> CommonKeyCode(CommonKeyCode.Type.SCANCODE, keyCode.value, modifiers) + InputConstants.Type.MOUSE -> CommonKeyCode(CommonKeyCode.Type.MOUSE, keyCode.value, modifiers) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigContainer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigContainer.kt new file mode 100644 index 000000000..62c620040 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigContainer.kt @@ -0,0 +1,60 @@ +package net.kernelpanicsoft.archie.config + +import dev.architectury.platform.Mod +import dev.architectury.platform.Platform +import net.kernelpanicsoft.archie.APlatform +import net.kernelpanicsoft.archie.util.onClient +import net.minecraft.network.chat.Component +import kotlin.reflect.KClass +import kotlin.reflect.full.isSubclassOf + +/** + * The single per-mod root of the config system, declared as a singleton `object` holding one or + * more nested [ConfigSpec] objects: + * ```kotlin + * object Config : ConfigContainer(MyMod.MOD) { + * object Common : ConfigSpec.Common(MyMod.MOD) { ... } + * object Client : ConfigSpec.Client(MyMod.MOD) { ... } + * } + * ``` + * Call [init] once during common mod init, on both physical sides; it initializes every nested + * [ConfigSpec] (loading/creating its file per its [ConfigSpec.predicate] timing) and, on the + * client, builds and registers the merged Cloth Config UI screen via [ClientConfigContainer]. + * + * @param mod The owning mod, used to derive each nested [ConfigSpec]'s default filename. + * @param title Display title used for the container's screen when it holds more than one + * [ConfigSpec]. Defaults to [mod]'s name. + */ +abstract class ConfigContainer(val mod: Mod, val title: Component = Component.literal(mod.name)) +{ + /** Client-side mirror of this container, used to build the merged Cloth Config UI screen. */ + internal val client by lazy { ClientConfigContainer(this) } + + /** Nested [ConfigSpec] objects declared inside this container. */ + val configs: List + get() = this::class.nestedClasses + .filterIsInstance>() + .filter { it.isSubclassOf(ConfigSpec::class) } + .mapNotNull { klass -> klass.objectInstance } + + /** Initializes every nested [ConfigSpec] and, on the client, registers the config UI screen. */ + fun init() + { + configs.forEach(ConfigSpec::init) + onClient { initClient() } + } + + internal fun initClient() + { + // Cloth Config's mod id differs by loader: Fabric allows hyphens ("cloth-config"), while + // NeoForge's mod id charset doesn't, so its variant registers as "cloth_config" instead. + val clothConfigModId = when (val platform = APlatform.platform) + { + "fabric" -> "cloth-config" + "neoforge" -> "cloth_config" + else -> throw UnsupportedOperationException("Unsupported platform: $platform") + } + if (Platform.isModLoaded(clothConfigModId)) + client.initClient() + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigSpec.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigSpec.kt new file mode 100644 index 000000000..2b4c5916c --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigSpec.kt @@ -0,0 +1,313 @@ +package net.kernelpanicsoft.archie.config + +import dev.architectury.event.events.client.ClientLifecycleEvent +import dev.architectury.event.events.client.ClientPlayerEvent +import dev.architectury.event.events.common.LifecycleEvent +import dev.architectury.event.events.common.PlayerEvent +import net.kernelpanicsoft.archie.config.serializer.Json5ConfigSerializer +import net.kernelpanicsoft.archie.config.serializer.TomlConfigSerializer +import net.kernelpanicsoft.archie.APlatform +import net.kernelpanicsoft.archie.util.onClient +import dev.architectury.platform.Mod +import dev.architectury.platform.Platform +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.* +import net.kernelpanicsoft.archie.config.ConfigSpec.Server.Companion.CONFIG_DIR +import net.kernelpanicsoft.archie.networking.NetworkChannel +import net.kernelpanicsoft.archie.serialization.SerializationManager +import net.kernelpanicsoft.archie.util.foldEnv +import net.kernelpanicsoft.archie.util.isClient +import net.kernelpanicsoft.archie.util.rem +import net.minecraft.network.chat.Component +import net.minecraft.world.level.storage.LevelResource +import java.nio.file.Path +import java.util.function.Predicate +import kotlin.reflect.KClass +import kotlin.reflect.full.isSubclassOf + +/** + * The root of a mod's config. [ConfigSpec] is sealed - declare one or more nested singleton + * `object`s subclassing [Common], [Client], [Server], or [Startup] (nested inside a + * [ConfigContainer]) depending on when the config should load and whether it should sync. + * [categories] are discovered automatically from nested [CategorySpec] objects - no need to + * override anything: + * ```kotlin + * object Config : ConfigContainer(MyMod.MOD) { + * object MyConfig : ConfigSpec.Common(MyMod.MOD, Component.literal("My Config")) { + * object General : CategorySpec(Component.literal("General"), "general") { ... } + * object Advanced : CategorySpec(Component.literal("Advanced"), "advanced") { ... } + * } + * } + * ``` + * Call [ConfigContainer.init] once during common mod init (on both physical sides); it loads (or + * creates) the config file(s) and, on the client, also registers the Cloth Config UI screen(s). + * There is no separate client-side init step to call. + * + * @param mod The owning mod, used to derive the default [filename] and locate the config + * directory. + * @param title Display title of the config, shown as the Cloth Config screen title. + * @param id Unique identifier for this config, used to derive the default [filename] and + * register the network channel. Defaults to the snake-cased [title]. + */ +@Suppress("unused") +sealed class ConfigSpec(val type: Type, val mod: Mod, val title: Component, val id: String = title.string.toSnakeCase()) +{ + internal val channel = NetworkChannel(mod % id) + /** Client-side mirror of this spec, used to build the Cloth Config UI screen. */ + internal val client by lazy { ClientConfigSpec(this) } + + open val synchronized = false + + /** Top-level sections of this config. */ + val categories: List + get() = this::class.nestedClasses + .filterIsInstance>() + .filter { it.isSubclassOf(CategorySpec::class) } + .mapNotNull { klass -> klass.objectInstance } + + /** [categories] indexed by [DataSpec.id]. */ + internal val categoriesMap: Map by lazy { + categories.associateBy { it.id } + } + + /** + * Serializer used to read/write the config file. Defaults per-platform: JSON5 on Fabric, + * TOML on NeoForge. Override to force a specific format regardless of platform. + */ + protected open val fileSerializer: IConfigSerializer = when (val platform = APlatform.platform) + { + "fabric" -> Json5ConfigSerializer + "neoforge" -> TomlConfigSerializer + else -> throw UnsupportedOperationException("Unsupported platform: $platform") + } + + /** Path of the config file, relative to the game's config directory, without extension. */ + open val filename: String = "${mod.modId}/${id}" + + /** Whether [load] has run at least once. */ + var isLoaded: Boolean = false + protected set + + var configFolder: Path = Platform.getConfigFolder() + protected set + + private var isEventsRegistered: Boolean = false + + abstract val predicate: () -> Boolean + + /** + * Registers all [categories] (and their subcategories) and, for a [synchronized] spec, + * registers this spec's [NetworkChannel] plus the player-join/quit listeners that push it to + * joining clients and reset [isLoaded] on disconnect. + * + * This does **not** load the config file or touch the client UI - each of [Common], [Client], + * [Server], and [Startup] overrides this to additionally register the lifecycle event + * (documented on that subclass) that calls [load] at the right time. The client-side settings + * screen is built and registered separately, once every nested spec in the container is + * initialized, by [ConfigContainer.initClient]. Call [ConfigContainer.init], not this method + * directly. + */ + open fun init() + { + SerializationManager { + module { + contextual(this@ConfigSpec::class) { + serializer + } + } + } + categoriesMap.values.forEach { cat -> + cat.init() + } + if (synchronized && !isEventsRegistered) + { + channel.configServerbound(this) + channel.configClientbound(this) + channel.register() + foldEnv( + client = { + ClientPlayerEvent.CLIENT_PLAYER_QUIT.register { + this.isLoaded = false + } + }, + server = { + PlayerEvent.PLAYER_JOIN.register { player -> + if (!player.server.isSingleplayer) + channel.toPlayer(player, this) + + } + } + ) + } + isEventsRegistered = true + } + + + /** Reads the config file via [fileSerializer], creating it with defaults if absent, and marks [isLoaded]. */ + fun load() = fileSerializer.load(this, configFolder).also { isLoaded = true } + + /** Writes the current values of every field in [categories] to the config file via [fileSerializer]. */ + fun save() = fileSerializer.save(this, configFolder) + + /** Serializes/deserializes a [ConfigSpec] by delegating each entry of [categoriesMap] to its own [DataSpec.serializer]. */ + internal class ConfigSerializer(val factory: () -> ConfigSpec) : KSerializer + { + override val descriptor: SerialDescriptor by lazy { + with(factory()) + { + buildClassSerialDescriptor(title.string) + { + categoriesMap.forEach { (key, value) -> + element(key, value.serializer.descriptor) + } + } + } + } + + override fun deserialize(decoder: Decoder): ConfigSpec + { + return decoder.decodeStructure(descriptor) + { + val spec = factory() + with(spec) + { + while (true) + { + when (val index = decodeElementIndex(descriptor)) + { + in categoriesMap.entries.indices -> + { + val (_, value) = categoriesMap.entries.toList()[index] + decodeSerializableElement(descriptor, index, value.serializer) + } + + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + } + spec + } + } + + override fun serialize(encoder: Encoder, value: ConfigSpec) + { + encoder.encodeStructure(descriptor) + { + value.categoriesMap.entries.forEachIndexed { index, (_, value) -> + encodeSerializableElement(descriptor, index, value.serializer, value) + } + } + } + } + + enum class Type + { + COMMON, + CLIENT, + SERVER, + STARTUP + } + + abstract class Common(mod: Mod, title: Component = Component.literal("Common"), id: String = title.string.toSnakeCase()) : ConfigSpec( + Type.COMMON, + mod, + title, + id, + ) + { + private var isEventsRegistered: Boolean = false + + override fun init() + { + super.init() + if (!isEventsRegistered) + { + LifecycleEvent.SETUP.register { + load() + } + } + isEventsRegistered = true + } + + override val predicate: () -> Boolean = { isLoaded } + } + + abstract class Client(mod: Mod, title: Component = Component.literal("Client"), id: String = title.string.toSnakeCase()) : ConfigSpec( + Type.CLIENT, + mod, + title, + id, + ) + { + private var isEventsRegistered: Boolean = false + + override fun init() + { + super.init() + if (!isEventsRegistered) + { + onClient { + ClientLifecycleEvent.CLIENT_SETUP.register { + load() + } + } + } + isEventsRegistered = true + } + + override val predicate: () -> Boolean = { isLoaded && isClient } + } + + abstract class Server(mod: Mod, title: Component = Component.literal("Server"), id: String = title.string.toSnakeCase()) : ConfigSpec( + Type.SERVER, + mod, + title, + id, + ) { + private var isEventsRegistered: Boolean = false + + override val synchronized: Boolean = true + + override fun init() + { + super.init() + if (!isEventsRegistered) + { + LifecycleEvent.SERVER_BEFORE_START.register { server -> + configFolder = server.getWorldPath(CONFIG_DIR) + load() + } + } + isEventsRegistered = true + } + + override val predicate: () -> Boolean = { isLoaded } + + companion object + { + private val CONFIG_DIR = LevelResource("serverconfig") + } + } + + abstract class Startup(mod: Mod, title: Component = Component.literal("Startup"), id: String = title.string.toSnakeCase()) : ConfigSpec( + Type.STARTUP, + mod, + title, + id, + ) + { + override fun init() + { + super.init() + load() + } + + override val predicate: () -> Boolean = { isLoaded } + } + + /** [KSerializer] for this spec, used by [fileSerializer] to read/write the config file. */ + internal val serializer by lazy { ConfigSerializer { this } } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/DataSpec.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/DataSpec.kt new file mode 100644 index 000000000..522648f89 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/DataSpec.kt @@ -0,0 +1,1673 @@ +package net.kernelpanicsoft.archie.config + +import io.github.xn32.json5k.SerialComment +import kotlinx.serialization.KSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.* +import me.shedaniel.math.Color +import net.kernelpanicsoft.archie.serialization.SerializationManager +import net.kernelpanicsoft.archie.util.onClient +import net.minecraft.core.Registry +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.peanuuutz.tomlkt.TomlComment +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KClass +import kotlin.reflect.KProperty + +/** + * A group of config fields, declared by subclassing this and adding fields with the + * `by boolean(...)`, `by int(...)`, etc. delegates below. + * + * Each delegate call registers a field under an id derived from the *property* name + * (snake_cased), stores the field's [FieldType] and default value, and - on the client - mirrors + * the field into [ClientDataSpec] so Cloth Config can render it. The delegate itself just + * reads the current value back out of this category's backing maps, so config values are read + * with plain property access (e.g. `MyConfig.General.enableFeature`). + * + * [CategorySpec] is a [DataSpec] subclass used for a [ConfigSpec]'s top-level sections (and + * supports nested [CategorySpec.subcategories] for grouping in the UI). Subclass [DataSpec] + * directly instead for non-category values embedded as fields via `spec`/`specList`/`specMap`. + * + * @param title Display title shown in the Cloth Config UI. + * @param id Stable identifier used as this category's key in its parent and in the serialized + * file. Defaults to the snake_cased [title]. + */ +@Suppress("unused") +abstract class DataSpec(val title: Component, val id: String = title.string.toSnakeCase()) +{ + /** Client-side mirror of this category, used to build the Cloth Config UI. */ + val client by lazy { ClientDataSpec(this) } + + internal val types: MutableMap> = linkedMapOf() + internal val comments: MutableMap = mutableMapOf() + internal val booleans: MutableMap = mutableMapOf() + internal val ints: MutableMap = mutableMapOf() + internal val longs: MutableMap = mutableMapOf() + internal val floats: MutableMap = mutableMapOf() + internal val doubles: MutableMap = mutableMapOf() + internal val strings: MutableMap = mutableMapOf() + internal val specs: MutableMap = mutableMapOf() + internal val registries: MutableMap = mutableMapOf() + internal val keycodes: MutableMap = mutableMapOf() + internal val colors: MutableMap = mutableMapOf() + internal val enums: MutableMap> = mutableMapOf() + internal val selectors: MutableMap = mutableMapOf() + internal val intLists: MutableMap> = mutableMapOf() + internal val longLists: MutableMap> = mutableMapOf() + internal val floatLists: MutableMap> = mutableMapOf() + internal val doubleLists: MutableMap> = mutableMapOf() + internal val stringLists: MutableMap> = mutableMapOf() + internal val specLists: MutableMap> = mutableMapOf() + internal val registryLists: MutableMap> = mutableMapOf() + internal val keycodeLists: MutableMap> = mutableMapOf() + internal val colorLists: MutableMap> = mutableMapOf() + internal val intMaps: MutableMap> = mutableMapOf() + internal val longMaps: MutableMap> = mutableMapOf() + internal val floatMaps: MutableMap> = mutableMapOf() + internal val doubleMaps: MutableMap> = mutableMapOf() + internal val stringMaps: MutableMap> = mutableMapOf() + internal val specMaps: MutableMap> = mutableMapOf() + internal val registryMaps: MutableMap> = mutableMapOf() + internal val keycodeMaps: MutableMap> = mutableMapOf() + internal val colorMaps: MutableMap> = mutableMapOf() + + /** + * Whether this category is currently active. When `false`, Cloth Config hides/disables the + * category's fields in the UI. Override with a `get()` that reads another field (e.g. a + * parent toggle) to make this category conditional. + */ + open val isEnabled: Boolean = true + + /** Registers [subcategories] as fields on this category, recursively. */ + internal open fun init() + { + SerializationManager { + module { + contextual(this@DataSpec::class) { + serializer + } + } + } + } + + internal var accessPredicate: () -> Boolean = { false } + + /** + * Declares a `Boolean` config field, e.g. `val/var enableFeature by boolean(...)`. + * + * The field's id is the delegated property's name, snake_cased. On the client, the field is + * also registered with [ClientDataSpec] so it renders as a toggle in the Cloth Config UI. + * Writing back to this delegate will update the current value, and reading from it will + * return the current value. To persist changes, you must call [ConfigSpec.save] or the value will not be saved to disk. + * + * @param title Display title shown in the Cloth Config UI. + * @param comment Optional comment written next to the field in the serialized file (JSON5/TOML) + * and used as the UI tooltip. + * @param default Value used until a stored/loaded value overrides it. + * @param resetKey Optional label for the UI's "reset to default" control. + * @return A read/write property delegate exposing the field's current value. + */ + protected fun boolean( + title: Component, + comment: Component? = null, + default: Boolean = false, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.boolean(id, title, comment, default, resetKey) + } + types[id] = FieldType.Boolean + booleans.putIfAbsent(id, default) + object : ReadWriteProperty + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Boolean = booleans.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Boolean + ) { booleans[id] = value } + } + } + + /** Declares an `Int` config field. See [boolean] for parameter semantics. */ + protected fun int( + title: Component, + comment: Component? = null, + default: Int = 0, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.int(id, title, comment, default, resetKey) + } + types[id] = FieldType.Int + ints.putIfAbsent(id, default) + object : ReadWriteProperty + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Int = ints.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Int + ) { ints[id] = value } + } + } + + /** Declares a `Long` config field. See [boolean] for parameter semantics. */ + protected fun long( + title: Component, + comment: Component? = null, + default: Long = 0, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.long(id, title, comment, default, resetKey) + } + types[id] = FieldType.Long + longs.putIfAbsent(id, default) + object : ReadWriteProperty + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Long = longs.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Long + ) { longs[id] = value } + } + } + + /** + * Declares an `Int` config field rendered as a slider bounded by [min]/[max]. See [boolean] + * for the remaining parameter semantics. + * + * @param min Minimum value the slider allows. + * @param max Maximum value the slider allows. + * @param default Defaults to the midpoint of [min] and [max] if not given. + */ + protected fun intSlider( + title: Component, + comment: Component? = null, + min: Int, + max: Int, + default: Int = min + (max - min) / 2, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.intSlider(id, title, comment, min, max, default, resetKey) + } + types[id] = FieldType.Int + ints.putIfAbsent(id, default) + object : ReadWriteProperty + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Int = ints.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Int + ) { ints[id] = value } + } + } + + /** Declares a `Long` config field rendered as a slider. See [intSlider] for parameter semantics. */ + protected fun longSlider( + title: Component, + comment: Component? = null, + min: Long, + max: Long, + default: Long = min + (max - min) / 2, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.longSlider(id, title, comment, min, max, default, resetKey) + } + types[id] = FieldType.Long + longs.putIfAbsent(id, default) + object : ReadWriteProperty + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Long = longs.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Long + ) { longs[id] = value } + } + } + + /** Declares a `Float` config field. See [boolean] for parameter semantics. */ + protected fun float( + title: Component, + comment: Component? = null, + default: Float = 0.0f, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.float(id, title, comment, default, resetKey) + } + types[id] = FieldType.Float + floats.putIfAbsent(id, default) + object : ReadWriteProperty + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Float = floats.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Float + ) { floats[id] = value } + } + } + + /** Declares a `Double` config field. See [boolean] for parameter semantics. */ + protected fun double( + title: Component, + comment: Component? = null, + default: Double = 0.0, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.double(id, title, comment, default, resetKey) + } + types[id] = FieldType.Double + doubles.putIfAbsent(id, default) + object : ReadWriteProperty + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Double = doubles.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Double + ) { doubles[id] = value } + } + } + + /** Declares a `String` config field. See [boolean] for parameter semantics. */ + protected fun string( + title: Component, + comment: Component? = null, + default: String = "", + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.string(id, title, comment, default, resetKey) + } + types[id] = FieldType.String + strings.putIfAbsent(id, default) + object : ReadWriteProperty + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): String = strings.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: String + ) { strings[id] = value } + } + } + + /** + * Declares a field that embeds another [DataSpec] as a nested, serializable section. See + * [boolean] for the remaining parameter semantics. + * + * @param default Instance used until a stored/loaded value overrides it. Never mutated in + * place - deserialization always builds a fresh instance via [factory]. + * @param factory Creates a new instance of the nested spec; used by the deserializer so + * loading a saved value never mutates [default]. + */ + @Suppress("UNCHECKED_CAST") + protected fun spec( + title: Component, + comment: Component? = null, + default: T, + factory: () -> T, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.spec(id, title, comment, default, resetKey) + } + types[id] = FieldType.Spec { factory() } as FieldType + (specs as MutableMap).putIfAbsent(id, default) + object : ReadWriteProperty + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): T = (specs as MutableMap).getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: T + ) { specs[id] = value } + } + } + + /** + * Declares a field whose value is an entry of a vanilla [Registry], stored as the entry's + * [ResourceLocation] key. See [boolean] for the remaining parameter semantics. + * + * @param registry Registry the field's value is looked up in. + * @param subclass If given, narrows the entries offered in the UI to this runtime type; the + * stored key is still resolved against the full [registry]. + */ + protected fun registry( + title: Component, + comment: Component? = null, + default: T, + registry: Registry, + resetKey: Component? = null, + subclass: KClass? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.registry(id, title, comment, default, registry, resetKey, subclass) + } + types[id] = FieldType.Registry + registries.putIfAbsent(id, registry.getKey(default)!!) + object : ReadWriteProperty + { + @Suppress("UNCHECKED_CAST") + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): R = (registry.get(registries.getOrPut(id) { + registry.getKey(default)!! + }) ?: default) as R + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: R + ) { registries[id] = registry.getKey(value)!! } + } + } + + /** + * Declares a [CommonKeyCode] config field, rendered as a keybind picker. See [boolean] for + * the remaining parameter semantics. + */ + protected fun keycode( + title: Component, + comment: Component? = null, + default: CommonKeyCode = CommonKeyCode.unknown, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.keycode(id, title, comment, default, resetKey) + } + types[id] = FieldType.KeyCode + keycodes.putIfAbsent(id, default) + object : ReadWriteProperty + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): CommonKeyCode = keycodes.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: CommonKeyCode + ) { keycodes[id] = value } + } + } + + /** + * Declares a [Color] (ARGB) config field, rendered as a color picker. See [boolean] for the + * remaining parameter semantics. + * + * @param alpha Whether the picker allows editing the alpha channel. + */ + protected fun color( + title: Component, + comment: Component? = null, + alpha: Boolean = false, + default: Color = if (alpha) Color.ofTransparent(-1) else Color.ofOpaque(-1), + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.color(id, title, comment, alpha, default, resetKey) + } + types[id] = FieldType.Color + colors.putIfAbsent(id, default) + object : ReadWriteProperty + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Color = colors.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Color + ) { colors[id] = value } + } + } + + /** + * Declares a field whose value is one entry of the enum [kclass], rendered as a cycling + * selector over all of the enum's entries. See [boolean] for the remaining parameter + * semantics. + * + * @param kclass The enum type to select from. + */ + @Suppress("UNCHECKED_CAST") + protected fun > enumSelector( + title: Component, + comment: Component? = null, + kclass: KClass, + default: T, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.enumSelector(id, title, comment, kclass, default, resetKey) + } + types[id] = FieldType.EnumSelector(kclass) + enums.putIfAbsent(id, default) + object : ReadWriteProperty + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): T = enums.getOrPut(id) { default } as T + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: T + ) { enums[id] = value } + } + } + + /** + * Declares a field whose value is one of an arbitrary fixed set of [entries], rendered as a + * cycling selector. Unlike [enumSelector], the value type isn't required to be an `enum + * class`. See [boolean] for the remaining parameter semantics. + * + * @param kclass Runtime type of the selectable values. + * @param entries The fixed set of values the selector cycles through. + */ + @Suppress("UNCHECKED_CAST") + protected fun selector( + title: Component, + comment: Component? = null, + kclass: KClass, + default: T, + entries: Array, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.selector(id, title, comment, kclass, default, entries, resetKey) + } + types[id] = FieldType.Selector(kclass) + selectors.putIfAbsent(id, default) + object : ReadWriteProperty + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): T = selectors.getOrPut(id) { default } as T + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: T + ) { selectors[id] = value } + } + } + + /** Declares a `List` config field. See [boolean] for parameter semantics. */ + protected fun intList( + title: Component, + comment: Component? = null, + default: List = listOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.intList(id, title, comment, default, resetKey) + } + types[id] = FieldType.IntList + intLists.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): List = intLists.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: List + ) { intLists[id] = value } + } + } + + /** Declares a `List` config field. See [boolean] for parameter semantics. */ + protected fun longList( + title: Component, + comment: Component? = null, + default: List = listOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.longList(id, title, comment, default, resetKey) + } + types[id] = FieldType.LongList + longLists.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): List = longLists.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: List + ) { longLists[id] = value } + } + } + + /** Declares a `List` config field. See [boolean] for parameter semantics. */ + protected fun floatList( + title: Component, + comment: Component? = null, + default: List = listOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.floatList(id, title, comment, default, resetKey) + } + types[id] = FieldType.FloatList + floatLists.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): List = floatLists.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: List + ) { floatLists[id] = value } + } + } + + /** Declares a `List` config field. See [boolean] for parameter semantics. */ + protected fun doubleList( + title: Component, + comment: Component? = null, + default: List = listOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.doubleList(id, title, comment, default, resetKey) + } + types[id] = FieldType.DoubleList + doubleLists.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): List = doubleLists.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: List + ) { doubleLists[id] = value } + } + } + + /** Declares a `List` config field. See [boolean] for parameter semantics. */ + protected fun stringList( + title: Component, + comment: Component? = null, + default: List = listOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.stringList(id, title, comment, default, resetKey) + } + types[id] = FieldType.StringList + stringLists.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): List = stringLists.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: List + ) { stringLists[id] = value } + } + } + + /** Declares a `List` of nested [DataSpec] entries. See [spec] for parameter semantics. */ + @Suppress("UNCHECKED_CAST") + protected fun specList( + title: Component, + comment: Component? = null, + default: List = listOf(), + factory: () -> T, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.specList(id, title, comment, default, factory, resetKey) + } + types[id] = FieldType.SpecList(factory) as FieldType> + specLists.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): List = specLists.getOrPut(id) { default } as List + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: List + ) { specLists[id] = value } + } + } + + /** + * Declares a `List` of [Registry] entries, stored as a list of [ResourceLocation] keys. See + * [registry] for parameter semantics. + * + * @param factory Used to produce a fallback value if a stored key no longer resolves in + * [registry] (e.g. the entry was removed by a datapack/mod update). + */ + protected fun registryList( + title: Component, + comment: Component? = null, + default: List = listOf(), + factory: () -> T, + registry: Registry, + resetKey: Component? = null, + subclass: KClass? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.registryList(id, title, comment, default, factory, registry, resetKey, subclass) + } + types[id] = FieldType.RegistryList + registryLists.putIfAbsent(id, default.map { registry.getKey(it)!! }) + object : ReadWriteProperty> + { + @Suppress("UNCHECKED_CAST") + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): List = registryLists.getOrPut(id) { + default.map { registry.getKey(it)!! } + }.map { (registry.get(it) ?: factory()) as R } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: List + ) { registryLists[id] = value.map { registry.getKey(it)!! } } + } + } + + /** Declares a `List` config field. See [keycode] for parameter semantics. */ + protected fun keycodeList( + title: Component, + comment: Component? = null, + default: List = listOf(), + factory: () -> CommonKeyCode = CommonKeyCode.Companion::unknown, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.keycodeList(id, title, comment, default, factory, resetKey) + } + types[id] = FieldType.KeyCodeList + keycodeLists.putIfAbsent(id, default ) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): List = keycodeLists.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: List + ) { keycodeLists[id] = value } + } + } + + /** Declares a `List` config field. See [color] for parameter semantics. */ + protected fun colorList( + title: Component, + comment: Component? = null, + alpha: Boolean = false, + default: List = listOf(), + factory: () -> Color = { if (alpha) Color.ofTransparent(-1) else Color.ofOpaque(-1) }, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.colorList(id, title, comment, alpha, default, factory, resetKey) + } + types[id] = FieldType.ColorList + colorLists.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): List = colorLists.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: List + ) { colorLists[id] = value } + } + } + + /** Declares a `Map` config field. See [boolean] for parameter semantics. */ + protected fun intMap( + title: Component, + comment: Component? = null, + default: Map = mapOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.intMap(id, title, comment, default, resetKey) + } + types[id] = FieldType.IntMap + intMaps.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Map = intMaps.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Map + ) { intMaps[id] = value } + } + } + + /** Declares a `Map` config field. See [boolean] for parameter semantics. */ + protected fun longMap( + title: Component, + comment: Component? = null, + default: Map = mapOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.longMap(id, title, comment, default, resetKey) + } + types[id] = FieldType.LongMap + longMaps.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Map = longMaps.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Map + ) { longMaps[id] = value } + } + } + + /** Declares a `Map` config field. See [boolean] for parameter semantics. */ + protected fun floatMap( + title: Component, + comment: Component? = null, + default: Map = mapOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.floatMap(id, title, comment, default, resetKey) + } + types[id] = FieldType.FloatMap + floatMaps.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Map = floatMaps.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Map + ) { floatMaps[id] = value } + } + } + + /** Declares a `Map` config field. See [boolean] for parameter semantics. */ + protected fun doubleMap( + title: Component, + comment: Component? = null, + default: Map = mapOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.doubleMap(id, title, comment, default, resetKey) + } + types[id] = FieldType.DoubleMap + doubleMaps.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Map = doubleMaps.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Map + ) { doubleMaps[id] = value } + } + } + + /** Declares a `Map` config field. See [boolean] for parameter semantics. */ + protected fun stringMap( + title: Component, + comment: Component? = null, + default: Map = mapOf(), + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.stringMap(id, title, comment, default, resetKey) + } + types[id] = FieldType.StringMap + stringMaps.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Map = stringMaps.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Map + ) { stringMaps[id] = value } + } + } + + /** Declares a `Map` of nested [DataSpec] entries. See [spec] for parameter semantics. */ + @Suppress("UNCHECKED_CAST") + protected fun specMap( + title: Component, + comment: Component? = null, + default: Map = mapOf(), + factory: () -> T, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.specMap(id, title, comment, default, factory, resetKey) + } + types[id] = FieldType.SpecMap(factory) as FieldType> + (specMaps as MutableMap>).putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Map = specMaps.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Map + ) { specMaps[id] = value } + } + } + + /** + * Declares a `Map` of [Registry] entries, stored as a map of [ResourceLocation] keys. See + * [registryList] for parameter semantics. + */ + protected fun registryMap( + title: Component, + comment: Component? = null, + default: Map = mapOf(), + factory: () -> T, + registry: Registry, + resetKey: Component? = null, + subclass: KClass? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.registryMap(id, title, comment, default, factory, registry, resetKey, subclass) + } + types[id] = FieldType.RegistryMap + registryMaps.putIfAbsent(id, default.mapValues { registry.getKey(it.value)!! }) + object : ReadWriteProperty> + { + @Suppress("UNCHECKED_CAST") + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Map = registryMaps.getOrPut(id) { + default.mapValues { registry.getKey(it.value)!! } + }.mapValues { registry.get(it.value) ?: factory() } as Map + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Map + ) { registryMaps[id] = value.mapValues { registry.getKey(it.value)!! } } + } + } + + /** Declares a `Map` config field. See [keycode] for parameter semantics. */ + protected fun keycodeMap( + title: Component, + comment: Component? = null, + default: Map = mapOf(), + factory: () -> CommonKeyCode = CommonKeyCode.Companion::unknown, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.keycodeMap(id, title, comment, default, factory, resetKey) + } + types[id] = FieldType.KeyCodeMap + keycodeMaps.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Map = keycodeMaps.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Map + ) { keycodeMaps[id] = value } + } + } + + /** Declares a `Map` config field. See [color] for parameter semantics. */ + protected fun colorMap( + title: Component, + comment: Component? = null, + alpha: Boolean = false, + default: Map = mapOf(), + factory: () -> Color = { if (alpha) Color.ofTransparent(-1) else Color.ofOpaque(-1) }, + resetKey: Component? = null, + needsRestart: Boolean = false + ): PropertyDelegateProvider>> = + PropertyDelegateProvider { _, property -> + val id = property.name.toSnakeCase() + if (comment != null) + { + comments[id] = comment.string + } + onClient { + client.colorMap(id, title, comment, alpha, default, factory, resetKey) + } + types[id] = FieldType.ColorMap + colorMaps.putIfAbsent(id, default) + object : ReadWriteProperty> + { + override fun getValue( + thisRef: DataSpec, + property: KProperty<*> + ): Map = colorMaps.getOrPut(id) { default } + + override fun setValue( + thisRef: DataSpec, + property: KProperty<*>, + value: Map + ) { colorMaps[id] = value } + } + } + + /** + * Serializes/deserializes a [DataSpec] by walking its registered [types] and reading from + * or writing into the corresponding backing map (e.g. [booleans], [ints]). + */ + internal class ConfigCategorySerializer(val factory: () -> DataSpec) : + KSerializer + { + override val descriptor: SerialDescriptor by lazy { + with(factory().also { it.init() }) + { + buildClassSerialDescriptor(title.string) + { + types.forEach { (id, type) -> + element( + elementName = id, + descriptor = type.serializer.descriptor, + annotations = buildList { + if (id in comments) + { + add(TomlComment(comments[id]!!)) + add(SerialComment(comments[id]!!)) + } + } + ) + } + } + } + } + + override fun deserialize(decoder: Decoder): DataSpec + { + return decoder.decodeStructure(descriptor) + { + val spec = factory().also { it.init() } + with(spec) + { + while (true) + { + when (val index = decodeElementIndex(descriptor)) + { + in types.entries.indices -> + { + val (key, type) = types.entries.toList()[index] + when (type) + { + is FieldType.Category -> + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.Boolean -> booleans[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.Int -> ints[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.Long -> longs[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.Float -> floats[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.Double -> doubles[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.String -> strings[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.Spec -> specs[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.Registry -> registries[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.KeyCode -> keycodes[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.Color -> colors[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.EnumSelector -> enums[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.Selector -> selectors[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.IntList -> intLists[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.LongList -> longLists[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.FloatList -> floatLists[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.DoubleList -> doubleLists[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.StringList -> stringLists[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.SpecList -> specLists[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.RegistryList -> registryLists[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.KeyCodeList -> keycodeLists[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.ColorList -> colorLists[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.IntMap -> intMaps[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.LongMap -> longMaps[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.FloatMap -> floatMaps[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.DoubleMap -> doubleMaps[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.StringMap -> stringMaps[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.SpecMap -> specMaps[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.RegistryMap -> registryMaps[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.KeyCodeMap -> keycodeMaps[key] = + decodeSerializableElement(descriptor, index, type.serializer) + + is FieldType.ColorMap -> colorMaps[key] = + decodeSerializableElement(descriptor, index, type.serializer) + } + } + + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + } + spec + } + } + + override fun serialize(encoder: Encoder, value: DataSpec) + { + value.init() + encoder.encodeStructure(descriptor) + { + value.types.entries.forEachIndexed { index, (key, type) -> + @Suppress("UNCHECKED_CAST") + when (type) + { + is FieldType.Category -> encodeSerializableElement( + descriptor, + index, + type.serializer, + type.category + ) + + is FieldType.Boolean -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.booleans[key]!! + ) + + is FieldType.Int -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.ints[key]!! + ) + + is FieldType.Long -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.longs[key]!! + ) + + is FieldType.Float -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.floats[key]!! + ) + + is FieldType.Double -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.doubles[key]!! + ) + + is FieldType.String -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.strings[key]!! + ) + + is FieldType.Spec -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.specs[key]!! + ) + + is FieldType.Registry -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.registries[key]!! + ) + + is FieldType.KeyCode -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.keycodes[key]!! + ) + + is FieldType.Color -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.colors[key]!! + ) + + is FieldType.EnumSelector -> encodeSerializableElement( + descriptor, + index, + type.serializer as KSerializer>, + value.enums[key]!! + ) + + is FieldType.Selector -> encodeSerializableElement( + descriptor, + index, + type.serializer as KSerializer, + value.selectors[key]!! + ) + + is FieldType.IntList -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.intLists[key]!! + ) + + is FieldType.LongList -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.longLists[key]!! + ) + + is FieldType.FloatList -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.floatLists[key]!! + ) + + is FieldType.DoubleList -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.doubleLists[key]!! + ) + + is FieldType.StringList -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.stringLists[key]!! + ) + + is FieldType.SpecList -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.specLists[key]!! + ) + + is FieldType.RegistryList -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.registryLists[key]!! + ) + + is FieldType.KeyCodeList -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.keycodeLists[key]!! + ) + + is FieldType.ColorList -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.colorLists[key]!! + ) + + is FieldType.IntMap -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.intMaps[key]!! + ) + + is FieldType.LongMap -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.longMaps[key]!! + ) + + is FieldType.FloatMap -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.floatMaps[key]!! + ) + + is FieldType.DoubleMap -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.doubleMaps[key]!! + ) + + is FieldType.StringMap -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.stringMaps[key]!! + ) + + is FieldType.SpecMap -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.specMaps[key]!! + ) + + is FieldType.RegistryMap -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.registryMaps[key]!! + ) + + is FieldType.KeyCodeMap -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.keycodeMaps[key]!! + ) + + is FieldType.ColorMap -> encodeSerializableElement( + descriptor, + index, + type.serializer, + value.colorMaps[key]!! + ) + } + } + } + } + } + + internal val serializer by lazy { ConfigCategorySerializer { this } } +} + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/FieldType.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/FieldType.kt new file mode 100644 index 000000000..4075d0014 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/FieldType.kt @@ -0,0 +1,188 @@ +package net.kernelpanicsoft.archie.config + +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.serializer +import net.kernelpanicsoft.archie.serialization.DeferredListSerializer +import net.kernelpanicsoft.archie.serialization.DeferredMapSerializer +import net.kernelpanicsoft.archie.serialization.serializers.ColorSerializer +import net.kernelpanicsoft.archie.serialization.serializers.ResourceLocationSerializer +import net.minecraft.resources.ResourceLocation +import kotlin.reflect.KClass + +/** + * Tags a [DataSpec] field with its runtime type and the [KSerializer] used to read/write it, + * so [DataSpec.ConfigCategorySerializer] can (de)serialize each field generically without a + * `when` over the raw value type. One subtype per builder function in [DataSpec] (e.g. + * [Boolean] for `boolean()`, [IntList] for `intList()`). + */ +internal sealed class FieldType +{ + abstract val serializer: KSerializer + + data class Category(val category: DataSpec) : FieldType() + { + override val serializer: KSerializer = category.serializer + } + + data object Boolean : FieldType() + { + override val serializer: KSerializer = kotlin.Boolean.serializer() + } + + data object Int : FieldType() + { + override val serializer: KSerializer = kotlin.Int.serializer() + } + + data object Long : FieldType() + { + override val serializer: KSerializer = kotlin.Long.serializer() + } + + data object Float : FieldType() + { + override val serializer: KSerializer = kotlin.Float.serializer() + } + + data object Double : FieldType() + { + override val serializer: KSerializer = kotlin.Double.serializer() + } + + data object String : FieldType() + { + override val serializer: KSerializer = kotlin.String.serializer() + } + + data class Spec(val factory: () -> DataSpec) : FieldType() + { + override val serializer: KSerializer = + DataSpec.ConfigCategorySerializer(factory) + } + + data object Registry : FieldType() + { + override val serializer: KSerializer = ResourceLocationSerializer + } + + data object KeyCode : FieldType() + { + override val serializer: KSerializer = CommonKeyCode.serializer() + } + + data object Color : FieldType() + { + override val serializer: KSerializer = ColorSerializer + } + + data class EnumSelector>(val kClass: KClass) : FieldType() + { + @OptIn(InternalSerializationApi::class) + override val serializer: KSerializer = kClass.serializer() + } + + data class Selector(val kClass: KClass) : FieldType() + { + @OptIn(InternalSerializationApi::class) + override val serializer: KSerializer = kClass.serializer() + } + + data object IntList : FieldType>() + { + override val serializer: KSerializer> = ListSerializer(kotlin.Int.serializer()) + } + + data object LongList : FieldType>() + { + override val serializer: KSerializer> = ListSerializer(kotlin.Long.serializer()) + } + + data object FloatList : FieldType>() + { + override val serializer: KSerializer> = ListSerializer(kotlin.Float.serializer()) + } + + data object DoubleList : FieldType>() + { + override val serializer: KSerializer> = ListSerializer(kotlin.Double.serializer()) + } + + data object StringList : FieldType>() + { + override val serializer: KSerializer> = ListSerializer(kotlin.String.serializer()) + } + + data class SpecList(val factory: () -> DataSpec) : FieldType>() + { + override val serializer: KSerializer> = DeferredListSerializer( + DataSpec.ConfigCategorySerializer(factory) + ) + } + + data object RegistryList : FieldType>() + { + override val serializer: KSerializer> = ListSerializer(ResourceLocationSerializer) + } + + data object KeyCodeList : FieldType>() + { + override val serializer: KSerializer> = ListSerializer( + CommonKeyCode.serializer()) + } + + data object ColorList : FieldType>() + { + override val serializer: KSerializer> = ListSerializer(ColorSerializer) + } + + data object IntMap : FieldType>() + { + override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), kotlin.Int.serializer()) + } + + data object LongMap : FieldType>() + { + override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), kotlin.Long.serializer()) + } + + data object FloatMap : FieldType>() + { + override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), kotlin.Float.serializer()) + } + + data object DoubleMap : FieldType>() + { + override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), kotlin.Double.serializer()) + } + + data object StringMap : FieldType>() + { + override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), kotlin.String.serializer()) + } + + data class SpecMap(val factory: () -> DataSpec) : FieldType>() + { + override val serializer: KSerializer> = DeferredMapSerializer(kotlin.String.serializer(), + DataSpec.ConfigCategorySerializer(factory) + ) + } + + data object RegistryMap : FieldType>() + { + override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), ResourceLocationSerializer) + } + + data object KeyCodeMap : FieldType>() + { + override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), CommonKeyCode.serializer()) + } + + data object ColorMap : FieldType>() + { + override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), ColorSerializer) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/IConfigSerializer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/IConfigSerializer.kt new file mode 100644 index 000000000..9edf90a8f --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/IConfigSerializer.kt @@ -0,0 +1,77 @@ +package net.kernelpanicsoft.archie.config + +import dev.architectury.platform.Platform +import net.kernelpanicsoft.archie.Archie +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.StandardCopyOption + +/** + * Reads and writes a [ConfigSpec] to/from a specific file format (JSON, JSON5, TOML, ...). Built-in + * implementations live in `net.kernelpanicsoft.archie.config.serializer`; [ConfigSpec.fileSerializer] + * picks one per-platform by default. + * + * [configPath], [load], and [save] all take a [configFolder] that defaults to the platform's shared + * config folder ([Platform.getConfigFolder]); [ConfigSpec] passes its own [ConfigSpec.configFolder] + * instead, which a [ConfigSpec.Server] repoints at the current world's per-save `serverconfig/` folder. + */ +interface IConfigSerializer +{ + /** File the given [config] is read from and written to, resolved under [configFolder]. */ + fun configPath(config: ConfigSpec, configFolder: Path = Platform.getConfigFolder()): Path + /** + * Reads [config]'s file if present via [loadString], then always [save]s it back out - this + * both formats a freshly-created file with defaults and rewrites an existing one with any + * newly-added fields. If the existing file fails to parse, it's logged and renamed to + * `.corrupted` rather than deleted, and loading falls through to writing fresh defaults + * so startup isn't blocked. + */ + fun load(config: ConfigSpec, configFolder: Path = Platform.getConfigFolder()) + { + val path = configPath(config, configFolder) + if (Files.exists(path)) + { + try + { + val string = Files.readString(path) + loadString(config, string) + } + catch (e: Throwable) + { + // A malformed/corrupt file must not permanently block startup. Back the bad + // file up rather than deleting it, log it, and fall through to save(config) + // below so a fresh default file gets written and the mod still loads. + Archie.LOGGER.error("Failed to load config at $path, resetting to defaults. The invalid file was backed up.", e) + runCatching { + Files.move(path, path.resolveSibling("${path.fileName}.corrupted"), StandardCopyOption.REPLACE_EXISTING) + } + } + } + + save(config, configFolder) + } + + /** Parses [string] and populates [config]'s fields from it. Implemented per-format. */ + fun loadString(config: ConfigSpec, string: String) + + /** Writes [config]'s current field values to [configPath], creating parent directories as needed. */ + fun save(config: ConfigSpec, configFolder: Path = Platform.getConfigFolder()) + { + val path = configPath(config, configFolder) + try + { + Files.createDirectories(path.parent) + Files.writeString(path, saveString(config)) + } + catch (e: Throwable) + { + throw SerializationException(e) + } + } + + /** Renders [config]'s current field values as a file-format string. Implemented per-format. */ + fun saveString(config: ConfigSpec): String + + /** Thrown when writing a config file fails (e.g. an I/O error). */ + class SerializationException(cause: Throwable) : Exception(cause) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorListBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorListBuilder.kt new file mode 100644 index 000000000..1ddd916e2 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorListBuilder.kt @@ -0,0 +1,38 @@ +package net.kernelpanicsoft.archie.config.builder + +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.ColorEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.impl.builders.FieldBuilder +import me.shedaniel.math.Color +import net.minecraft.network.chat.Component + +/** [ListFieldBuilder] for [Color] values, using Cloth Config's `startColorField` per row. Set [alphaMode] to allow editing alpha. */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +class ColorListBuilder( + resetButtonKey: Component, + fieldNameKey: Component, + value: List, + private val factory: () -> Color +) : + ListFieldBuilder( + resetButtonKey, + fieldNameKey, + value.map { it.color } + ) +{ + var alphaMode: Boolean = false + + override fun factory(): Int = factory.invoke().color + + override fun ConfigEntryBuilder.builder( + title: Component, + value: Int, + list: NestedListListEntry + ): FieldBuilder + { + return startColorField(title, value) + .setAlphaMode(alphaMode) + } + +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorMapBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorMapBuilder.kt new file mode 100644 index 000000000..62a03900d --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorMapBuilder.kt @@ -0,0 +1,40 @@ +package net.kernelpanicsoft.archie.config.builder + +import net.kernelpanicsoft.archie.util.MutableEntry +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.ColorEntry +import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.impl.builders.FieldBuilder +import me.shedaniel.math.Color +import net.minecraft.network.chat.Component + +/** [MapFieldBuilder] for [Color] values, using Cloth Config's `startColorField` per row. Set [alphaMode] to allow editing alpha. */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +class ColorMapBuilder( + resetButtonKey: Component, + fieldNameKey: Component, + value: Map, + private val factory: () -> Color +) : + MapFieldBuilder( + resetButtonKey, + fieldNameKey, + value.mapValues { it.value.color } + ) +{ + var alphaMode: Boolean = false + + override fun valueFactory(): Int = factory.invoke().color + + override fun ConfigEntryBuilder.valueBuilder( + title: Component, + value: Int, + list: NestedListListEntry, MultiElementListEntry>> + ): FieldBuilder + { + return startColorField(title, value) + .setAlphaMode(alphaMode) + } + +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ConfigFieldBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ConfigFieldBuilder.kt new file mode 100644 index 000000000..85d8d13d9 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ConfigFieldBuilder.kt @@ -0,0 +1,40 @@ +package net.kernelpanicsoft.archie.config.builder + +import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder +import net.kernelpanicsoft.archie.config.ConfigSpec +import net.kernelpanicsoft.archie.config.entry.ConfigSpecEntry +import net.minecraft.network.chat.Component +import java.util.* +import kotlin.jvm.optionals.getOrNull + +/** + * Cloth Config field builder producing a [ConfigSpecEntry] - a single "Edit" button field that + * navigates into [value]'s own config screen. Built via `ConfigEntryBuilder.startConfigField`, + * used by [net.kernelpanicsoft.archie.config.ClientConfigContainer] to let a container screen with + * multiple [ConfigSpec]s drill down into each one. + */ +class ConfigFieldBuilder( + resetButtonKey: Component, + fieldNameKey: Component, + private val value: T +) : AbstractFieldBuilder, ConfigFieldBuilder>( + resetButtonKey, fieldNameKey +) +{ + var buttonText: Component = Component.literal("Edit") + var requiresRestart: Boolean = false + + override fun build(): ConfigSpecEntry + { + val entry = ConfigSpecEntry( + fieldNameKey, + buttonText, + value, + requiresRestart + ) + entry.setErrorSupplier { + Optional.ofNullable(errorSupplier?.apply(entry.value)?.getOrNull()) + } + return finishBuilding(entry) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DoubleMapBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DoubleMapBuilder.kt new file mode 100644 index 000000000..5a923c57d --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DoubleMapBuilder.kt @@ -0,0 +1,26 @@ +package net.kernelpanicsoft.archie.config.builder + +import net.kernelpanicsoft.archie.util.MutableEntry +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.DoubleListEntry +import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder +import net.minecraft.network.chat.Component + +/** [MapFieldBuilder] for `Double` values, using Cloth Config's `startDoubleField` per row. */ +class DoubleMapBuilder(resetButtonKey: Component, fieldNameKey: Component, value: Map) : MapFieldBuilder( + resetButtonKey, fieldNameKey, value +) +{ + override fun valueFactory(): Double = 0.0 + + override fun ConfigEntryBuilder.valueBuilder( + title: Component, + value: Double, + list: NestedListListEntry, MultiElementListEntry>> + ): AbstractFieldBuilder + { + return startDoubleField(title, value) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DropdownFieldBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DropdownFieldBuilder.kt new file mode 100644 index 000000000..9f2614d5a --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DropdownFieldBuilder.kt @@ -0,0 +1,46 @@ +package net.kernelpanicsoft.archie.config.builder + +import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry +import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder +import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder +import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder.CellCreatorBuilder +import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder.TopCellElementBuilder +import net.minecraft.network.chat.Component + +/** + * Cloth Config builder for a dropdown/autocomplete field over [selections]. Set [toObjectFunction] + * to parse free-typed text back into a `T` (required when [suggestionMode] is enabled); override + * [toTextFunction] to customize how values are displayed. + */ +open class DropdownFieldBuilder( + resetButtonKey: Component, + fieldNameKey: Component, + private val value: T, + open var selections: Iterable = emptyList() +) : AbstractFieldBuilder, DropdownFieldBuilder>( + resetButtonKey, fieldNameKey +) +{ + open lateinit var toObjectFunction: (String) -> T + open var toTextFunction: (T) -> Component = { Component.literal(it.toString()) } + open var suggestionMode: Boolean = true + + override fun build(): DropdownBoxEntry + { + val entry = DropdownMenuBuilder( + resetButtonKey, + fieldNameKey, + TopCellElementBuilder.of(value, toObjectFunction, toTextFunction), + CellCreatorBuilder.of(toTextFunction) + ) + entry.setSuggestionMode(suggestionMode) + entry.setSelections(selections) + + entry.setSaveConsumer(saveConsumer) + entry.setErrorSupplier(errorSupplier) + entry.setTooltipSupplier(tooltipSupplier) + entry.setDefaultValue(defaultValue) + entry.requireRestart(requireRestart) + return finishBuilding(entry.build()) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/FloatMapBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/FloatMapBuilder.kt new file mode 100644 index 000000000..c7e217dda --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/FloatMapBuilder.kt @@ -0,0 +1,26 @@ +package net.kernelpanicsoft.archie.config.builder + +import net.kernelpanicsoft.archie.util.MutableEntry +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.FloatListEntry +import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder +import net.minecraft.network.chat.Component + +/** [MapFieldBuilder] for `Float` values, using Cloth Config's `startFloatField` per row. */ +class FloatMapBuilder(resetButtonKey: Component, fieldNameKey: Component, value: Map) : MapFieldBuilder( + resetButtonKey, fieldNameKey, value +) +{ + override fun valueFactory(): Float = 0.0f + + override fun ConfigEntryBuilder.valueBuilder( + title: Component, + value: Float, + list: NestedListListEntry, MultiElementListEntry>> + ): AbstractFieldBuilder + { + return startFloatField(title, value) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/IntegerMapBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/IntegerMapBuilder.kt new file mode 100644 index 000000000..c27eed956 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/IntegerMapBuilder.kt @@ -0,0 +1,26 @@ +package net.kernelpanicsoft.archie.config.builder + +import net.kernelpanicsoft.archie.util.MutableEntry +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.IntegerListEntry +import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder +import net.minecraft.network.chat.Component + +/** [MapFieldBuilder] for `Int` values, using Cloth Config's `startIntField` per row. */ +class IntegerMapBuilder(resetButtonKey: Component, fieldNameKey: Component, value: Map) : MapFieldBuilder( + resetButtonKey, fieldNameKey, value +) +{ + override fun valueFactory(): Int = 0 + + override fun ConfigEntryBuilder.valueBuilder( + title: Component, + value: Int, + list: NestedListListEntry, MultiElementListEntry>> + ): AbstractFieldBuilder + { + return startIntField(title, value) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeListBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeListBuilder.kt new file mode 100644 index 000000000..e1c8657f1 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeListBuilder.kt @@ -0,0 +1,61 @@ +package net.kernelpanicsoft.archie.config.builder + +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.api.Modifier +import me.shedaniel.clothconfig2.api.ModifierKeyCode +import me.shedaniel.clothconfig2.gui.entries.KeyCodeEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.impl.builders.FieldBuilder +import me.shedaniel.clothconfig2.impl.builders.KeyCodeBuilder +import net.minecraft.network.chat.Component + +/** + * [ListFieldBuilder] for [ModifierKeyCode] values, using Cloth Config's `startModifierKeyCodeField` + * per row. [allowKey] and [allowMouse] can't both be `false` - at least one input source must + * remain selectable. [allowModifiers] toggles whether Ctrl/Shift/Alt can be bound alongside the key. + */ +class KeycodeListBuilder( + resetButtonKey: Component, + fieldNameKey: Component, + value: List, + private val factory: () -> ModifierKeyCode +) : + ListFieldBuilder( + resetButtonKey, + fieldNameKey, + value + ) +{ + var allowModifiers: Boolean = true + private var _allowKey: Boolean = true + var allowKey: Boolean + get() = _allowKey + set(allowKey) + { + require(!(!this.allowMouse && !allowKey)) + _allowKey = allowKey + } + private var _allowMouse: Boolean = true + var allowMouse: Boolean + get() = _allowMouse + set(allowMouse) + { + require(!(!this.allowKey && !allowMouse)) + _allowMouse = allowMouse + } + + override fun factory(): ModifierKeyCode = factory.invoke() + + override fun ConfigEntryBuilder.builder( + title: Component, + value: ModifierKeyCode, + list: NestedListListEntry + ): FieldBuilder + { + return startModifierKeyCodeField(title, value) + .setAllowModifiers(allowModifiers) + .setAllowKey(allowKey) + .setAllowMouse(allowMouse) + } + +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeMapBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeMapBuilder.kt new file mode 100644 index 000000000..4f9c97ddc --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeMapBuilder.kt @@ -0,0 +1,56 @@ +package net.kernelpanicsoft.archie.config.builder + +import net.kernelpanicsoft.archie.util.MutableEntry +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.api.ModifierKeyCode +import me.shedaniel.clothconfig2.gui.entries.KeyCodeEntry +import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.impl.builders.FieldBuilder +import net.minecraft.network.chat.Component + +/** + * [MapFieldBuilder] for [ModifierKeyCode] values, using Cloth Config's `startModifierKeyCodeField` + * per row. See [KeycodeListBuilder] for the `allowKey`/`allowMouse`/`allowModifiers` constraints. + */ +class KeycodeMapBuilder( + resetButtonKey: Component, + fieldNameKey: Component, + value: Map, + private val factory: () -> ModifierKeyCode +) : MapFieldBuilder( + resetButtonKey, fieldNameKey, value +) +{ + private var allowModifiers: Boolean = true + private var _allowKey: Boolean = true + private var allowKey: Boolean + get() = _allowKey + set(allowKey) + { + require(!(!this.allowMouse && !allowKey)) + _allowKey = allowKey + } + private var _allowMouse: Boolean = true + private var allowMouse: Boolean + get() = _allowMouse + set(allowMouse) + { + require(!(!this.allowKey && !allowMouse)) + _allowMouse = allowMouse + } + + override fun valueFactory(): ModifierKeyCode = factory.invoke() + + override fun ConfigEntryBuilder.valueBuilder( + title: Component, + value: ModifierKeyCode, + list: NestedListListEntry, MultiElementListEntry>> + ): FieldBuilder + { + return startModifierKeyCodeField(title, value) + .setAllowModifiers(allowModifiers) + .setAllowKey(allowKey) + .setAllowMouse(allowMouse) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ListFieldBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ListFieldBuilder.kt new file mode 100644 index 000000000..ae58cae49 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ListFieldBuilder.kt @@ -0,0 +1,105 @@ +package net.kernelpanicsoft.archie.config.builder + +import me.shedaniel.clothconfig2.api.AbstractConfigListEntry +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.api.ModifierKeyCode +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder +import me.shedaniel.clothconfig2.impl.builders.AbstractListBuilder +import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder +import me.shedaniel.clothconfig2.impl.builders.FieldBuilder +import me.shedaniel.clothconfig2.impl.builders.KeyCodeBuilder +import net.minecraft.network.chat.Component +import java.util.* +import java.util.function.Supplier +import kotlin.jvm.optionals.getOrNull + +/** + * Base Cloth Config builder for a list field, rendered as an editable list of rows sharing one + * element type. Concrete subclasses (e.g. [KeycodeListBuilder]) only need to implement [factory] + * (the default for a newly-inserted row) and [builder] (the field builder for each row); this + * class handles add/remove wiring and error/tooltip propagation. + */ +abstract class ListFieldBuilder, SELF : ListFieldBuilder>( + resetButtonKey: Component, + fieldNameKey: Component, + value: List +) : AbstractListBuilder, SELF>( + resetButtonKey, fieldNameKey +) +{ + init + { + this.value = value + } + + /** Value assigned to a row inserted via the UI's "add" button. */ + abstract fun factory(): T + + /** Builds the Cloth Config field for a single row. */ + abstract fun ConfigEntryBuilder.builder(title: Component, value: T, list: NestedListListEntry): FieldBuilder + + override fun build(): NestedListListEntry + { + val entryBuilder: ConfigEntryBuilder = ConfigEntryBuilder.create() + @Suppress("UnstableApiUsage") + val entry = NestedListListEntry( + fieldNameKey, + value, + isExpanded, + null, + saveConsumer, + defaultValue, + resetButtonKey, + isDeleteButtonEnabled, + isInsertInFront + ) { entryNullable: T?, list: NestedListListEntry -> + val entry = entryNullable ?: factory() + entryBuilder.builder(Component.literal("Entry"), entry, list).apply { + when (this) + { + is AbstractFieldBuilder -> + { + setErrorSupplier { cellValue -> + Optional.ofNullable(cellErrorSupplier?.apply(cellValue)?.getOrNull()) + } + setDefaultValue { + factory() + } + } + + is DropdownMenuBuilder -> + { + setErrorSupplier { cellValue -> + Optional.ofNullable(cellErrorSupplier?.apply(cellValue)?.getOrNull()) + } + setDefaultValue { + factory() + } + } + + is KeyCodeBuilder -> + { + setModifierErrorSupplier { cellValue -> + @Suppress("UNCHECKED_CAST") + Optional.ofNullable(cellErrorSupplier?.apply(cellValue as T)?.getOrNull()) + } + setModifierDefaultValue { + factory() as ModifierKeyCode + } + } + } + requireRestart(this@ListFieldBuilder.isRequireRestart) + setRequirement(this@ListFieldBuilder.enableRequirement) + setDisplayRequirement(this@ListFieldBuilder.displayRequirement) + }.build() + } + entry.setTooltipSupplier { + tooltipSupplier.apply(entry.value) + } + entry.setErrorSupplier { + Optional.ofNullable(errorSupplier?.apply(entry.value)?.getOrNull()) + } + return entry + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/LongMapBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/LongMapBuilder.kt new file mode 100644 index 000000000..738007a3a --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/LongMapBuilder.kt @@ -0,0 +1,26 @@ +package net.kernelpanicsoft.archie.config.builder + +import net.kernelpanicsoft.archie.util.MutableEntry +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.LongListEntry +import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder +import net.minecraft.network.chat.Component + +/** [MapFieldBuilder] for `Long` values, using Cloth Config's `startLongField` per row. */ +class LongMapBuilder(resetButtonKey: Component, fieldNameKey: Component, value: Map) : MapFieldBuilder( + resetButtonKey, fieldNameKey, value +) +{ + override fun valueFactory(): Long = 0 + + override fun ConfigEntryBuilder.valueBuilder( + title: Component, + value: Long, + list: NestedListListEntry, MultiElementListEntry>> + ): AbstractFieldBuilder + { + return startLongField(title, value) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/MapFieldBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/MapFieldBuilder.kt new file mode 100644 index 000000000..bc9f9428c --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/MapFieldBuilder.kt @@ -0,0 +1,181 @@ +package net.kernelpanicsoft.archie.config.builder + +import net.kernelpanicsoft.archie.util.MutableEntry +import net.kernelpanicsoft.archie.util.toMutableEntry +import me.shedaniel.clothconfig2.api.AbstractConfigListEntry +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.api.ModifierKeyCode +import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.gui.entries.StringListEntry +import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder +import me.shedaniel.clothconfig2.impl.builders.AbstractListBuilder +import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder +import me.shedaniel.clothconfig2.impl.builders.FieldBuilder +import me.shedaniel.clothconfig2.impl.builders.KeyCodeBuilder +import net.minecraft.network.chat.Component +import java.util.* +import java.util.function.Consumer +import java.util.function.Function +import java.util.function.Supplier +import kotlin.jvm.optionals.getOrNull + +/** + * Base Cloth Config builder for a `String`-keyed map field, rendered as a nested list of + * editable key/value rows. Concrete subclasses (e.g. [IntegerMapBuilder]) only need to implement + * [valueFactory] (the default for a newly-inserted row) and [valueBuilder] (the field builder for + * the value column); this class handles the key column, duplicate-key validation, and row + * add/remove wiring. + */ +abstract class MapFieldBuilder, SELF : MapFieldBuilder>( + resetButtonKey: Component, + fieldNameKey: Component, + value: Map +) : + AbstractListBuilder, NestedListListEntry, MultiElementListEntry>>, SELF>( + resetButtonKey, + fieldNameKey + ) +{ + + open var keyErrorSupplier: ((String) -> Optional)? = null + open var valueErrorSupplier: ((T) -> Optional)? = null + open var valueTooltipSupplier: ((T) -> Optional>)? = null + + init + { + this.value = value.entries.toList().map(Map.Entry::toMutableEntry) + } + + /** Value assigned to a row inserted via the UI's "add" button. */ + abstract fun valueFactory(): T + + /** Builds the Cloth Config field for a row's value column. */ + abstract fun ConfigEntryBuilder.valueBuilder(title: Component, value: T, list: NestedListListEntry, MultiElementListEntry>>): FieldBuilder + + override fun build(): NestedListListEntry, MultiElementListEntry>> + { + val entryBuilder: ConfigEntryBuilder = ConfigEntryBuilder.create() + + val fields: MutableList = mutableListOf() + + @Suppress("UnstableApiUsage") + val entry = NestedListListEntry( + fieldNameKey, + value, + isExpanded, + null, + saveConsumer, + defaultValue, + resetButtonKey, + isDeleteButtonEnabled, + isInsertInFront + ) { entryNullable: MutableEntry?, list: NestedListListEntry, MultiElementListEntry>> -> + val entry: MutableEntry = entryNullable ?: ("" to valueFactory()).toMutableEntry() + val cell = MultiElementListEntry( + Component.literal("Entry"), + entry, + buildList { + add(entryBuilder.startStrField(Component.literal("Key"), entry.key).apply { + saveConsumer = Consumer { key -> + entryNullable?.key = key + entry.key = key + } + setErrorSupplier { entryKey -> + Optional.ofNullable(keyErrorSupplier?.invoke(entryKey)?.getOrNull()).or { + if (fields.count { + it.value == entryKey + } > 1) + Optional.of(Component.literal("Duplicate Key: $entryKey")) + else + Optional.empty() + } + } + requireRestart(this@MapFieldBuilder.requireRestart) + setRequirement(this@MapFieldBuilder.enableRequirement) + setDisplayRequirement(this@MapFieldBuilder.displayRequirement) + }.build().also { + fields.add(it) + }) + + add(entryBuilder.valueBuilder(Component.literal("Value"), entry.value, list).apply { + when (this) + { + is AbstractFieldBuilder -> + { + setSaveConsumer { value -> + entryNullable?.value = value + entry.value = value + } + setErrorSupplier { entryValue -> + Optional.ofNullable(valueErrorSupplier?.invoke(entryValue)?.getOrNull()) + } + setTooltipSupplier { entryValue -> + Optional.ofNullable(valueTooltipSupplier?.invoke(entryValue)?.getOrNull()) + } + setDefaultValue { + valueFactory() + } + } + + is DropdownMenuBuilder -> + { + setSaveConsumer { value -> + entryNullable?.value = value + entry.value = value + } + setErrorSupplier { entryValue -> + Optional.ofNullable(valueErrorSupplier?.invoke(entryValue)?.getOrNull()) + } + setTooltipSupplier { entryValue -> + Optional.ofNullable(valueTooltipSupplier?.invoke(entryValue)?.getOrNull()) + } + setDefaultValue { + valueFactory() + } + } + + is KeyCodeBuilder -> + { + setModifierSaveConsumer { value -> + @Suppress("UNCHECKED_CAST") + entryNullable?.value = value as T + @Suppress("UNCHECKED_CAST") + entry.value = value as T + } + setModifierErrorSupplier { entryValue -> + @Suppress("UNCHECKED_CAST") + Optional.ofNullable(valueErrorSupplier?.invoke(entryValue as T)?.getOrNull()) + } + setModifierTooltipSupplier { entryValue -> + @Suppress("UNCHECKED_CAST") + Optional.ofNullable(valueTooltipSupplier?.invoke(entryValue as T)?.getOrNull()) + } + setModifierDefaultValue { + valueFactory() as ModifierKeyCode + } + } + } + requireRestart(this@MapFieldBuilder.requireRestart) + setRequirement(this@MapFieldBuilder.enableRequirement) + setDisplayRequirement(this@MapFieldBuilder.displayRequirement) + }.build()) + }, + list.isExpanded + ) + cell.setErrorSupplier { + Optional.ofNullable(cellErrorSupplier?.apply(cell.value)?.getOrNull()) + } + cell + } + entry.setTooltipSupplier { + tooltipSupplier.apply(entry.value) + } + entry.setErrorSupplier { + Optional.ofNullable(errorSupplier?.apply(entry.value)?.getOrNull()) + } + return finishBuilding(entry) + } + + +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryFieldBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryFieldBuilder.kt new file mode 100644 index 000000000..d19b088dc --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryFieldBuilder.kt @@ -0,0 +1,56 @@ +package net.kernelpanicsoft.archie.config.builder + +import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry +import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.core.Registry +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.entity.BlockEntityType +import net.kernelpanicsoft.archie.config.builder.ofBlockEntityTypeObject +import java.lang.reflect.Field +import kotlin.reflect.KClass + +/** + * Cloth Config builder for a single [registry] entry, rendered as a dropdown over every entry + * (optionally filtered to instances of [subclass]) sorted by registry name. Recognized entry + * types (`Item`, `Block`, `BlockEntityType`) get an icon in their dropdown cell; anything else + * falls back to a plain text cell. + */ +@Suppress("UNCHECKED_CAST") +class RegistryFieldBuilder( + resetButtonKey: Component, + fieldNameKey: Component, + subclass: KClass? = null, + registry: Registry, + value: T +) : DropdownMenuBuilder(resetButtonKey, fieldNameKey, TopCellElementBuilder.of(value, { + registry.getOptional( + ResourceLocation.parse(it) + ).orElse(null) +}, { + Component.literal(registry.getKey(it).toString()) +}), when (value) +{ + is Item -> CellCreatorBuilder.ofItemObject() as DropdownBoxEntry.SelectionCellCreator + is Block -> CellCreatorBuilder.ofBlockObject() as DropdownBoxEntry.SelectionCellCreator + is BlockEntityType<*> -> ofBlockEntityTypeObject() as DropdownBoxEntry.SelectionCellCreator + else -> CellCreatorBuilder.of(20, 146, 7) { + Component.literal(registry.getKey(it).toString()) + } +}) +{ + init + { + selections = ( + if (subclass != null) registry.filterIsInstance(subclass.java) + else registry + ).sortedBy { + registry.getKey(it).toString() + }.toSet() + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryListBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryListBuilder.kt new file mode 100644 index 000000000..79ffadbbd --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryListBuilder.kt @@ -0,0 +1,34 @@ +package net.kernelpanicsoft.archie.config.builder + +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.impl.builders.FieldBuilder +import net.minecraft.core.Registry +import net.minecraft.network.chat.Component +import kotlin.reflect.KClass + +/** [ListFieldBuilder] for [registry] entries, using [RegistryFieldBuilder] per row. */ +class RegistryListBuilder( + resetButtonKey: Component, + fieldNameKey: Component, + value: List, + private val factory: () -> T, + private val subclass: KClass? = null, + private val registry: Registry +) : + ListFieldBuilder, RegistryListBuilder>( + resetButtonKey, fieldNameKey, value + ) +{ + override fun factory(): T = this.factory.invoke() + + override fun ConfigEntryBuilder.builder( + title: Component, + value: T, + list: NestedListListEntry> + ): FieldBuilder, *> + { + return startRegistryField(title, value, subclass, registry) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryMapBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryMapBuilder.kt new file mode 100644 index 000000000..9644a2331 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryMapBuilder.kt @@ -0,0 +1,36 @@ +package net.kernelpanicsoft.archie.config.builder + +import net.kernelpanicsoft.archie.util.MutableEntry +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry +import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.impl.builders.FieldBuilder +import net.minecraft.core.Registry +import net.minecraft.network.chat.Component +import kotlin.reflect.KClass + +/** [MapFieldBuilder] for [registry] entries, using [RegistryFieldBuilder] per row. */ +class RegistryMapBuilder( + resetButtonKey: Component, + fieldNameKey: Component, + value: Map, + private val factory: () -> T, + private val subclass: KClass? = null, + private val registry: Registry +) : + MapFieldBuilder, RegistryMapBuilder>( + resetButtonKey, fieldNameKey, value + ) +{ + override fun valueFactory(): T = this.factory.invoke() + + override fun ConfigEntryBuilder.valueBuilder( + title: Component, + value: T, + list: NestedListListEntry, MultiElementListEntry>> + ): FieldBuilder, *> + { + return startRegistryField(title, value, subclass, registry) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecFieldBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecFieldBuilder.kt new file mode 100644 index 000000000..be961f31f --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecFieldBuilder.kt @@ -0,0 +1,56 @@ +package net.kernelpanicsoft.archie.config.builder + +import net.kernelpanicsoft.archie.config.DataSpec +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry +import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder +import net.minecraft.network.chat.Component +import java.util.* +import java.util.function.Supplier +import kotlin.jvm.optionals.getOrNull + +/** + * Cloth Config builder for a nested [DataSpec] field, rendered as a collapsible group + * containing [value]'s own fields and subcategories (via [DataSpec.client]). + */ +class SpecFieldBuilder( + resetButtonKey: Component, + fieldNameKey: Component, + value: T +) : AbstractFieldBuilder, SpecFieldBuilder>( + resetButtonKey, fieldNameKey +) +{ + /** Whether the group starts expanded in the UI. */ + var isExpanded: Boolean = false + init + { + this.value = value + } + @Suppress("UnstableApiUsage") + override fun build(): MultiElementListEntry + { + val entryBuilder: ConfigEntryBuilder = ConfigEntryBuilder.create() + val entry = MultiElementListEntry( + fieldNameKey, + value, + buildList { + value.client.builders.forEach { builder -> + add(entryBuilder.builder()) + } + +// value.subcategories.forEach { cat -> +// add(cat.client.buildSub(entryBuilder)) +// } + }, + isExpanded + ) + entry.tooltipSupplier = Supplier { + tooltipSupplier.apply(entry.value) + } + entry.setErrorSupplier { + Optional.ofNullable(errorSupplier?.apply(entry.value)?.getOrNull()) + } + return finishBuilding(entry) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecListBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecListBuilder.kt new file mode 100644 index 000000000..92ff2865f --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecListBuilder.kt @@ -0,0 +1,30 @@ +package net.kernelpanicsoft.archie.config.builder + +import net.kernelpanicsoft.archie.config.DataSpec +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder +import net.minecraft.network.chat.Component + +/** [ListFieldBuilder] for nested [DataSpec] entries, using [SpecFieldBuilder] per row. */ +class SpecListBuilder( + resetButtonKey: Component, + fieldNameKey: Component, + value: List, + private val factory: () -> T +) : ListFieldBuilder, SpecListBuilder>( + resetButtonKey, fieldNameKey, value +) +{ + override fun factory(): T = this.factory.invoke() + + override fun ConfigEntryBuilder.builder( + title: Component, + value: T, + list: NestedListListEntry> + ): AbstractFieldBuilder, *> + { + return startSpecField(title, value).also { it.isExpanded = list.isExpanded } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecMapBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecMapBuilder.kt new file mode 100644 index 000000000..f9e5e0b8c --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecMapBuilder.kt @@ -0,0 +1,31 @@ +package net.kernelpanicsoft.archie.config.builder + +import net.kernelpanicsoft.archie.config.DataSpec +import net.kernelpanicsoft.archie.util.MutableEntry +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder +import net.minecraft.network.chat.Component + +/** [MapFieldBuilder] for nested [DataSpec] entries, using [SpecFieldBuilder] per row. */ +class SpecMapBuilder( + resetButtonKey: Component, + fieldNameKey: Component, + value: Map, + private val valueFactory: () -> T +) : MapFieldBuilder, SpecMapBuilder>( + resetButtonKey, fieldNameKey, value +) +{ + override fun valueFactory(): T = this.valueFactory.invoke() + + override fun ConfigEntryBuilder.valueBuilder( + title: Component, + value: T, + list: NestedListListEntry, MultiElementListEntry>> + ): AbstractFieldBuilder, *> + { + return startSpecField(title, value).also { it.isExpanded = list.isExpanded } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/StringMapBuilder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/StringMapBuilder.kt new file mode 100644 index 000000000..ebe9faf9c --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/StringMapBuilder.kt @@ -0,0 +1,26 @@ +package net.kernelpanicsoft.archie.config.builder + +import net.kernelpanicsoft.archie.util.MutableEntry +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry +import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry +import me.shedaniel.clothconfig2.gui.entries.StringListEntry +import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder +import net.minecraft.network.chat.Component + +/** [MapFieldBuilder] for `String` values, using Cloth Config's `startStrField` per row. */ +class StringMapBuilder(resetButtonKey: Component, fieldNameKey: Component, value: Map) : MapFieldBuilder( + resetButtonKey, fieldNameKey, value +) +{ + override fun valueFactory(): String = "" + + override fun ConfigEntryBuilder.valueBuilder( + title: Component, + value: String, + list: NestedListListEntry, MultiElementListEntry>> + ): AbstractFieldBuilder + { + return startStrField(title, value) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/extensions.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/extensions.kt new file mode 100644 index 000000000..3143d6df6 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/extensions.kt @@ -0,0 +1,259 @@ +package net.kernelpanicsoft.archie.config.builder + +import net.kernelpanicsoft.archie.config.DataSpec +import net.kernelpanicsoft.archie.util.getReflection +import net.kernelpanicsoft.archie.util.setReflection +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.api.ModifierKeyCode +import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry +import me.shedaniel.clothconfig2.impl.builders.ColorFieldBuilder +import me.shedaniel.math.Color +import net.kernelpanicsoft.archie.config.ConfigSpec +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.core.Registry +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.network.chat.Component +import net.minecraft.world.item.ItemStack +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.entity.BlockEntityType +import java.lang.reflect.Field +import kotlin.reflect.KClass + +/** + * A [DropdownBoxEntry] cell creator that renders each [BlockEntityType] option as its owning + * block's item icon plus registry name, for use with a dropdown field over block entity types. + */ +fun ofBlockEntityTypeObject(): DropdownBoxEntry.SelectionCellCreator> +{ + return object : DropdownBoxEntry.DefaultSelectionCellCreator>({ + Component.literal(BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(it).toString()) + }) + { + @Suppress("UNCHECKED_CAST") + override fun create(selection: BlockEntityType<*>): DropdownBoxEntry.SelectionCellElement> + { + val blocksField: Field = BlockEntityType::class.java.getDeclaredField("validBlocks") + blocksField.isAccessible = true + val blocks = blocksField.get(selection) as Set + val block = blocks.first() + val stack = ItemStack(block) + return object : DropdownBoxEntry.DefaultSelectionCellElement>(selection, toTextFunction) + { + override fun render( + graphics: GuiGraphics?, + mouseX: Int, + mouseY: Int, + x: Int, + y: Int, + width: Int, + height: Int, + delta: Float + ) + { + this.rendering = true + this.x = x + this.y = y + this.width = width + this.height = height + val b = mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height + if (b) + { + graphics!!.fill(x + 1, y + 1, x + width - 1, y + height - 1, -15132391) + } + + graphics!!.drawString( + Minecraft.getInstance().font, + (this.toTextFunction.apply( + r + ) as Component).visualOrderText, x + 6 + 18, y + 6, if (b) 16777215 else 8947848 + ) + graphics.renderItem(stack, x + 4, y + 2) + } + } + } + + override fun getCellHeight(): Int + { + return 20 + } + + override fun getCellWidth(): Int + { + return 146 + } + + override fun getDropBoxMaxHeight(): Int + { + return cellHeight * 7 + } + } +} + +/** Reflectively exposes Cloth Config's private `alpha` flag on [ColorFieldBuilder], since it has no public getter/setter. */ +var ColorFieldBuilder.alphaMode: Boolean + get() = getReflection("alpha") + set(value) = setReflection("alpha", value) + +/** + * The `start*Field`/`start*List`/`start*Map` functions below extend [ConfigEntryBuilder] the same + * way Cloth Config's own built-ins do (`startBooleanToggle`, `startIntField`, ...), so the field + * types Archie adds - nested specs, registry entries, keybind/color lists and maps - are used the + * same way. Each one just forwards to the matching builder class's constructor. + */ +fun ConfigEntryBuilder.startSpecField(fieldNameKey: Component, value: T): SpecFieldBuilder +{ + return SpecFieldBuilder(resetButtonKey, fieldNameKey, value) +} + +fun ConfigEntryBuilder.startConfigField(fieldNameKey: Component, value: T): ConfigFieldBuilder +{ + return ConfigFieldBuilder(resetButtonKey, fieldNameKey, value) +} + +/** See [startSpecField]. Builds a single registry-entry field, resolved against [registry] and optionally narrowed to [subclass]. */ +fun ConfigEntryBuilder.startRegistryField( + fieldNameKey: Component, + value: T, + subclass: KClass? = null, + registry: Registry +): RegistryFieldBuilder +{ + return RegistryFieldBuilder(resetButtonKey, fieldNameKey, subclass, registry, value) +} + +/** See [startSpecField]. Builds a dropdown field over arbitrary [selections], accepting free-text input for a `String` value. */ +fun ConfigEntryBuilder.startStringDropdownField( + fieldNameKey: Component, + value: String, + selections: Iterable = emptyList() +): DropdownFieldBuilder +{ + return DropdownFieldBuilder(resetButtonKey, fieldNameKey, value, selections).apply { + toObjectFunction = { it } + } +} + +/** See [startSpecField]. Builds a dropdown field over arbitrary [selections] of any type [T]. */ +fun ConfigEntryBuilder.startDropdownField( + fieldNameKey: Component, + value: T, + selections: Iterable = emptyList() +): DropdownFieldBuilder +{ + return DropdownFieldBuilder(resetButtonKey, fieldNameKey, value, selections) +} + +/** See [startSpecField]. Builds a list of nested [DataSpec] entries. */ +fun ConfigEntryBuilder.startSpecList( + fieldNameKey: Component, + value: List, + factory: () -> T +): SpecListBuilder +{ + return SpecListBuilder(resetButtonKey, fieldNameKey, value, factory) +} + +/** See [startSpecField]. Builds a list of [registry] entries; [factory] supplies a value for newly-inserted rows. */ +fun ConfigEntryBuilder.startRegistryList( + fieldNameKey: Component, + value: List, + factory: () -> T, + subclass: KClass? = null, + registry: Registry +): RegistryListBuilder +{ + return RegistryListBuilder(resetButtonKey, fieldNameKey, value, factory, subclass, registry) +} + +/** See [startSpecField]. Builds a list of keybind entries; [factory] supplies a value for newly-inserted rows. */ +fun ConfigEntryBuilder.startKeycodeList( + fieldNameKey: Component, + value: List, + factory: () -> ModifierKeyCode +): KeycodeListBuilder +{ + return KeycodeListBuilder(resetButtonKey, fieldNameKey, value, factory) +} + +/** See [startSpecField]. Builds a list of color entries; [factory] supplies a value for newly-inserted rows. */ +fun ConfigEntryBuilder.startColorList( + fieldNameKey: Component, + value: List, + factory: () -> Color +): ColorListBuilder +{ + return ColorListBuilder(resetButtonKey, fieldNameKey, value, factory) +} + +/** See [startSpecField]. Builds a `String`-keyed map of nested [DataSpec] entries. */ +fun ConfigEntryBuilder.startSpecMap( + fieldNameKey: Component, + value: Map, + factory: () -> T +): SpecMapBuilder +{ + return SpecMapBuilder(resetButtonKey, fieldNameKey, value, factory) +} + +/** See [startSpecField]. Builds a `String`-keyed map of [registry] entries; [factory] supplies a value for newly-inserted rows. */ +fun ConfigEntryBuilder.startRegistryMap( + fieldNameKey: Component, + value: Map, + factory: () -> T, + subclass: KClass? = null, + registry: Registry +): RegistryMapBuilder +{ + return RegistryMapBuilder(resetButtonKey, fieldNameKey, value, factory, subclass, registry) +} + +/** See [startSpecField]. Builds a `String`-keyed map of keybind entries; [factory] supplies a value for newly-inserted rows. */ +fun ConfigEntryBuilder.startKeycodeMap( + fieldNameKey: Component, + value: Map, + factory: () -> ModifierKeyCode +): KeycodeMapBuilder +{ + return KeycodeMapBuilder(resetButtonKey, fieldNameKey, value, factory) +} + +/** See [startSpecField]. Builds a `String`-keyed map of color entries; [factory] supplies a value for newly-inserted rows. */ +fun ConfigEntryBuilder.startColorMap( + fieldNameKey: Component, + value: Map, + factory: () -> Color +): ColorMapBuilder +{ + return ColorMapBuilder(resetButtonKey, fieldNameKey, value, factory) +} + +/** See [startSpecField]. Builds a `String`-keyed map of `Int` entries. */ +fun ConfigEntryBuilder.startIntMap(fieldNameKey: Component, value: Map): IntegerMapBuilder +{ + return IntegerMapBuilder(resetButtonKey, fieldNameKey, value) +} + +/** See [startSpecField]. Builds a `String`-keyed map of `Long` entries. */ +fun ConfigEntryBuilder.startLongMap(fieldNameKey: Component, value: Map): LongMapBuilder +{ + return LongMapBuilder(resetButtonKey, fieldNameKey, value) +} + +/** See [startSpecField]. Builds a `String`-keyed map of `Float` entries. */ +fun ConfigEntryBuilder.startFloatMap(fieldNameKey: Component, value: Map): FloatMapBuilder +{ + return FloatMapBuilder(resetButtonKey, fieldNameKey, value) +} + +/** See [startSpecField]. Builds a `String`-keyed map of `Double` entries. */ +fun ConfigEntryBuilder.startDoubleMap(fieldNameKey: Component, value: Map): DoubleMapBuilder +{ + return DoubleMapBuilder(resetButtonKey, fieldNameKey, value) +} + +/** See [startSpecField]. Builds a `String`-keyed map of `String` entries. */ +fun ConfigEntryBuilder.startStrMap(fieldNameKey: Component, value: Map): StringMapBuilder +{ + return StringMapBuilder(resetButtonKey, fieldNameKey, value) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/entry/ConfigSpecEntry.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/entry/ConfigSpecEntry.kt new file mode 100644 index 000000000..cdae6e293 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/entry/ConfigSpecEntry.kt @@ -0,0 +1,111 @@ +package net.kernelpanicsoft.archie.config.entry + +import com.google.common.collect.Lists +import me.shedaniel.clothconfig2.api.AbstractConfigListEntry +import net.kernelpanicsoft.archie.config.ConfigSpec +import net.kernelpanicsoft.archie.util.minecraftClient +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.components.AbstractWidget +import net.minecraft.client.gui.components.Button +import net.minecraft.client.gui.components.events.GuiEventListener +import net.minecraft.client.gui.narration.NarratableEntry +import net.minecraft.network.chat.Component +import java.util.* + +/** + * Cloth Config list entry rendering a single "Edit" button for [value] that, when clicked, opens + * `value.client.buildConfig(...)` - i.e. navigates from a container's screen into that + * [ConfigSpec]'s own screen. Built by `ConfigFieldBuilder`. + */ +class ConfigSpecEntry( + fieldName: Component, + buttonText: Component, + value: T, + requiresRestart: Boolean +) : AbstractConfigListEntry(fieldName, requiresRestart) +{ + private var value: T + private val buttonWidget: Button + private val widgets: MutableList + + init + { + this.value = value + this.buttonWidget = Button.builder( + buttonText + ) { + configScreen?.let {minecraftClient.setScreen(value.client.buildConfig(it))} + } + .bounds(0, 0, 150, 20).build() + this.widgets = + Lists.newArrayList(*arrayOf(this.buttonWidget)) + } + + override fun getValue(): T + { + return this.value + } + + fun setValue(value: T) + { + this.value = value + } + + override fun getDefaultValue(): Optional = Optional.empty() + + override fun render( + graphics: GuiGraphics, + index: Int, + y: Int, + x: Int, + entryWidth: Int, + entryHeight: Int, + mouseX: Int, + mouseY: Int, + isHovered: Boolean, + delta: Float + ) + { + super.render(graphics, index, y, x, entryWidth, entryHeight, mouseX, mouseY, isHovered, delta) + val window = Minecraft.getInstance().window + this.buttonWidget.active = this.isEditable + this.buttonWidget.y = y + + val displayedFieldName = this.displayedFieldName + if (minecraftClient.font.isBidirectional) + { + graphics.drawString( + minecraftClient.font, + displayedFieldName.visualOrderText, + window.guiScaledWidth - x - Minecraft.getInstance().font.width(displayedFieldName), + y + 6, + 16777215 + ) + this.buttonWidget.x = x + } else + { + graphics.drawString( + minecraftClient.font, + displayedFieldName.visualOrderText, + x, + y + 6, + this.preferredTextColor + ) + this.buttonWidget.x = x + entryWidth - 150 + } + + this.buttonWidget.setWidth(150) + this.buttonWidget.render(graphics, mouseX, mouseY, delta) + } + + override fun children(): MutableList + { + return this.widgets + } + + override fun narratables(): MutableList + { + return this.widgets + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/extensions.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/extensions.kt new file mode 100644 index 000000000..3af284e05 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/extensions.kt @@ -0,0 +1,22 @@ +package net.kernelpanicsoft.archie.config + +import me.shedaniel.clothconfig2.api.ConfigEntryBuilder +import me.shedaniel.clothconfig2.gui.entries.TextListEntry +import java.util.* + +/** + * Converts this string to `snake_case`, splitting on both spaces and camelCase humps. Used to + * derive field/category ids from titles and delegated property names (e.g. `"Max Items"` and + * `maxItems` both become `max_items`). + */ +fun String.toSnakeCase() = + split(" ") + .joinToString("") { word -> + word.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } + } + .replace(humps, "_").lowercase() + +private val humps = "(?<=.)(?=\\p{Upper})".toRegex() + +/** A comment-list entry id paired with a factory for the Cloth Config [TextListEntry] it renders as. */ +typealias Comment = Pair TextListEntry> \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/Json5ConfigSerializer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/Json5ConfigSerializer.kt new file mode 100644 index 000000000..73d44d8df --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/Json5ConfigSerializer.kt @@ -0,0 +1,31 @@ +package net.kernelpanicsoft.archie.config.serializer + +import net.kernelpanicsoft.archie.config.ConfigSpec +import net.kernelpanicsoft.archie.config.IConfigSerializer +import io.github.xn32.json5k.Json5 +import java.nio.file.Path + + +/** [IConfigSerializer] for the JSON5 format (JSON with comments). Archie's default on Fabric. */ +object Json5ConfigSerializer : IConfigSerializer +{ + private val json5 = Json5 { + prettyPrint = true + quoteMemberNames = true + encodeDefaults = true + } + override fun configPath(config: ConfigSpec, configFolder: Path): Path + { + return configFolder.resolve("${config.filename}.json5") + } + + override fun loadString(config: ConfigSpec, string: String) + { + json5.decodeFromString(config.serializer, string) + } + + override fun saveString(config: ConfigSpec): String + { + return json5.encodeToString(config.serializer, config) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/JsonConfigSerializer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/JsonConfigSerializer.kt new file mode 100644 index 000000000..1c1f54854 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/JsonConfigSerializer.kt @@ -0,0 +1,32 @@ +package net.kernelpanicsoft.archie.config.serializer + +import net.kernelpanicsoft.archie.config.ConfigSpec +import net.kernelpanicsoft.archie.config.IConfigSerializer +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.json.Json +import java.nio.file.Path + +/** [IConfigSerializer] for plain JSON (no comments). Not used by default on any platform - opt in explicitly by overriding [ConfigSpec.fileSerializer]. */ +object JsonConfigSerializer : IConfigSerializer +{ + @OptIn(ExperimentalSerializationApi::class) + private val json = Json { + prettyPrint = true + prettyPrintIndent = "\t" + ignoreUnknownKeys = true + } + override fun configPath(config: ConfigSpec, configFolder: Path): Path + { + return configFolder.resolve("${config.filename}.json") + } + + override fun loadString(config: ConfigSpec, string: String) + { + json.decodeFromString(config.serializer, string) + } + + override fun saveString(config: ConfigSpec): String + { + return json.encodeToString(config.serializer, config) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/NullConfigSerializer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/NullConfigSerializer.kt new file mode 100644 index 000000000..2b01c49b9 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/NullConfigSerializer.kt @@ -0,0 +1,32 @@ +package net.kernelpanicsoft.archie.config.serializer + +import net.kernelpanicsoft.archie.config.ConfigSpec +import net.kernelpanicsoft.archie.config.IConfigSerializer +import java.nio.file.Path + +/** + * No-op [IConfigSerializer]: [load] and [save] do nothing, and the string-based methods all throw. + * Useful as a [ConfigSpec.fileSerializer] override for a spec that should never persist to disk + * (e.g. an in-memory-only or test config). + */ +object NullConfigSerializer : IConfigSerializer +{ + override fun configPath(config: ConfigSpec, configFolder: Path): Path + { + throw UnsupportedOperationException() + } + + override fun loadString(config: ConfigSpec, string: String) + { + throw UnsupportedOperationException() + } + + override fun saveString(config: ConfigSpec): String + { + throw UnsupportedOperationException() + } + + override fun load(config: ConfigSpec, configFolder: Path) = Unit + + override fun save(config: ConfigSpec, configFolder: Path) = Unit +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/TomlConfigSerializer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/TomlConfigSerializer.kt new file mode 100644 index 000000000..212a45551 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/TomlConfigSerializer.kt @@ -0,0 +1,32 @@ +package net.kernelpanicsoft.archie.config.serializer + +import net.kernelpanicsoft.archie.config.ConfigSpec +import net.kernelpanicsoft.archie.config.IConfigSerializer +import net.peanuuutz.tomlkt.Toml +import net.peanuuutz.tomlkt.TomlIndentation +import java.nio.file.Path + +/** [IConfigSerializer] for the TOML format. Archie's default on NeoForge. */ +object TomlConfigSerializer : IConfigSerializer +{ + private val toml = Toml { + ignoreUnknownKeys = true + indentation = TomlIndentation.Tab + } + + override fun configPath(config: ConfigSpec, configFolder: Path): Path + { + return configFolder.resolve("${config.filename}.toml") + } + + override fun loadString(config: ConfigSpec, string: String) + { + toml.decodeFromString(config.serializer, string) + } + + override fun saveString(config: ConfigSpec): String + { + return toml.encodeToString(config.serializer, config) + } + +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt new file mode 100644 index 000000000..0fe4bf2d5 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt @@ -0,0 +1,60 @@ +package net.kernelpanicsoft.archie.data + +import dev.architectury.platform.Mod +import net.minecraft.core.HolderLookup +import net.minecraft.data.DataProvider +import net.minecraft.data.PackOutput +import java.util.concurrent.CompletableFuture + +/** + * Base class for a platform's datagen entrypoint. Trimmed to the minimal surface `archie-core` + * needs (just enough for [net.kernelpanicsoft.archie.events.AEvents]'s `GatherDataHandler` to + * reference it as a type) - the full `client { }`/`common { }` provider DSL (models, languages, + * tags, recipes) lives in `archie-datagen` as extension functions/classes on this type, since it + * pulls in the whole datagen provider graph. See `archie-datagen`'s `ADataGenerator` extensions. + */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +abstract class ADataGenerator +{ + /** Whether client-only providers should run, from the `archie.datagen.client` system property. */ + val isClient: Boolean + get() = System.getProperty("archie.datagen.client").toBoolean() + + /** Whether server-only providers should run, from the `archie.datagen.server` system property. */ + val isServer: Boolean + get() = System.getProperty("archie.datagen.server").toBoolean() + + abstract val mod: Mod + + /** + * Registers [factory] with the underlying platform data generator, running it only when + * [run] is `true`, and returns the constructed provider so it can be reused (e.g. an item + * tags provider depending on a previously created block tags provider). + */ + abstract fun addProvider( + run: Boolean = true, + factory: ARegistryAwareDataProviderFactory + ): T + + /** [addProvider] overload for providers that don't need access to [HolderLookup.Provider]. */ + fun addProvider(run: Boolean = true, factory: ADataProviderFactory): T + { + return addProvider(run) { output, _ -> + factory(output) + } + } + + operator fun invoke(block: ADataGenerator.() -> Unit) = apply(block) + + /** Factory for a [DataProvider] that only needs a [PackOutput] to be constructed. */ + fun interface ADataProviderFactory + { + operator fun invoke(output: PackOutput): T + } + + /** Factory for a [DataProvider] that also needs the registry [HolderLookup.Provider] future. */ + fun interface ARegistryAwareDataProviderFactory + { + operator fun invoke(output: PackOutput, registries: CompletableFuture): T + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt new file mode 100644 index 000000000..94c57929b --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt @@ -0,0 +1,12 @@ +package net.kernelpanicsoft.archie.data + +/** + * Cross-loader switch reporting whether the current run is a datagen run. + * + * Loader implementations resolve [isDataGen] from run configuration system properties set by + * the `runDatagen` Gradle tasks. + */ +expect object ADataGeneratorPlatform +{ + val isDataGen: Boolean +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AAndCondition.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AAndCondition.kt new file mode 100644 index 000000000..209ea31ab --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AAndCondition.kt @@ -0,0 +1,35 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.google.common.base.Joiner +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.kernelpanicsoft.archie.Archie +import net.minecraft.resources.ResourceLocation + +/** Condition that holds only when every one of [children] holds (logical AND). */ +data class AAndCondition(override val children: List) : + AGroupCondition() +{ + constructor(vararg values: IACondition) : this(values.toList()) + + override fun reducer(a: Boolean, b: Boolean): Boolean = a and b + + override val codec: MapCodec = CODEC + override val identifier: ResourceLocation = ID + + override fun toString(): String + { + return "(${Joiner.on(" && ").join(children)})" + } + + companion object + { + val CODEC: MapCodec = + RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> + builder.group( + IACondition.CODEC.listOf().fieldOf("children").forGetter(AAndCondition::children) + ).apply(builder, ::AAndCondition) + } + val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "and") + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ABuiltinConditions.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ABuiltinConditions.kt new file mode 100644 index 000000000..e05139993 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ABuiltinConditions.kt @@ -0,0 +1,30 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.mojang.serialization.MapCodec +import net.minecraft.resources.ResourceLocation + +/** Registers Archie's built-in [IACondition] types (logical combinators + [AModLoadedCondition]/[ARegistryCondition]/[APlatformCondition]) so their codecs can decode from datapacks. */ +object ABuiltinConditions +{ + /** Registers every built-in condition type; called once during [net.kernelpanicsoft.archie.Archie.init]. */ + fun init() + { + register(AModLoadedCondition.ID, AModLoadedCondition.CODEC) + register(ARegistryCondition.ID, ARegistryCondition.CODEC) + register(APlatformCondition.ID, APlatformCondition.CODEC) + + register(AAndCondition.ID, AAndCondition.CODEC) + register(AOrCondition.ID, AOrCondition.CODEC) + register(AXorCondition.ID, AXorCondition.CODEC) + register(ANotCondition.ID, ANotCondition.CODEC) + register(AEqualsCondition.ID, AEqualsCondition.CODEC) + + register(ATrueCondition.ID, ATrueCondition.CODEC) + register(AFalseCondition.ID, AFalseCondition.CODEC) + } + + private inline fun register(identifier: ResourceLocation, codec: MapCodec) + { + IACondition.register(identifier, codec) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt new file mode 100644 index 000000000..c660e024e --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt @@ -0,0 +1,22 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import net.minecraft.resources.ResourceLocation + +/** + * Cross-loader hooks that plug [IACondition] into each loader's native datapack condition + * system, since Fabric and NeoForge each have their own recipe/tag condition machinery. + * + * Trimmed to the runtime-needed half (backs [IACondition.register]/[IACondition.CODEC], evaluated + * whenever a datapack loads a condition) - `withCondition`/`fabricRecipeProvider` (used only by the + * recipe-datagen DSL) live in `archie-datagen` instead. + */ +expect object AConditionsPlatform +{ + /** Registers condition type keyed by [identifier], decodable with [codec]; backs [IACondition.register]. */ + fun register(identifier: ResourceLocation, codec: MapCodec) + + /** The dispatch codec decoding any registered [IACondition]; backs [IACondition.CODEC]. */ + fun codec(): Codec +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AEqualsCondition.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AEqualsCondition.kt new file mode 100644 index 000000000..9280541dd --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AEqualsCondition.kt @@ -0,0 +1,36 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.google.common.base.Joiner +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.util.rem +import net.minecraft.resources.ResourceLocation + +/** Condition that folds [children]'s results pairwise with `==` (holds when they agree). */ +data class AEqualsCondition(override val children: List) : + AGroupCondition() +{ + constructor(vararg values: IACondition) : this(values.toList()) + + override fun reducer(a: Boolean, b: Boolean): Boolean = a == b + + override val codec: MapCodec = CODEC + override val identifier: ResourceLocation = ID + + override fun toString(): String + { + return "(${Joiner.on(" == ").join(children)})" + } + + companion object + { + val CODEC: MapCodec = + RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> + builder.group( + IACondition.CODEC.listOf().fieldOf("children").forGetter(AEqualsCondition::children), + ).apply(builder, ::AEqualsCondition) + } + val ID: ResourceLocation = Archie % "equals" + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AFalseCondition.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AFalseCondition.kt new file mode 100644 index 000000000..138bed941 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AFalseCondition.kt @@ -0,0 +1,26 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.mojang.serialization.MapCodec +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.util.rem +import net.minecraft.resources.ResourceLocation + +/** Condition that never holds. */ +data object AFalseCondition : + IACondition +{ + val CODEC: MapCodec = MapCodec.unit(AFalseCondition).stable() + val ID: ResourceLocation = Archie % "false" + override fun test(context: IACondition.IContext): Boolean + { + return false + } + + override val codec: MapCodec = CODEC + override val identifier: ResourceLocation = ID + + override fun toString(): String + { + return "false" + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AGroupCondition.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AGroupCondition.kt new file mode 100644 index 000000000..e0c46fc90 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AGroupCondition.kt @@ -0,0 +1,16 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +/** + * Base for [IACondition]s that combine [children] pairwise via [reducer], e.g. [AAndCondition], + * [AOrCondition], [AXorCondition]. [children] must be non-empty. + */ +abstract class AGroupCondition : IACondition +{ + abstract val children: List + + /** Combines two child results into one; applied left-to-right across [children]. */ + abstract fun reducer(a: Boolean, b: Boolean): Boolean + + override fun test(context: IACondition.IContext): Boolean = + children.map { it.test(context) }.reduce(::reducer) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AModLoadedCondition.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AModLoadedCondition.kt new file mode 100644 index 000000000..f890955ba --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AModLoadedCondition.kt @@ -0,0 +1,46 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.google.common.base.Joiner +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.kernelpanicsoft.archie.Archie +import dev.architectury.platform.Platform +import net.minecraft.resources.ResourceLocation + +/** Condition that holds when every mod id in [mods] is loaded, per [Platform.isModLoaded]. */ +data class AModLoadedCondition(val mods: List) : IACondition +{ + constructor(vararg mods: String) : this(mods.toList()) + override fun test(context: IACondition.IContext): Boolean + { + return mods.map(Platform::isModLoaded).reduce { a, b -> a && b } + } + + override val codec: MapCodec = CODEC + override val identifier: ResourceLocation = ID + + override fun toString(): String + { + return "mod_loaded(${Joiner.on(", ").join(mods.map { "\"$it\"" })})" + } + + companion object + { + val CODEC: MapCodec = + RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> + builder + .group( + Codec.STRING.listOf().fieldOf("mods").forGetter(AModLoadedCondition::mods) + ) + .apply( + builder + ) { mods: List -> + AModLoadedCondition( + mods + ) + } + } + val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "mod_loaded") + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ANotCondition.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ANotCondition.kt new file mode 100644 index 000000000..7163f2ccd --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ANotCondition.kt @@ -0,0 +1,34 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.kernelpanicsoft.archie.Archie +import net.minecraft.resources.ResourceLocation + +/** Condition that inverts the result of [child] (logical NOT). */ +data class ANotCondition(val child: IACondition) : + IACondition +{ + override fun test(context: IACondition.IContext): Boolean + { + return !child.test(context) + } + + override val codec: MapCodec = CODEC + override val identifier: ResourceLocation = ID + + override fun toString(): String + { + return "!$child" + } + companion object + { + val CODEC: MapCodec = + RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> + builder.group( + IACondition.CODEC.fieldOf("child").forGetter(ANotCondition::child) + ).apply(builder, ::ANotCondition) + } + val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "not") + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AOrCondition.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AOrCondition.kt new file mode 100644 index 000000000..a728aa2da --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AOrCondition.kt @@ -0,0 +1,35 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.google.common.base.Joiner +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.kernelpanicsoft.archie.Archie +import net.minecraft.resources.ResourceLocation + +/** Condition that holds when at least one of [children] holds (logical OR). */ +data class AOrCondition(override val children: List) : + AGroupCondition() +{ + constructor(vararg values: IACondition) : this(values.toList()) + + override fun reducer(a: Boolean, b: Boolean): Boolean = a or b + + override val codec: MapCodec = CODEC + override val identifier: ResourceLocation = ID + + override fun toString(): String + { + return "(${Joiner.on(" || ").join(children)})" + } + + companion object + { + val CODEC: MapCodec = + RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> + builder.group( + IACondition.CODEC.listOf().fieldOf("children").forGetter(AOrCondition::children), + ).apply(builder, ::AOrCondition) + } + val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "or") + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/APlatformCondition.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/APlatformCondition.kt new file mode 100644 index 000000000..fd7ea81a7 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/APlatformCondition.kt @@ -0,0 +1,47 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.APlatform +import kotlinx.serialization.Transient +import net.minecraft.resources.ResourceLocation + +/** Condition that holds when the running loader's [APlatform.platform] id equals [platform]. */ +data class APlatformCondition(val platform: String) : IACondition +{ + override fun test(context: IACondition.IContext): Boolean + { + return APlatform.platform == platform + } + + @Transient + override val codec: MapCodec = CODEC + @Transient + override val identifier: ResourceLocation = ID + + override fun toString(): String + { + return "platform(\"$platform\")" + } + + companion object + { + val CODEC: MapCodec = + RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> + builder + .group( + Codec.STRING.fieldOf("platform").forGetter(APlatformCondition::platform) + ) + .apply( + builder + ) { platform: String -> + APlatformCondition( + platform + ) + } + } + val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "platform") + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ARegistryCondition.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ARegistryCondition.kt new file mode 100644 index 000000000..f8d1fb863 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ARegistryCondition.kt @@ -0,0 +1,49 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.google.common.base.Joiner +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.serialization.serializers.ResourceLocationSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.Transient +import net.minecraft.core.Registry +import net.minecraft.resources.ResourceKey +import net.minecraft.resources.ResourceLocation + +/** Condition that holds when every id in [entries] is registered in the registry keyed by [registry]. */ +data class ARegistryCondition(private val registry: @Serializable(with = ResourceLocationSerializer::class) ResourceLocation, private val entries: List<@Serializable(with = ResourceLocationSerializer::class) ResourceLocation>) : + IACondition +{ + constructor(registry: ResourceLocation, vararg entries: ResourceLocation) : this(registry, entries.toList()) + + override fun test(context: IACondition.IContext): Boolean + { + val registryRef: ResourceKey> = ResourceKey.createRegistryKey(registry) + val registry: Registry = context.getRegistry(registryRef) + return entries.all { registry.keySet().contains(it) } + } + + @Transient + override val codec: MapCodec = CODEC + @Transient + override val identifier: ResourceLocation = ID + + override fun toString(): String + { + return "registry(\"$registry\", ${Joiner.on(", ").join(entries.map { "\"$it\"" })})" + } + + companion object + { + val CODEC: MapCodec = + RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> + builder.group( + ResourceLocation.CODEC.optionalFieldOf("registry", ResourceLocation.parse("item")).forGetter(ARegistryCondition::registry), + ResourceLocation.CODEC.listOf().fieldOf("entries").forGetter(ARegistryCondition::entries) + ).apply(builder, ::ARegistryCondition) + } + val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "registry") + } + +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ATrueCondition.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ATrueCondition.kt new file mode 100644 index 000000000..daecc9b88 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ATrueCondition.kt @@ -0,0 +1,25 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.mojang.serialization.MapCodec +import net.kernelpanicsoft.archie.Archie +import net.minecraft.resources.ResourceLocation + +/** Condition that always holds. */ +data object ATrueCondition : + IACondition +{ + val CODEC: MapCodec = MapCodec.unit(ATrueCondition).stable() + val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "true") + override fun test(context: IACondition.IContext): Boolean + { + return true + } + + override val codec: MapCodec = CODEC + override val identifier: ResourceLocation = ID + + override fun toString(): String + { + return "true" + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AXorCondition.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AXorCondition.kt new file mode 100644 index 000000000..daec85946 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AXorCondition.kt @@ -0,0 +1,35 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.google.common.base.Joiner +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.kernelpanicsoft.archie.Archie +import net.minecraft.resources.ResourceLocation + +/** Condition that holds when an odd number of [children] hold (logical XOR, folded pairwise). */ +data class AXorCondition(override val children: List) : + AGroupCondition() +{ + constructor(vararg values: IACondition) : this(values.toList()) + + override fun reducer(a: Boolean, b: Boolean): Boolean = a xor b + + override val codec: MapCodec = CODEC + override val identifier: ResourceLocation = ID + + override fun toString(): String + { + return "(${Joiner.on(" ^^ ").join(children)})" + } + + companion object + { + val CODEC: MapCodec = + RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> + builder.group( + IACondition.CODEC.listOf().fieldOf("children").forGetter(AXorCondition::children), + ).apply(builder, ::AXorCondition) + } + val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "xor") + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt new file mode 100644 index 000000000..de779e370 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt @@ -0,0 +1,84 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import net.minecraft.core.Holder +import net.minecraft.core.Registry +import net.minecraft.resources.ResourceKey +import net.minecraft.resources.ResourceLocation +import net.minecraft.tags.TagKey + +/** + * A cross-loader condition, evaluated at datapack load time, that decides whether the entry it's + * attached to (a recipe, tag entry, etc.) should be active. Mirrors NeoForge's/Fabric's native + * condition systems but is decoded through [CODEC] so the same condition classes work on both + * loaders. + * + * Built-in implementations live alongside this file (e.g. [AAndCondition], [AOrCondition], + * [ANotCondition], [ATrueCondition], [AModLoadedCondition], [ARegistryCondition]); see + * [ABuiltinConditions] for the full set and [AConditionBuilder] for a DSL to combine them. + * Register custom conditions with [register]. + */ +interface IACondition +{ + /** Evaluates this condition against [context], returning whether it holds. */ + fun test(context: IContext): Boolean + + /** The codec used to (de)serialize this condition to/from JSON. */ + val codec: MapCodec + + /** The condition type's registered id, matching the key it was [register]ed under. */ + val identifier: ResourceLocation + + /** Read-only view of loaded tags/registries available to [test] while a condition is evaluated. */ + interface IContext + { + /** + * Return the requested tag if available, or an empty tag otherwise. + */ + fun getTag(key: TagKey): Collection> + { + return getAllTags(key.registry()).getOrDefault(key.location(), setOf>()) + } + + /** + * Return all the loaded tags for the passed registry, or an empty map if none is available. + * Note that the map and the tags are unmodifiable. + */ + fun getAllTags(registry: ResourceKey>): Map>> + + /** + * Return the registry entry for the specified [ResourceKey] + */ + fun getRegistryEntry(key: ResourceKey) : T? + { + return getRegistry(ResourceKey.createRegistryKey(key.registry()))[key] + } + + /** + * Return the registry entry for the given [ResourceLocation] in the [Registry] specified by the registry key + */ + fun getRegistryEntry(registry: ResourceKey>, key: ResourceLocation): T? + { + return getRegistryEntry(ResourceKey.create(registry, key)) + } + + /** + * Return the [Registry] for the given registry key + */ + fun getRegistry(registry: ResourceKey>): Registry + } + + companion object + { + /** Registers condition type [T] under [identifier] so it can be decoded via [CODEC]. */ + inline fun register(identifier: ResourceLocation, codec: MapCodec) + { + AConditionsPlatform.register(identifier, codec) + } + + /** The dispatch codec that decodes any registered [IACondition] type by its [identifier]. */ + val CODEC: Codec + get() = AConditionsPlatform.codec() + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAllIngredient.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAllIngredient.kt new file mode 100644 index 000000000..a6429423e --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAllIngredient.kt @@ -0,0 +1,60 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.util.rem +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.crafting.Ingredient + +/** Custom ingredient that matches a stack only when every one of its sub-ingredients matches it. Build via [of]. */ +class AAllIngredient private constructor(ingredients: List) : + ACombinedIngredient(ingredients) +{ + override fun test(stack: ItemStack): Boolean + { + return ingredients.all { ingredient -> ingredient.test(stack) } + } + + override val matchingStacks: MutableList by lazy { + // There's always at least one sub ingredient, so accessing ingredients[0] is safe. + val previewStacks: MutableList = + mutableListOf(*ingredients[0].items) + + for (i in 1 until ingredients.size) + { + val ing: Ingredient = ingredients[i] + previewStacks.removeIf { stack: ItemStack -> + !ing.test( + stack + ) + } + } + + previewStacks + } + + override val serializer: IACustomIngredientSerializer<*> = Serializer + + companion object + { + /** Creates a vanilla [Ingredient] that matches only when every one of [ingredients] matches. */ + fun of(vararg ingredients: Ingredient): Ingredient = AAllIngredient(ingredients.toList()).vanilla + private val ALLOW_EMPTY_CODEC = createCodec(Ingredient.CODEC) + private val DISALLOW_EMPTY_CODEC = createCodec(Ingredient.CODEC_NONEMPTY) + + private fun createCodec(ingredientCodec: Codec): MapCodec + { + return ingredientCodec + .listOf() + .fieldOf("ingredients") + .xmap(::AAllIngredient, AAllIngredient::ingredients) + } + + val Serializer: IACustomIngredientSerializer = + Serializer( + Archie % "all", + ::AAllIngredient, ALLOW_EMPTY_CODEC, DISALLOW_EMPTY_CODEC + ) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAnyIngredient.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAnyIngredient.kt new file mode 100644 index 000000000..3ea3659e8 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAnyIngredient.kt @@ -0,0 +1,51 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.util.rem +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.crafting.Ingredient +import java.util.* + +/** Custom ingredient that matches a stack when at least one of its sub-ingredients matches it. Build via [of]. */ +class AAnyIngredient private constructor(ingredients: List): ACombinedIngredient(ingredients) +{ + override fun test(stack: ItemStack): Boolean + { + return ingredients.any { ingredient -> ingredient.test(stack) } + } + + override val matchingStacks: MutableList by lazy { + val previewStacks: MutableList = ArrayList() + for (ingredient in ingredients) + { + previewStacks.addAll(listOf(*ingredient.items)) + } + + previewStacks + } + override val serializer: IACustomIngredientSerializer<*> = Serializer + + companion object + { + /** Creates a vanilla [Ingredient] that matches when at least one of [ingredients] matches. */ + fun of(vararg ingredients: Ingredient): Ingredient = AAnyIngredient(ingredients.toList()).vanilla + private val ALLOW_EMPTY_CODEC = createCodec(Ingredient.CODEC) + private val DISALLOW_EMPTY_CODEC = createCodec(Ingredient.CODEC_NONEMPTY) + + private fun createCodec(ingredientCodec: Codec): MapCodec + { + return ingredientCodec + .listOf() + .fieldOf("ingredients") + .xmap(::AAnyIngredient, AAnyIngredient::ingredients) + } + + val Serializer: IACustomIngredientSerializer = + Serializer( + Archie % "any", + ::AAnyIngredient, ALLOW_EMPTY_CODEC, DISALLOW_EMPTY_CODEC + ) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ABuiltinIngredients.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ABuiltinIngredients.kt new file mode 100644 index 000000000..2e7071633 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ABuiltinIngredients.kt @@ -0,0 +1,15 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +/** Registers Archie's built-in [IACustomIngredient] serializers ([AAllIngredient], [AAnyIngredient], [AComponentsIngredient], [ACustomDataIngredient]). */ +object ABuiltinIngredients +{ + /** Registers every built-in ingredient serializer; called once during [net.kernelpanicsoft.archie.Archie.init]. */ + fun init() + { + IACustomIngredientSerializer.register(AAllIngredient.Serializer) + IACustomIngredientSerializer.register(AAnyIngredient.Serializer) + + IACustomIngredientSerializer.register(AComponentsIngredient.Serializer) + IACustomIngredientSerializer.register(ACustomDataIngredient.Serializer) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACombinedIngredient.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACombinedIngredient.kt new file mode 100644 index 000000000..2f5b10132 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACombinedIngredient.kt @@ -0,0 +1,64 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +import com.mojang.serialization.MapCodec +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.ByteBufCodecs +import net.minecraft.network.codec.StreamCodec +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.crafting.Ingredient +import java.util.function.Function + + +/** + * Base class for [IACustomIngredient]s that combine multiple sub-[ingredients], e.g. [AAllIngredient] + * (matches when every sub-ingredient matches) and [AAnyIngredient] (matches when any does). + */ +abstract class ACombinedIngredient protected constructor(ingredients: List) : + IACustomIngredient +{ + /** The sub-ingredients being combined; always non-empty. */ + val ingredients: List + + init + { + require(ingredients.isNotEmpty()) { "Combined ingredient must have at least one sub-ingredient" } + + this.ingredients = ingredients + } + + /** `true` if any sub-ingredient is a custom ingredient that itself requires testing. */ + override val requiresTesting: Boolean + get() + { + for (ingredient in ingredients) + { + if (ingredient is IACustomIngredientHolder<*> && ingredient.custom.requiresTesting) + { + return true + } + } + + return false + } + + /** Generic [IACustomIngredientSerializer] for [ACombinedIngredient] subtypes, built from a [factory] and empty/non-empty codecs. */ + class Serializer( + override val identifier: ResourceLocation, + private val factory: Function, I>, + private val allowEmptyCodec: MapCodec, + private val disallowEmptyCodec: MapCodec + ) : + IACustomIngredientSerializer + { + override fun getCodec(allowEmpty: Boolean): MapCodec + { + return if (allowEmpty) allowEmptyCodec else disallowEmptyCodec + } + + override val packetCodec: StreamCodec = run { + Ingredient.CONTENTS_STREAM_CODEC.apply(ByteBufCodecs.list()) + .map(factory, ACombinedIngredient::ingredients) + } + } + +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AComponentsIngredient.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AComponentsIngredient.kt new file mode 100644 index 000000000..455d74225 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AComponentsIngredient.kt @@ -0,0 +1,120 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.serialization.buildComponentPatch +import net.kernelpanicsoft.archie.util.rem +import net.minecraft.core.component.DataComponentPatch +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.crafting.Ingredient +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Custom ingredient that matches stacks accepted by [base] and additionally requires their data + * components to match [components] (present components must equal the patch's value; absent + * components must stay absent). Build via [of]. + */ +class AComponentsIngredient private constructor(val base: Ingredient, components: DataComponentPatch) : IACustomIngredient +{ + /** Non-empty patch of component values a matching stack must satisfy. */ + val components: DataComponentPatch + + init + { + require(!components.isEmpty) { "ComponentIngredient must have at least one defined component" } + this.components = components + } + + override fun test(stack: ItemStack): Boolean + { + if (!base.test(stack)) return false + + for ((type, value) in components.entrySet()) + { + if (value.isPresent) + { + if (!stack.has(type)) return false + + if (value.get() != stack.get(type)) return false + } else + { + if (stack.has(type)) return false + } + } + + return true + } + + override val matchingStacks: MutableList by lazy { + val stacks: MutableList = base.items.toMutableList() + stacks.replaceAll { stack -> + val copy = stack.copy() + copy.applyComponentsAndValidate(components) + copy + } + stacks + } + + override val requiresTesting: Boolean = true + override val serializer: IACustomIngredientSerializer<*> = Serializer + + object Serializer : IACustomIngredientSerializer + { + private val ID = Archie % "components" + + private val ALLOW_EMPTY_CODEC: MapCodec = createCodec( + Ingredient.CODEC + ) + private val DISALLOW_EMPTY_CODEC: MapCodec = createCodec( + Ingredient.CODEC_NONEMPTY + ) + + private val PACKET_CODEC: StreamCodec = StreamCodec.composite( + Ingredient.CONTENTS_STREAM_CODEC, + AComponentsIngredient::base, + DataComponentPatch.STREAM_CODEC, + AComponentsIngredient::components, + ::AComponentsIngredient + ) + + private fun createCodec(ingredientCodec: Codec): MapCodec + { + return RecordCodecBuilder.mapCodec { instance: RecordCodecBuilder.Instance -> + instance.group( + ingredientCodec.fieldOf("base").forGetter(AComponentsIngredient::base), + DataComponentPatch.CODEC.fieldOf("components").forGetter(AComponentsIngredient::components) + ).apply( + instance, ::AComponentsIngredient + ) + } + } + + override val identifier: ResourceLocation = ID + + override fun getCodec(allowEmpty: Boolean): MapCodec = + if (allowEmpty) ALLOW_EMPTY_CODEC else DISALLOW_EMPTY_CODEC + + override val packetCodec: StreamCodec = PACKET_CODEC + } + + companion object + { + /** Creates a vanilla [Ingredient] matching [base] stacks whose components satisfy [components]. */ + fun of(base: Ingredient, components: DataComponentPatch): Ingredient = AComponentsIngredient(base, components).vanilla + + /** [of] overload that builds the component patch with [builderAction]. */ + @OptIn(ExperimentalContracts::class) + fun of(base: Ingredient, builderAction: DataComponentPatch.Builder.() -> Unit): Ingredient + { + contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } + return of(base, buildComponentPatch(builderAction)) + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomDataIngredient.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomDataIngredient.kt new file mode 100644 index 000000000..dcfb843fd --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomDataIngredient.kt @@ -0,0 +1,122 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.serialization.buildCompoundTag +import net.benwoodworth.knbt.NbtCompoundBuilder +import net.kernelpanicsoft.archie.util.rem +import net.minecraft.core.component.DataComponents +import net.minecraft.nbt.CompoundTag +import net.minecraft.nbt.TagParser +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.ByteBufCodecs +import net.minecraft.network.codec.StreamCodec +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.component.CustomData +import net.minecraft.world.item.crafting.Ingredient +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +/** + * Custom ingredient that matches stacks accepted by [base] whose `minecraft:custom_data` + * component NBT is matched by [nbt] (a partial/sub-tag match, not exact equality). Build via [of]. + */ +class ACustomDataIngredient private constructor( + val base: Ingredient, + + nbt: CompoundTag +) : IACustomIngredient +{ + /** Non-empty NBT that a matching stack's custom data must be matched by. */ + val nbt: CompoundTag + + init + { + require(!nbt.isEmpty) { "NBT cannot be null or empty; use components ingredient for strict matching" } + this.nbt = nbt + } + + override fun test(stack: ItemStack): Boolean + { + if (!base.test(stack)) return false + + val nbt: CustomData? = stack[DataComponents.CUSTOM_DATA] + + return nbt?.matchedBy(this.nbt) ?: false + } + + override val matchingStacks: MutableList by lazy { + val stacks: MutableList = base.items.toMutableList() + stacks.replaceAll { stack -> + val copy: ItemStack = stack.copy() + copy.update(DataComponents.CUSTOM_DATA, CustomData.EMPTY) { + CustomData.of(it.copyTag().merge(this.nbt)) + } + copy + } + + stacks + } + + override val requiresTesting: Boolean = true + override val serializer: IACustomIngredientSerializer<*> = Serializer + + + object Serializer : IACustomIngredientSerializer + { + private val ID = Archie % "custom_data" + + private val ALLOW_EMPTY_CODEC: MapCodec = createCodec( + Ingredient.CODEC + ) + private val DISALLOW_EMPTY_CODEC: MapCodec = createCodec( + Ingredient.CODEC_NONEMPTY + ) + + private val PACKET_CODEC: StreamCodec = StreamCodec.composite( + Ingredient.CONTENTS_STREAM_CODEC, + ACustomDataIngredient::base, + ByteBufCodecs.COMPOUND_TAG, + ACustomDataIngredient::nbt, + ::ACustomDataIngredient + ) + + private fun createCodec(ingredientCodec: Codec): MapCodec + { + return RecordCodecBuilder.mapCodec { instance: RecordCodecBuilder.Instance -> + instance.group( + ingredientCodec.fieldOf("base").forGetter(ACustomDataIngredient::base), + TagParser.LENIENT_CODEC.fieldOf("nbt").forGetter(ACustomDataIngredient::nbt) + ).apply( + instance, ::ACustomDataIngredient + ) + } + } + + override val identifier: ResourceLocation = ID + + override fun getCodec(allowEmpty: Boolean): MapCodec = + if (allowEmpty) ALLOW_EMPTY_CODEC else DISALLOW_EMPTY_CODEC + + override val packetCodec: StreamCodec = PACKET_CODEC + } + + companion object + { + /** Creates a vanilla [Ingredient] matching [base] stacks whose custom data is matched by [nbt]. */ + fun of(base: Ingredient, nbt: CompoundTag): Ingredient = ACustomDataIngredient(base, nbt).vanilla + + /** [of] overload that builds the NBT with [builderAction]. */ + @OptIn(ExperimentalContracts::class) + fun of(base: Ingredient, builderAction: NbtCompoundBuilder.() -> Unit) + { + contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } + of(base, buildCompoundTag(builderAction)) + } + } + +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.common.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.common.kt new file mode 100644 index 000000000..53fe000e7 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.common.kt @@ -0,0 +1,9 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +import net.minecraft.world.item.crafting.Ingredient + +/** Cross-loader hook converting an [IACustomIngredient] into a vanilla [Ingredient]; backs [IACustomIngredient.vanilla]. */ +internal expect object ACustomIngredientPlatform +{ + fun vanillaOf(custom: IACustomIngredient): Ingredient +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.common.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.common.kt new file mode 100644 index 000000000..92a36a728 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.common.kt @@ -0,0 +1,8 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + + +/** Cross-loader hook registering an [IACustomIngredientSerializer]; backs [IACustomIngredientSerializer.register]. */ +internal expect object ACustomIngredientSerializerPlatform +{ + fun register(serializer: IACustomIngredientSerializer) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredient.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredient.kt new file mode 100644 index 000000000..8eb851491 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredient.kt @@ -0,0 +1,59 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.crafting.Ingredient +import org.jetbrains.annotations.ApiStatus + +/** + * Interface that modders can implement to create new recipe-matching behaviors beyond vanilla + * [Ingredient]s, ported from Fabric's custom ingredient API to work cross-loader. + * + * This is not directly implemented on vanilla [Ingredient]s; use [vanilla] to convert a custom + * ingredient into one. On disk, a custom ingredient is encoded by its [serializer], keyed by + * that serializer's registered identifier, plus whatever extra fields the serializer needs. + * + * @see IACustomIngredientSerializer + */ +interface IACustomIngredient +{ + /** + * Checks whether [stack] matches this ingredient. Must not modify [stack]. + */ + fun test(stack: ItemStack): Boolean + + /** + * The stacks that match this ingredient, for display purposes (e.g. in a recipe viewer). + * + * Guidelines for good compatibility: + * - These stacks need not be exhaustive or perfectly accurate, except when [requiresTesting] + * is `false`, in which case they must correspond exactly to every accepted item. + * - At least one stack must be returned, or the ingredient is considered + * [empty][Ingredient.isEmpty]. + * - Try to include at least one stack per accepted item, so inspecting mods can enumerate + * what the ingredient might accept. + * + * No caching is required here; the ingredient itself already caches this. + */ + val matchingStacks: MutableList + + /** + * Whether [test] must always be called to know if a stack matches, as opposed to relying on + * [matchingStacks] alone. `false` when this ingredient ignores extra stack data (like + * components/NBT) and matching is fully determined by item type. + */ + val requiresTesting: Boolean + + /** + * The serializer for this ingredient. Must have been registered via + * [IACustomIngredientSerializer.register]. + */ + val serializer: IACustomIngredientSerializer<*> + + /** Converts this custom ingredient into a vanilla [Ingredient] behaving the same way. */ + @get:ApiStatus.NonExtendable + val vanilla: Ingredient + get() + { + return ACustomIngredientPlatform.vanillaOf(this) + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientHolder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientHolder.kt new file mode 100644 index 000000000..8a36a7eb3 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientHolder.kt @@ -0,0 +1,11 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +/** + * Implemented by the vanilla `Ingredient` produced from [IACustomIngredient.vanilla], exposing + * the wrapped [custom] ingredient so it can be recovered from a vanilla `Ingredient` reference. + */ +interface IACustomIngredientHolder +{ + /** The custom ingredient this vanilla `Ingredient` was converted from. */ + val custom: T +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientSerializer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientSerializer.kt new file mode 100644 index 000000000..6f41b2283 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientSerializer.kt @@ -0,0 +1,43 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +import com.mojang.serialization.MapCodec +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.crafting.Ingredient + + +/** + * Serializer for an [IACustomIngredient] of type [T]. + * + * All instances must be registered using [register] for deserialization to work. + */ +interface IACustomIngredientSerializer +{ + /** The id this serializer is registered under; used to identify it in recipe JSON. */ + val identifier: ResourceLocation + + /** + * The codec used to read the ingredient from recipe JSON files. + * + * @param allowEmpty Whether an ingredient matching no items should be accepted, mirroring + * [Ingredient.CODEC] vs `Ingredient.CODEC_NONEMPTY`. + */ + fun getCodec(allowEmpty: Boolean): MapCodec + + /** The codec used to sync the ingredient to the client over the network. */ + val packetCodec: StreamCodec + + companion object + { + /** + * Registers [serializer] under its [identifier][IACustomIngredientSerializer.identifier]. + * + * @throws IllegalArgumentException if a serializer is already registered under that identifier + */ + fun register(serializer: IACustomIngredientSerializer) + { + return ACustomIngredientSerializerPlatform.register(serializer) + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt new file mode 100644 index 000000000..af825addb --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt @@ -0,0 +1,1090 @@ +package net.kernelpanicsoft.archie.data.common.tags + +import net.minecraft.core.Registry +import net.minecraft.core.registries.Registries +import net.minecraft.resources.ResourceKey +import net.minecraft.resources.ResourceLocation +import net.minecraft.tags.ItemTags +import net.minecraft.tags.TagKey +import net.minecraft.world.entity.EntityType +import net.minecraft.world.item.DyeColor +import net.minecraft.world.item.Item +import net.minecraft.world.level.biome.Biome +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.state.BlockBehaviour +import net.minecraft.world.level.material.Fluid + +/** + * Constants for the `c` (common) convention tags shared across the modding ecosystem, grouped by + * registry ([Blocks], [Items], [Fluids], [EntityTypes], [Biomes]). Each constant is a [TagKey] + * that can be used directly when building recipes/tags; entries not documented inline are + * self-explanatory from their name. + */ +@Suppress("unused") +object ACommonTags +{ + /** Registers every group's tags (currently a no-op per group; tag keys are created eagerly as constants). */ + fun init() + { + Blocks.init() + EntityTypes.init() + Items.init() + Fluids.init() + Biomes.init() + } + + /** Base for a group of [TagKey] constants in a single [registry], tracked in [tags] for lookup by id. */ + abstract class Tags private constructor( + private val registry: ResourceKey>, + private val tags: MutableMap> + ) : Map> by tags + { + constructor(registry: ResourceKey>) : this(registry, mutableMapOf()) + + /** Creates (and tracks) a `c:` tag key. */ + protected fun tag(name: String): TagKey = tag("c", name) + + /** Creates (and tracks) a `:` tag key. */ + protected fun tag(namespace: String, name: String): TagKey + { + return TagKey.create(registry, ResourceLocation.fromNamespaceAndPath(namespace, name)).also { tags[it.location] = it } + } + + /** Tracks a pre-existing [tag] key (e.g. a vanilla tag) alongside this group's own tags. */ + protected fun existing(tag: TagKey): TagKey + { + return tag.also { tags[it.location] = it } + } + + } + + object Blocks : Tags(Registries.BLOCK) + { + internal fun init() + { + } + + val ENDERMAN_PLACE_ON_BLACKLIST: TagKey = tag("neoforge", "enderman_place_on_blacklist") + val NEEDS_WOOD_TOOL: TagKey = tag("neoforge", "needs_wood_tool") + val NEEDS_GOLD_TOOL: TagKey = tag("neoforge", "needs_gold_tool") + val NEEDS_NETHERITE_TOOL: TagKey = tag("neoforge", "needs_netherite_tool") + + // `c` tags for common conventions + val BARRELS: TagKey = tag("barrels") + val BARRELS_WOODEN: TagKey = tag("barrels/wooden") + val BOOKSHELVES: TagKey = tag("bookshelves") + + /** + * For blocks that are similar to amethyst where their budding block produces buds and cluster blocks + */ + val BUDDING_BLOCKS: TagKey = tag("budding_blocks") + + /** + * For blocks that are similar to amethyst where they have buddings forming from budding blocks + */ + val BUDS: TagKey = tag("buds") + val CHAINS: TagKey = tag("chains") + val CHESTS: TagKey = tag("chests") + val CHESTS_ENDER: TagKey = tag("chests/ender") + val CHESTS_TRAPPED: TagKey = tag("chests/trapped") + val CHESTS_WOODEN: TagKey = tag("chests/wooden") + + /** + * For blocks that are similar to amethyst where they have clusters forming from budding blocks + */ + val CLUSTERS: TagKey = tag("clusters") + val COBBLESTONES: TagKey = tag("cobblestones") + val COBBLESTONES_NORMAL: TagKey = tag("cobblestones/normal") + val COBBLESTONES_INFESTED: TagKey = tag("cobblestones/infested") + val COBBLESTONES_MOSSY: TagKey = tag("cobblestones/mossy") + val COBBLESTONES_DEEPSLATE: TagKey = tag("cobblestones/deepslate") + + /** + * Tag that holds all blocks that can be dyed a specific color. + * (Does not include color blending blocks that would behave similar to leather armor item) + */ + val DYED: TagKey = tag("dyed") + val DYED_BLACK: TagKey = tag("dyed/black") + val DYED_BLUE: TagKey = tag("dyed/blue") + val DYED_BROWN: TagKey = tag("dyed/brown") + val DYED_CYAN: TagKey = tag("dyed/cyan") + val DYED_GRAY: TagKey = tag("dyed/gray") + val DYED_GREEN: TagKey = tag("dyed/green") + val DYED_LIGHT_BLUE: TagKey = tag("dyed/light_blue") + val DYED_LIGHT_GRAY: TagKey = tag("dyed/light_gray") + val DYED_LIME: TagKey = tag("dyed/lime") + val DYED_MAGENTA: TagKey = tag("dyed/magenta") + val DYED_ORANGE: TagKey = tag("dyed/orange") + val DYED_PINK: TagKey = tag("dyed/pink") + val DYED_PURPLE: TagKey = tag("dyed/purple") + val DYED_RED: TagKey = tag("dyed/red") + val DYED_WHITE: TagKey = tag("dyed/white") + val DYED_YELLOW: TagKey = tag("dyed/yellow") + val END_STONES: TagKey = tag("end_stones") + val FENCE_GATES: TagKey = tag("fence_gates") + val FENCE_GATES_WOODEN: TagKey = tag("fence_gates/wooden") + val FENCES: TagKey = tag("fences") + val FENCES_NETHER_BRICK: TagKey = tag("fences/nether_brick") + val FENCES_WOODEN: TagKey = tag("fences/wooden") + + val GLASS_BLOCKS: TagKey = tag("glass_blocks") + val GLASS_BLOCKS_COLORLESS: TagKey = tag("glass_blocks/colorless") + + /** + * Glass which is made from cheap resources like sand and only minor additional ingredients like dyes + */ + val GLASS_BLOCKS_CHEAP: TagKey = tag("glass_blocks/cheap") + val GLASS_BLOCKS_STAINED: TagKey = tag("glass_blocks/stained") + val GLASS_BLOCKS_TINTED: TagKey = tag("glass_blocks/tinted") + + val GLASS_PANES: TagKey = tag("glass_panes") + val GLASS_PANES_COLORLESS: TagKey = tag("glass_panes/colorless") + val GLASS_PANES_STAINED: TagKey = tag("glass_panes/stained") + + val GRAVELS: TagKey = tag("gravels") + + /** + * Tag that holds all blocks that recipe viewers should not show to users. + * Recipe viewers may use this to automatically find the corresponding BlockItem to hide. + */ + val HIDDEN_FROM_RECIPE_VIEWERS: TagKey = tag("hidden_from_recipe_viewers") + val NETHERRACKS: TagKey = tag("netherrack") + val OBSIDIANS: TagKey = tag("obsidians") + + /** + * Blocks which are often replaced by deepslate ores, i.e. the ores in the tag [.ORES_IN_GROUND_DEEPSLATE], during world generation + */ + val ORE_BEARING_GROUND_DEEPSLATE: TagKey = tag("ore_bearing_ground/deepslate") + + /** + * Blocks which are often replaced by netherrack ores, i.e. the ores in the tag [.ORES_IN_GROUND_NETHERRACK], during world generation + */ + val ORE_BEARING_GROUND_NETHERRACK: TagKey = tag("ore_bearing_ground/netherrack") + + /** + * Blocks which are often replaced by stone ores, i.e. the ores in the tag [.ORES_IN_GROUND_STONE], during world generation + */ + val ORE_BEARING_GROUND_STONE: TagKey = tag("ore_bearing_ground/stone") + + /** + * Ores which on average result in more than one resource worth of materials + */ + val ORE_RATES_DENSE: TagKey = tag("ore_rates/dense") + + /** + * Ores which on average result in one resource worth of materials + */ + val ORE_RATES_SINGULAR: TagKey = tag("ore_rates/singular") + + /** + * Ores which on average result in less than one resource worth of materials + */ + val ORE_RATES_SPARSE: TagKey = tag("ore_rates/sparse") + val ORES: TagKey = tag("ores") + val ORES_COAL: TagKey = tag("ores/coal") + val ORES_COPPER: TagKey = tag("ores/copper") + val ORES_DIAMOND: TagKey = tag("ores/diamond") + val ORES_EMERALD: TagKey = tag("ores/emerald") + val ORES_GOLD: TagKey = tag("ores/gold") + val ORES_IRON: TagKey = tag("ores/iron") + val ORES_LAPIS: TagKey = tag("ores/lapis") + val ORES_NETHERITE_SCRAP: TagKey = tag("ores/netherite_scrap") + val ORES_QUARTZ: TagKey = tag("ores/quartz") + val ORES_REDSTONE: TagKey = tag("ores/redstone") + + /** + * Ores in deepslate (or in equivalent blocks in the tag [.ORE_BEARING_GROUND_DEEPSLATE]) which could logically use deepslate as recipe input or output + */ + val ORES_IN_GROUND_DEEPSLATE: TagKey = tag("ores_in_ground/deepslate") + + /** + * Ores in netherrack (or in equivalent blocks in the tag [.ORE_BEARING_GROUND_NETHERRACK]) which could logically use netherrack as recipe input or output + */ + val ORES_IN_GROUND_NETHERRACK: TagKey = tag("ores_in_ground/netherrack") + + /** + * Ores in stone (or in equivalent blocks in the tag [.ORE_BEARING_GROUND_STONE]) which could logically use stone as recipe input or output + */ + val ORES_IN_GROUND_STONE: TagKey = tag("ores_in_ground/stone") + val PLAYER_WORKSTATIONS_CRAFTING_TABLES: TagKey = tag("player_workstations/crafting_tables") + val PLAYER_WORKSTATIONS_FURNACES: TagKey = tag("player_workstations/furnaces") + + /** + * Blocks should be included in this tag if their movement/relocation can cause serious issues such + * as world corruption upon being moved or for balance reason where the block should not be able to be relocated. + * Example: Chunk loaders or pipes where other mods that move blocks do not respect + * [BlockBehaviour.BlockStateBase.getPistonPushReaction]. + */ + val RELOCATION_NOT_SUPPORTED: TagKey = tag("relocation_not_supported") + val ROPES: TagKey = tag("ropes") + + val SANDS: TagKey = tag("sands") + val SANDS_COLORLESS: TagKey = tag("sands/colorless") + val SANDS_RED: TagKey = tag("sands/red") + + val SANDSTONE_BLOCKS: TagKey = tag("sandstone/blocks") + val SANDSTONE_SLABS: TagKey = tag("sandstone/slabs") + val SANDSTONE_STAIRS: TagKey = tag("sandstone/stairs") + val SANDSTONE_RED_BLOCKS: TagKey = tag("sandstone/red_blocks") + val SANDSTONE_RED_SLABS: TagKey = tag("sandstone/red_slabs") + val SANDSTONE_RED_STAIRS: TagKey = tag("sandstone/red_stairs") + val SANDSTONE_UNCOLORED_BLOCKS: TagKey = tag("sandstone/uncolored_blocks") + val SANDSTONE_UNCOLORED_SLABS: TagKey = tag("sandstone/uncolored_slabs") + val SANDSTONE_UNCOLORED_STAIRS: TagKey = tag("sandstone/uncolored_stairs") + + val SHULKER_BOXES: TagKey = tag("shulker_boxes") + + /** + * Tag that holds all head based blocks such as Skeleton Skull or Player Head. (Named skulls to match minecraft:skulls item tag) + */ + val SKULLS: TagKey = tag("skulls") + + /** + * Natural stone-like blocks that can be used as a base ingredient in recipes that takes stone. + */ + val STONES: TagKey = tag("stones") + + /** + * A storage block is generally a block that has a recipe to craft a bulk of 1 kind of resource to a block + * and has a mirror recipe to reverse the crafting with no loss in resources. + * + * + * Honey Block is special in that the reversing recipe is not a perfect mirror of the crafting recipe + * and so, it is considered a special case and not given a storage block tag. + */ + val STORAGE_BLOCKS: TagKey = tag("storage_blocks") + val STORAGE_BLOCKS_AMETHYST: TagKey = tag("storage_blocks/amethyst") + val STORAGE_BLOCKS_BONE_MEAL: TagKey = tag("storage_blocks/bone_meal") + val STORAGE_BLOCKS_COAL: TagKey = tag("storage_blocks/coal") + val STORAGE_BLOCKS_COPPER: TagKey = tag("storage_blocks/copper") + val STORAGE_BLOCKS_DIAMOND: TagKey = tag("storage_blocks/diamond") + val STORAGE_BLOCKS_DRIED_KELP: TagKey = tag("storage_blocks/dried_kelp") + val STORAGE_BLOCKS_EMERALD: TagKey = tag("storage_blocks/emerald") + val STORAGE_BLOCKS_GOLD: TagKey = tag("storage_blocks/gold") + val STORAGE_BLOCKS_IRON: TagKey = tag("storage_blocks/iron") + val STORAGE_BLOCKS_LAPIS: TagKey = tag("storage_blocks/lapis") + val STORAGE_BLOCKS_NETHERITE: TagKey = tag("storage_blocks/netherite") + val STORAGE_BLOCKS_QUARTZ: TagKey = tag("storage_blocks/quartz") + val STORAGE_BLOCKS_RAW_COPPER: TagKey = tag("storage_blocks/raw_copper") + val STORAGE_BLOCKS_RAW_GOLD: TagKey = tag("storage_blocks/raw_gold") + val STORAGE_BLOCKS_RAW_IRON: TagKey = tag("storage_blocks/raw_iron") + val STORAGE_BLOCKS_REDSTONE: TagKey = tag("storage_blocks/redstone") + val STORAGE_BLOCKS_SLIME: TagKey = tag("storage_blocks/slime") + val STORAGE_BLOCKS_WHEAT: TagKey = tag("storage_blocks/wheat") + val VILLAGER_JOB_SITES: TagKey = tag("villager_job_sites") + } + + object EntityTypes : Tags>(Registries.ENTITY_TYPE) + { + internal fun init() + { + } + + val BOSSES: TagKey> = tag("bosses") + val MINECARTS: TagKey> = tag("minecarts") + val BOATS: TagKey> = tag("boats") + + /** + * Entities should be included in this tag if they are not allowed to be picked up by items or grabbed in a way + * that a player can easily move the entity to anywhere they want. Ideal for special entities that should not + * be able to be put into a mob jar for example. + */ + val CAPTURING_NOT_SUPPORTED: TagKey> = tag("capturing_not_supported") + + /** + * Entities should be included in this tag if they are not allowed to be teleported in any way. + * This is more for mods that allow teleporting entities within the same dimension. Any mod that is + * teleporting entities to new dimensions should be checking canChangeDimensions method on the entity itself. + */ + val TELEPORTING_NOT_SUPPORTED: TagKey> = tag("teleporting_not_supported") + } + + object Items : Tags(Registries.ITEM) + { + internal fun init() + { + } + + + /** + * Controls what items can be consumed for enchanting such as Enchanting Tables. + * This tag defaults to [net.minecraft.world.item.Items.LAPIS_LAZULI] when not present in any datapacks, including forge client on vanilla server + */ + val ENCHANTING_FUELS: TagKey = tag("neoforge", "enchanting_fuels") + + + // `c` tags for common conventions + val BARRELS: TagKey = tag("barrels") + val BARRELS_WOODEN: TagKey = tag("barrels/wooden") + val BONES: TagKey = tag("bones") + val BOOKSHELVES: TagKey = tag("bookshelves") + val BRICKS: TagKey = tag("bricks") + val BRICKS_NORMAL: TagKey = tag("bricks/normal") + val BRICKS_NETHER: TagKey = tag("bricks/nether") + val BUCKETS: TagKey = tag("buckets") + val BUCKETS_EMPTY: TagKey = tag("buckets/empty") + + /** + * Does not include entity water buckets. + * If checking for the fluid this bucket holds in code, please use `net.neoforged.neoforge.fluids.capability.wrappers.FluidBucketWrapper.getFluid` instead. + */ + val BUCKETS_WATER: TagKey = tag("buckets/water") + + /** + * If checking for the fluid this bucket holds in code, please use `net.neoforged.neoforge.fluids.capability.wrappers.FluidBucketWrapper.getFluid` instead. + */ + val BUCKETS_LAVA: TagKey = tag("buckets/lava") + val BUCKETS_MILK: TagKey = tag("buckets/milk") + val BUCKETS_POWDER_SNOW: TagKey = tag("buckets/powder_snow") + val BUCKETS_ENTITY_WATER: TagKey = tag("buckets/entity_water") + + /** + * For blocks that are similar to amethyst where their budding block produces buds and cluster blocks + */ + val BUDDING_BLOCKS: TagKey = tag("budding_blocks") + + /** + * For blocks that are similar to amethyst where they have buddings forming from budding blocks + */ + val BUDS: TagKey = tag("buds") + val CHAINS: TagKey = tag("chains") + val CHESTS: TagKey = tag("chests") + val CHESTS_ENDER: TagKey = tag("chests/ender") + val CHESTS_TRAPPED: TagKey = tag("chests/trapped") + val CHESTS_WOODEN: TagKey = tag("chests/wooden") + val COBBLESTONES: TagKey = tag("cobblestones") + val COBBLESTONES_NORMAL: TagKey = tag("cobblestones/normal") + val COBBLESTONES_INFESTED: TagKey = tag("cobblestones/infested") + val COBBLESTONES_MOSSY: TagKey = tag("cobblestones/mossy") + val COBBLESTONES_DEEPSLATE: TagKey = tag("cobblestones/deepslate") + + /** + * For blocks that are similar to amethyst where they have clusters forming from budding blocks + */ + val CLUSTERS: TagKey = tag("clusters") + val CROPS: TagKey = tag("crops") + val CROPS_BEETROOT: TagKey = tag("crops/beetroot") + val CROPS_CARROT: TagKey = tag("crops/carrot") + val CROPS_NETHER_WART: TagKey = tag("crops/nether_wart") + val CROPS_POTATO: TagKey = tag("crops/potato") + val CROPS_WHEAT: TagKey = tag("crops/wheat") + val DUSTS: TagKey = tag("dusts") + val DUSTS_PRISMARINE: TagKey = tag("dusts/prismarine") + val DUSTS_REDSTONE: TagKey = tag("dusts/redstone") + val DUSTS_GLOWSTONE: TagKey = tag("dusts/glowstone") + + /** + * Tag that holds all blocks and items that can be dyed a specific color. + * (Does not include color blending items like leather armor + * Use [net.minecraft.tags.ItemTags.DYEABLE] tag instead for color blending items) + * + * + * Note: Use custom ingredients in recipes to do tag intersections and/or tag exclusions + * to make more powerful recipes utilizing multiple tags such as dyed tags for an ingredient. + * See `net.neoforged.neoforge.common.crafting.DifferenceIngredient` and `net.neoforged.neoforge.common.crafting.CompoundIngredient` + * for various custom ingredients available that can also be used in data generation. + */ + val DYED: TagKey = tag("dyed") + val DYED_BLACK: TagKey = tag("dyed/black") + val DYED_BLUE: TagKey = tag("dyed/blue") + val DYED_BROWN: TagKey = tag("dyed/brown") + val DYED_CYAN: TagKey = tag("dyed/cyan") + val DYED_GRAY: TagKey = tag("dyed/gray") + val DYED_GREEN: TagKey = tag("dyed/green") + val DYED_LIGHT_BLUE: TagKey = tag("dyed/light_blue") + val DYED_LIGHT_GRAY: TagKey = tag("dyed/light_gray") + val DYED_LIME: TagKey = tag("dyed/lime") + val DYED_MAGENTA: TagKey = tag("dyed/magenta") + val DYED_ORANGE: TagKey = tag("dyed/orange") + val DYED_PINK: TagKey = tag("dyed/pink") + val DYED_PURPLE: TagKey = tag("dyed/purple") + val DYED_RED: TagKey = tag("dyed/red") + val DYED_WHITE: TagKey = tag("dyed/white") + val DYED_YELLOW: TagKey = tag("dyed/yellow") + + val DYES: TagKey = tag("dyes") + val DYES_BLACK: TagKey = DyeColor.BLACK.tag + val DYES_RED: TagKey = DyeColor.RED.tag + val DYES_GREEN: TagKey = DyeColor.GREEN.tag + val DYES_BROWN: TagKey = DyeColor.BROWN.tag + val DYES_BLUE: TagKey = DyeColor.BLUE.tag + val DYES_PURPLE: TagKey = DyeColor.PURPLE.tag + val DYES_CYAN: TagKey = DyeColor.CYAN.tag + val DYES_LIGHT_GRAY: TagKey = DyeColor.LIGHT_GRAY.tag + val DYES_GRAY: TagKey = DyeColor.GRAY.tag + val DYES_PINK: TagKey = DyeColor.PINK.tag + val DYES_LIME: TagKey = DyeColor.LIME.tag + val DYES_YELLOW: TagKey = DyeColor.YELLOW.tag + val DYES_LIGHT_BLUE: TagKey = DyeColor.LIGHT_BLUE.tag + val DYES_MAGENTA: TagKey = DyeColor.MAGENTA.tag + val DYES_ORANGE: TagKey = DyeColor.ORANGE.tag + val DYES_WHITE: TagKey = DyeColor.WHITE.tag + + private val DyeColor.tag: TagKey + get() = tag("dyes/$name".lowercase()) + + + val EGGS: TagKey = tag("eggs") + val END_STONES: TagKey = tag("end_stones") + val ENDER_PEARLS: TagKey = tag("ender_pearls") + val FEATHERS: TagKey = tag("feathers") + val FENCE_GATES: TagKey = tag("fence_gates") + val FENCE_GATES_WOODEN: TagKey = tag("fence_gates/wooden") + val FENCES: TagKey = tag("fences") + val FENCES_NETHER_BRICK: TagKey = tag("fences/nether_brick") + val FENCES_WOODEN: TagKey = tag("fences/wooden") + val FOODS: TagKey = tag("foods") + + /** + * Apples and other foods that are considered fruits in the culinary field belong in this tag. + * Cherries would go here as they are considered a "stone fruit" within culinary fields. + */ + val FOODS_FRUITS: TagKey = tag("foods/fruits") + + /** + * Tomatoes and other foods that are considered vegetables in the culinary field belong in this tag. + */ + val FOODS_VEGETABLES: TagKey = tag("foods/vegetables") + + /** + * Strawberries, raspberries, and other berry foods belong in this tag. + * Cherries would NOT go here as they are considered a "stone fruit" within culinary fields. + */ + val FOODS_BERRIES: TagKey = tag("foods/berries") + val FOODS_BREADS: TagKey = tag("foods/breads") + val FOODS_COOKIES: TagKey = tag("foods/cookies") + val FOODS_RAW_MEATS: TagKey = tag("foods/raw_meats") + val FOODS_COOKED_MEATS: TagKey = tag("foods/cooked_meats") + val FOODS_RAW_FISHES: TagKey = tag("foods/raw_fishes") + val FOODS_COOKED_FISHES: TagKey = tag("foods/cooked_fishes") + + /** + * Soups, stews, and other liquid food in bowls belongs in this tag. + */ + val FOODS_SOUPS: TagKey = tag("foods/soups") + + /** + * Sweets and candies like lollipops or chocolate belong in this tag. + */ + val FOODS_CANDIES: TagKey = tag("foods/candies") + + /** + * Foods like cake that can be eaten when placed in the world belong in this tag. + */ + val FOODS_EDIBLE_WHEN_PLACED: TagKey = tag("foods/edible_when_placed") + + /** + * For foods that inflict food poisoning-like effects. + * Examples are Rotten Flesh's Hunger or Pufferfish's Nausea, or Poisonous Potato's Poison. + */ + val FOODS_FOOD_POISONING: TagKey = tag("foods/food_poisoning") + val GEMS: TagKey = tag("gems") + val GEMS_DIAMOND: TagKey = tag("gems/diamond") + val GEMS_EMERALD: TagKey = tag("gems/emerald") + val GEMS_AMETHYST: TagKey = tag("gems/amethyst") + val GEMS_LAPIS: TagKey = tag("gems/lapis") + val GEMS_PRISMARINE: TagKey = tag("gems/prismarine") + val GEMS_QUARTZ: TagKey = tag("gems/quartz") + + val GLASS_BLOCKS: TagKey = tag("glass_blocks") + val GLASS_BLOCKS_COLORLESS: TagKey = tag("glass_blocks/colorless") + + /** + * Glass which is made from cheap resources like sand and only minor additional ingredients like dyes + */ + val GLASS_BLOCKS_CHEAP: TagKey = tag("glass_blocks/cheap") + val GLASS_BLOCKS_STAINED: TagKey = tag("glass_blocks/stained") + val GLASS_BLOCKS_TINTED: TagKey = tag("glass_blocks/tinted") + + val GLASS_PANES: TagKey = tag("glass_panes") + val GLASS_PANES_COLORLESS: TagKey = tag("glass_panes/colorless") + val GLASS_PANES_STAINED: TagKey = tag("glass_panes/stained") + + val GRAVELS: TagKey = tag("gravel") + val GUNPOWDERS: TagKey = tag("gunpowder") + + /** + * Tag that holds all items that recipe viewers should not show to users. + */ + val HIDDEN_FROM_RECIPE_VIEWERS: TagKey = tag("hidden_from_recipe_viewers") + val INGOTS: TagKey = tag("ingots") + val INGOTS_COPPER: TagKey = tag("ingots/copper") + val INGOTS_GOLD: TagKey = tag("ingots/gold") + val INGOTS_IRON: TagKey = tag("ingots/iron") + val INGOTS_NETHERITE: TagKey = tag("ingots/netherite") + val LEATHERS: TagKey = tag("leather") + val MUSHROOMS: TagKey = tag("mushrooms") + val NETHER_STARS: TagKey = tag("nether_stars") + val NETHERRACKS: TagKey = tag("netherrack") + val NUGGETS: TagKey = tag("nuggets") + val NUGGETS_GOLD: TagKey = tag("nuggets/gold") + val NUGGETS_IRON: TagKey = tag("nuggets/iron") + val OBSIDIANS: TagKey = tag("obsidians") + + /** + * Blocks which are often replaced by deepslate ores, i.e. the ores in the tag [.ORES_IN_GROUND_DEEPSLATE], during world generation + */ + val ORE_BEARING_GROUND_DEEPSLATE: TagKey = tag("ore_bearing_ground/deepslate") + + /** + * Blocks which are often replaced by netherrack ores, i.e. the ores in the tag [.ORES_IN_GROUND_NETHERRACK], during world generation + */ + val ORE_BEARING_GROUND_NETHERRACK: TagKey = tag("ore_bearing_ground/netherrack") + + /** + * Blocks which are often replaced by stone ores, i.e. the ores in the tag [.ORES_IN_GROUND_STONE], during world generation + */ + val ORE_BEARING_GROUND_STONE: TagKey = tag("ore_bearing_ground/stone") + + /** + * Ores which on average result in more than one resource worth of materials + */ + val ORE_RATES_DENSE: TagKey = tag("ore_rates/dense") + + /** + * Ores which on average result in one resource worth of materials + */ + val ORE_RATES_SINGULAR: TagKey = tag("ore_rates/singular") + + /** + * Ores which on average result in less than one resource worth of materials + */ + val ORE_RATES_SPARSE: TagKey = tag("ore_rates/sparse") + val ORES: TagKey = tag("ores") + val ORES_COAL: TagKey = tag("ores/coal") + val ORES_COPPER: TagKey = tag("ores/copper") + val ORES_DIAMOND: TagKey = tag("ores/diamond") + val ORES_EMERALD: TagKey = tag("ores/emerald") + val ORES_GOLD: TagKey = tag("ores/gold") + val ORES_IRON: TagKey = tag("ores/iron") + val ORES_LAPIS: TagKey = tag("ores/lapis") + val ORES_NETHERITE_SCRAP: TagKey = tag("ores/netherite_scrap") + val ORES_QUARTZ: TagKey = tag("ores/quartz") + val ORES_REDSTONE: TagKey = tag("ores/redstone") + + /** + * Ores in deepslate (or in equivalent blocks in the tag [.ORE_BEARING_GROUND_DEEPSLATE]) which could logically use deepslate as recipe input or output + */ + val ORES_IN_GROUND_DEEPSLATE: TagKey = tag("ores_in_ground/deepslate") + + /** + * Ores in netherrack (or in equivalent blocks in the tag [.ORE_BEARING_GROUND_NETHERRACK]) which could logically use netherrack as recipe input or output + */ + val ORES_IN_GROUND_NETHERRACK: TagKey = tag("ores_in_ground/netherrack") + + /** + * Ores in stone (or in equivalent blocks in the tag [.ORE_BEARING_GROUND_STONE]) which could logically use stone as recipe input or output + */ + val ORES_IN_GROUND_STONE: TagKey = tag("ores_in_ground/stone") + val PLAYER_WORKSTATIONS_CRAFTING_TABLES: TagKey = tag("player_workstations/crafting_tables") + val PLAYER_WORKSTATIONS_FURNACES: TagKey = tag("player_workstations/furnaces") + val RAW_BLOCKS: TagKey = tag("raw_blocks") + val RAW_BLOCKS_COPPER: TagKey = tag("raw_blocks/copper") + val RAW_BLOCKS_GOLD: TagKey = tag("raw_blocks/gold") + val RAW_BLOCKS_IRON: TagKey = tag("raw_blocks/iron") + val RAW_MATERIALS: TagKey = tag("raw_materials") + val RAW_MATERIALS_COPPER: TagKey = tag("raw_materials/copper") + val RAW_MATERIALS_GOLD: TagKey = tag("raw_materials/gold") + val RAW_MATERIALS_IRON: TagKey = tag("raw_materials/iron") + + /** + * For rod-like materials to be used in recipes. + */ + val RODS: TagKey = tag("rods") + val RODS_BLAZE: TagKey = tag("rods/blaze") + val RODS_BREEZE: TagKey = tag("rods/breeze") + + /** + * For stick-like materials to be used in recipes. + * One example is a mod adds stick variants such as Spruce Sticks but would like stick recipes to be able to use it. + */ + val RODS_WOODEN: TagKey = tag("rods/wooden") + val ROPES: TagKey = tag("ropes") + + val SANDS: TagKey = tag("sands") + val SANDS_COLORLESS: TagKey = tag("sands/colorless") + val SANDS_RED: TagKey = tag("sands/red") + + val SANDSTONE_BLOCKS: TagKey = tag("sandstone/blocks") + val SANDSTONE_SLABS: TagKey = tag("sandstone/slabs") + val SANDSTONE_STAIRS: TagKey = tag("sandstone/stairs") + val SANDSTONE_RED_BLOCKS: TagKey = tag("sandstone/red_blocks") + val SANDSTONE_RED_SLABS: TagKey = tag("sandstone/red_slabs") + val SANDSTONE_RED_STAIRS: TagKey = tag("sandstone/red_stairs") + val SANDSTONE_UNCOLORED_BLOCKS: TagKey = tag("sandstone/uncolored_blocks") + val SANDSTONE_UNCOLORED_SLABS: TagKey = tag("sandstone/uncolored_slabs") + val SANDSTONE_UNCOLORED_STAIRS: TagKey = tag("sandstone/uncolored_stairs") + + val SEEDS: TagKey = tag("seeds") + val SEEDS_BEETROOT: TagKey = tag("seeds/beetroot") + val SEEDS_MELON: TagKey = tag("seeds/melon") + val SEEDS_PUMPKIN: TagKey = tag("seeds/pumpkin") + val SEEDS_WHEAT: TagKey = tag("seeds/wheat") + val SHULKER_BOXES: TagKey = tag("shulker_boxes") + val SLIMEBALLS: TagKey = tag("slimeballs") + + /** + * Natural stone-like blocks that can be used as a base ingredient in recipes that takes stone. + */ + val STONES: TagKey = tag("stones") + + val SKULLS: TagKey = existing(ItemTags.SKULLS) + + /** + * A storage block is generally a block that has a recipe to craft a bulk of 1 kind of resource to a block + * and has a mirror recipe to reverse the crafting with no loss in resources. + * + * + * Honey Block is special in that the reversing recipe is not a perfect mirror of the crafting recipe + * and so, it is considered a special case and not given a storage block tag. + */ + val STORAGE_BLOCKS: TagKey = tag("storage_blocks") + val STORAGE_BLOCKS_AMETHYST: TagKey = tag("storage_blocks/amethyst") + val STORAGE_BLOCKS_BONE_MEAL: TagKey = tag("storage_blocks/bone_meal") + val STORAGE_BLOCKS_COAL: TagKey = tag("storage_blocks/coal") + val STORAGE_BLOCKS_COPPER: TagKey = tag("storage_blocks/copper") + val STORAGE_BLOCKS_DIAMOND: TagKey = tag("storage_blocks/diamond") + val STORAGE_BLOCKS_DRIED_KELP: TagKey = tag("storage_blocks/dried_kelp") + val STORAGE_BLOCKS_EMERALD: TagKey = tag("storage_blocks/emerald") + val STORAGE_BLOCKS_GOLD: TagKey = tag("storage_blocks/gold") + val STORAGE_BLOCKS_IRON: TagKey = tag("storage_blocks/iron") + val STORAGE_BLOCKS_LAPIS: TagKey = tag("storage_blocks/lapis") + val STORAGE_BLOCKS_NETHERITE: TagKey = tag("storage_blocks/netherite") + val STORAGE_BLOCKS_QUARTZ: TagKey = tag("storage_blocks/quartz") + val STORAGE_BLOCKS_RAW_COPPER: TagKey = tag("storage_blocks/raw_copper") + val STORAGE_BLOCKS_RAW_GOLD: TagKey = tag("storage_blocks/raw_gold") + val STORAGE_BLOCKS_RAW_IRON: TagKey = tag("storage_blocks/raw_iron") + val STORAGE_BLOCKS_REDSTONE: TagKey = tag("storage_blocks/redstone") + val STORAGE_BLOCKS_SLIME: TagKey = tag("storage_blocks/slime") + val STORAGE_BLOCKS_WHEAT: TagKey = tag("storage_blocks/wheat") + val STRINGS: TagKey = tag("strings") + val VILLAGER_JOB_SITES: TagKey = tag("villager_job_sites") + + + // Tools and Armors + /** + * A tag containing all existing tools. Do not use this tag for determining a tool's behavior. + * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. + */ + val TOOLS: TagKey = tag("tools") + + /** + * A tag containing all existing axes. Do not use this tag for determining a tool's behavior. + * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. + */ + val TOOLS_AXES: TagKey = existing(ItemTags.AXES) + + /** + * A tag containing all existing hoes. Do not use this tag for determining a tool's behavior. + * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. + */ + val TOOLS_HOES: TagKey = existing(ItemTags.HOES) + + /** + * A tag containing all existing pickaxes. Do not use this tag for determining a tool's behavior. + * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. + */ + val TOOLS_PICKAXES: TagKey = existing(ItemTags.PICKAXES) + + /** + * A tag containing all existing shovels. Do not use this tag for determining a tool's behavior. + * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. + */ + val TOOLS_SHOVELS: TagKey = existing(ItemTags.SHOVELS) + + /** + * A tag containing all existing swords. Do not use this tag for determining a tool's behavior. + * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. + */ + val TOOLS_SWORDS: TagKey = existing(ItemTags.SWORDS) + + /** + * A tag containing all existing shields. Do not use this tag for determining a tool's behavior. + * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. + */ + val TOOLS_SHIELDS: TagKey = tag("tools/shields") + + /** + * A tag containing all existing bows. Do not use this tag for determining a tool's behavior. + * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. + */ + val TOOLS_BOWS: TagKey = tag("tools/bows") + + /** + * A tag containing all existing crossbows. Do not use this tag for determining a tool's behavior. + * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. + */ + val TOOLS_CROSSBOWS: TagKey = tag("tools/crossbows") + + /** + * A tag containing all existing fishing rods. Do not use this tag for determining a tool's behavior. + * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. + */ + val TOOLS_FISHING_RODS: TagKey = tag("tools/fishing_rods") + + /** + * A tag containing all existing spears. Other tools such as throwing knives or boomerangs + * should not be put into this tag and should be put into their own tool tags. + * Do not use this tag for determining a tool's behavior. + * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. + */ + val TOOLS_SPEARS: TagKey = tag("tools/spears") + + /** + * A tag containing all existing shears. Do not use this tag for determining a tool's behavior. + * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. + */ + val TOOLS_SHEARS: TagKey = tag("tools/shears") + + /** + * A tag containing all existing brushes. Do not use this tag for determining a tool's behavior. + * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. + */ + val TOOLS_BRUSHES: TagKey = tag("tools/brushes") + + /** + * Collects the 4 vanilla armor tags into one parent collection for ease. + */ + val ARMORS: TagKey = tag("armors") + + /** + * A tag containing all existing helmets. + */ + val ARMORS_HELMETS: TagKey = existing(ItemTags.HEAD_ARMOR) + + /** + * A tag containing all chestplates. + */ + val ARMORS_CHESTPLATES: TagKey = existing(ItemTags.CHEST_ARMOR) + + /** + * A tag containing all existing leggings. + */ + val ARMORS_LEGGINGS: TagKey = existing(ItemTags.LEG_ARMOR) + + /** + * A tag containing all existing boots. + */ + val ARMORS_BOOTS: TagKey = existing(ItemTags.FOOT_ARMOR) + + /** + * Collects the many enchantable tags into one parent collection for ease. + */ + val ENCHANTABLES: TagKey = tag("enchantables") + + } + + object Fluids : Tags(Registries.FLUID) + { + internal fun init() + { + } + + /** + * Holds all fluids related to water. + * This tag is done to help out multi-loader mods/datapacks where the vanilla water tag has attached behaviors outside Neo. + */ + val WATER: TagKey = tag("water") + + /** + * Holds all fluids related to lava. + * This tag is done to help out multi-loader mods/datapacks where the vanilla lava tag has attached behaviors outside Neo. + */ + val LAVA: TagKey = tag("lava") + + /** + * Holds all fluids related to milk. + */ + val MILK: TagKey = tag("milk") + + /** + * Holds all fluids that are gaseous at room temperature. + */ + val GASEOUS: TagKey = tag("gaseous") + + /** + * Holds all fluids related to honey.

+ * (Standard unit for honey bottle is 250mb per bottle) + */ + val HONEY: TagKey = tag("honey") + + /** + * Holds all fluids related to potions. The effects of the potion fluid should be read from NBT. + * The effects and color of the potion fluid should be read from [net.minecraft.core.component.DataComponents.POTION_CONTENTS] + * component that people should be attaching to the fluidstack of this fluid.

+ * (Standard unit for potions is 250mb per bottle) + */ + val POTION: TagKey = tag("potion") + + /** + * Holds all fluids related to Suspicious Stew. + * The effects of the suspicious stew fluid should be read from [net.minecraft.core.component.DataComponents.SUSPICIOUS_STEW_EFFECTS] + * component that people should be attaching to the fluidstack of this fluid.

+ * (Standard unit for suspicious stew is 250mb per bowl) + */ + val SUSPICIOUS_STEW: TagKey = tag("suspicious_stew") + + /** + * Holds all fluids related to Mushroom Stew.

+ * (Standard unit for mushroom stew is 250mb per bowl) + */ + val MUSHROOM_STEW: TagKey = tag("mushroom_stew") + + /** + * Holds all fluids related to Rabbit Stew.

+ * (Standard unit for rabbit stew is 250mb per bowl) + */ + val RABBIT_STEW: TagKey = tag("rabbit_stew") + + /** + * Holds all fluids related to Beetroot Soup.

+ * (Standard unit for beetroot soup is 250mb per bowl) + */ + val BEETROOT_SOUP: TagKey = tag("beetroot_soup") + + /** + * Tag that holds all fluids that recipe viewers should not show to users. + */ + val HIDDEN_FROM_RECIPE_VIEWERS: TagKey = tag("hidden_from_recipe_viewers") + } + + object Biomes : Tags(Registries.BIOME) + { + internal fun init() + { + } + + /** + * For biomes that should not spawn monsters over time the normal way. + * In other words, their Spawners and Spawn Cost entries have the monster category empty. + * Example: Mushroom Biomes not having Zombies, Creepers, Skeleton, nor any other normal monsters. + */ + val NO_DEFAULT_MONSTERS: TagKey = tag("no_default_monsters") + + /** + * Biomes that should not be locatable/selectable by modded biome-locating items or abilities. + */ + val HIDDEN_FROM_LOCATOR_SELECTION: TagKey = tag("hidden_from_locator_selection") + + val IS_VOID: TagKey = tag("is_void") + + val IS_HOT: TagKey = tag("is_hot") + val IS_HOT_OVERWORLD: TagKey = tag("is_hot/overworld") + val IS_HOT_NETHER: TagKey = tag("is_hot/nether") + val IS_HOT_END: TagKey = tag("is_hot/end") + + val IS_COLD: TagKey = tag("is_cold") + val IS_COLD_OVERWORLD: TagKey = tag("is_cold/overworld") + val IS_COLD_NETHER: TagKey = tag("is_cold/nether") + val IS_COLD_END: TagKey = tag("is_cold/end") + + val IS_SPARSE_VEGETATION: TagKey = tag("is_sparse_vegetation") + val IS_SPARSE_VEGETATION_OVERWORLD: TagKey = tag("is_sparse_vegetation/overworld") + val IS_SPARSE_VEGETATION_NETHER: TagKey = tag("is_sparse_vegetation/nether") + val IS_SPARSE_VEGETATION_END: TagKey = tag("is_sparse_vegetation/end") + val IS_DENSE_VEGETATION: TagKey = tag("is_dense_vegetation") + val IS_DENSE_VEGETATION_OVERWORLD: TagKey = tag("is_dense_vegetation/overworld") + val IS_DENSE_VEGETATION_NETHER: TagKey = tag("is_dense_vegetation/nether") + val IS_DENSE_VEGETATION_END: TagKey = tag("is_dense_vegetation/end") + + val IS_WET: TagKey = tag("is_wet") + val IS_WET_OVERWORLD: TagKey = tag("is_wet/overworld") + val IS_WET_NETHER: TagKey = tag("is_wet/nether") + val IS_WET_END: TagKey = tag("is_wet/end") + val IS_DRY: TagKey = tag("is_dry") + val IS_DRY_OVERWORLD: TagKey = tag("is_dry/overworld") + val IS_DRY_NETHER: TagKey = tag("is_dry/nether") + val IS_DRY_END: TagKey = tag("is_dry/end") + + /** + * Biomes that spawn in the Overworld. + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_OVERWORLD] + * + * + * NOTE: If you do not add to the vanilla Overworld tag, be sure to add to + * [net.minecraft.tags.BiomeTags.HAS_STRONGHOLD] so some Strongholds do not go missing.) + */ + val IS_OVERWORLD: TagKey = tag("is_overworld") + + val IS_CONIFEROUS_TREE: TagKey = tag("is_tree/coniferous") + val IS_SAVANNA_TREE: TagKey = tag("is_tree/savanna") + val IS_JUNGLE_TREE: TagKey = tag("is_tree/jungle") + val IS_DECIDUOUS_TREE: TagKey = tag("is_tree/deciduous") + + /** + * Biomes that spawn as part of giant mountains. + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_MOUNTAIN]) + */ + val IS_MOUNTAIN: TagKey = tag("is_mountain") + val IS_MOUNTAIN_PEAK: TagKey = tag("is_mountain/peak") + val IS_MOUNTAIN_SLOPE: TagKey = tag("is_mountain/slope") + + /** + * For temperate or warmer plains-like biomes. + * For snowy plains-like biomes, see [.IS_SNOWY_PLAINS]. + */ + val IS_PLAINS: TagKey = tag("is_plains") + + /** + * For snowy plains-like biomes. + * For warmer plains-like biomes, see [.IS_PLAINS]. + */ + val IS_SNOWY_PLAINS: TagKey = tag("is_snowy_plains") + + /** + * Biomes densely populated with deciduous trees. + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_FOREST]) + */ + val IS_FOREST: TagKey = tag("is_forest") + val IS_BIRCH_FOREST: TagKey = tag("is_birch_forest") + val IS_FLOWER_FOREST: TagKey = tag("is_flower_forest") + + /** + * Biomes that spawn as a taiga. + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_TAIGA]) + */ + val IS_TAIGA: TagKey = tag("is_taiga") + val IS_OLD_GROWTH: TagKey = tag("is_old_growth") + + /** + * Biomes that spawn as a hills biome. (Previously was called Extreme Hills biome in past) + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_HILL]) + */ + val IS_HILL: TagKey = tag("is_hill") + val IS_WINDSWEPT: TagKey = tag("is_windswept") + + /** + * Biomes that spawn as a jungle. + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_JUNGLE]) + */ + val IS_JUNGLE: TagKey = tag("is_jungle") + + /** + * Biomes that spawn as a savanna. + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_SAVANNA]) + */ + val IS_SAVANNA: TagKey = tag("is_savanna") + val IS_SWAMP: TagKey = tag("is_swamp") + val IS_DESERT: TagKey = tag("is_desert") + + /** + * Biomes that spawn as a badlands. + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_BADLANDS]) + */ + val IS_BADLANDS: TagKey = tag("is_badlands") + + /** + * Biomes that are dedicated to spawning on the shoreline of a body of water. + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_BEACH]) + */ + val IS_BEACH: TagKey = tag("is_beach") + val IS_STONY_SHORES: TagKey = tag("is_stony_shores") + val IS_MUSHROOM: TagKey = tag("is_mushroom") + + /** + * Biomes that spawn as a river. + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_RIVER]) + */ + val IS_RIVER: TagKey = tag("is_river") + + /** + * Biomes that spawn as part of the world's oceans. + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_OCEAN]) + */ + val IS_OCEAN: TagKey = tag("is_ocean") + + /** + * Biomes that spawn as part of the world's oceans that have low depth. + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_DEEP_OCEAN]) + */ + val IS_DEEP_OCEAN: TagKey = tag("is_deep_ocean") + val IS_SHALLOW_OCEAN: TagKey = tag("is_shallow_ocean") + + val IS_UNDERGROUND: TagKey = tag("is_underground") + val IS_CAVE: TagKey = tag("is_cave") + + val IS_LUSH: TagKey = tag("is_lush") + val IS_MAGICAL: TagKey = tag("is_magical") + val IS_RARE: TagKey = tag("is_rare") + val IS_PLATEAU: TagKey = tag("is_plateau") + val IS_MODIFIED: TagKey = tag("is_modified") + val IS_SPOOKY: TagKey = tag("is_spooky") + + /** + * Biomes that lack any natural life or vegetation. + * (Example, land destroyed and sterilized by nuclear weapons) + */ + val IS_WASTELAND: TagKey = tag("is_wasteland") + + /** + * Biomes whose flora primarily consists of dead or decaying vegetation. + */ + val IS_DEAD: TagKey = tag("is_dead") + + /** + * Biomes with a large amount of flowers. + */ + val IS_FLORAL: TagKey = tag("is_floral") + + /** + * Biomes that are able to spawn sand-based blocks on the surface. + */ + val IS_SANDY: TagKey = tag("is_sandy") + + /** + * For biomes that contains lots of naturally spawned snow. + * For biomes where lot of ice is present, see [IS_ICY]. + * Biome with lots of both snow and ice may be in both tags. + */ + val IS_SNOWY: TagKey = tag("is_snowy") + + /** + * For land biomes where ice naturally spawns. + * For biomes where snow alone spawns, see [IS_SNOWY]. + */ + val IS_ICY: TagKey = tag("is_icy") + + /** + * Biomes consisting primarily of water. + */ + val IS_AQUATIC: TagKey = tag("is_aquatic") + + /** + * For water biomes where ice naturally spawns. + * For biomes where snow alone spawns, see [IS_SNOWY]. + */ + val IS_AQUATIC_ICY: TagKey = tag("is_aquatic_icy") + + /** + * Biomes that spawn in the Nether. + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_NETHER]) + */ + val IS_NETHER: TagKey = tag("is_nether") + val IS_NETHER_FOREST: TagKey = tag("is_nether_forest") + + /** + * Biomes that spawn in the End. + * (This is for people who want to tag their biomes without getting + * side effects from [net.minecraft.tags.BiomeTags.IS_END]) + */ + val IS_END: TagKey = tag("is_end") + + /** + * Biomes that spawn as part of the large islands outside the center island in The End dimension. + */ + val IS_OUTER_END_ISLAND: TagKey = tag("is_outer_end_island") + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt new file mode 100644 index 000000000..6556c99e8 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt @@ -0,0 +1,25 @@ +package net.kernelpanicsoft.archie.events + +import dev.architectury.event.Event + +/** + * A simpler alternative to [AEventObject] for wrapping an Architectury [event] that isn't + * scoped to a particular [dev.architectury.platform.Mod] and doesn't need a + * [AEvents.HandlerConstructor]. + * + * @param T The Architectury handler/listener type expected by [event]. + */ +abstract class ABasicEventObject() +{ + /** The underlying Architectury event this wrapper registers [handler] with. */ + abstract val event: Event + + /** The listener registered with [event] by [init]. */ + abstract val handler: T + + /** Registers [handler] with [event]. Not idempotent; call once during initialization. */ + fun init() + { + event.register(handler) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt new file mode 100644 index 000000000..5ae408e7f --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt @@ -0,0 +1,47 @@ +package net.kernelpanicsoft.archie.events + +import dev.architectury.event.Event +import dev.architectury.platform.Mod + +/** + * Base class for Archie's mod-scoped Architectury event wrappers, such as + * [AEvents.GatherDataHandler] and [AEvents.RegisterGameTestHandler]. + * + * Subclasses wire together an Architectury [event], the [handlerConstructor] that builds a + * [mod]-scoped [H] from a [T] callback, and the [handler] logic itself, then call [init] once + * (idempotently, thread-safely) to register with the underlying event. + * + * @param T The event payload/receiver type passed to [handler]. + * @param H The [AEvents.Handler] type produced for [mod]. + * @param C The [AEvents.HandlerConstructor] that builds an [H]. + * @param mod The [Mod] this event object is scoped to. + */ +abstract class AEventObject, C : AEvents.HandlerConstructor>(val mod: Mod) +{ + /** The underlying Architectury event this wrapper registers a handler with. */ + abstract val event: Event + + @Volatile + private var initialized: Boolean = false + + /** Builds a [mod]-scoped [H] from the [handler] callback. */ + abstract val handlerConstructor: C + + /** The callback invoked when [event] fires for [mod]. */ + abstract fun T.handler() + + /** + * Registers [handler] with [event] via [handlerConstructor]. Safe to call multiple times; + * only the first call has any effect. + */ + fun init() + { + if (initialized) return + synchronized(this) + { + if (initialized) return + event.register(handlerConstructor.create(mod) { handler() }) + initialized = true + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEvents.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEvents.kt new file mode 100644 index 000000000..2ca7be7da --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEvents.kt @@ -0,0 +1,185 @@ +package net.kernelpanicsoft.archie.events + +import net.kernelpanicsoft.archie.data.ADataGenerator +import net.kernelpanicsoft.archie.gametest.AGameTestPlatform +import dev.architectury.event.Event +import dev.architectury.event.EventFactory +import dev.architectury.event.EventResult +import dev.architectury.platform.Mod +import dev.architectury.platform.Platform +import dev.architectury.utils.Env +import net.kernelpanicsoft.archie.gametest.AGameTestSide + +/** + * Archie's central, mod-scoped event registry, built on top of Architectury's event system. + * + * A downstream mod opts in with `AEvents += MOD` (its own [Mod] descriptor), then listens for + * [GATHER_DATA] and/or [REGISTER_GAME_TEST] via the corresponding handler's + * [HandlerConstructor.create]. Handlers are mod-scoped: [GatherDataHandler] and + * [RegisterGameTestHandler] both check the invoking [Mod] and no-op (via + * [EventResult.pass]) for any mod other than the one they were created for. + */ +object AEvents +{ + /** Fired during datagen runs; handlers should gate by owning [Mod]. */ + val GATHER_DATA: Event = EventFactory.createEventResult() + + /** Fired during gametest registration runs; handlers should register test classes per [Mod]. */ + val REGISTER_GAME_TEST: Event = EventFactory.createEventResult() + + private val mods: MutableList = mutableListOf() + + /** Mods that opted into Archie event plumbing via `AEvents += MOD`. */ + val MODS: List + get() = mods + + fun register(mod: Mod) + { + mods.add(mod) + } + + operator fun plusAssign(mod: Mod) = register(mod) + + /** Marker for a mod-scoped Architectury event listener created by a [HandlerConstructor]. */ + interface Handler + + /** Builds a mod-scoped [H] whose body invokes `block` on the event's [T] payload. */ + fun interface HandlerConstructor> + { + /** Creates an [H] for [mod] that runs [block] against the [T] payload when invoked. */ + fun create(mod: Mod, block: T.() -> Unit): H + } + + /** + * Handler for [GATHER_DATA]. Implementations are produced via [HandlerConstructor.create] + * and forward to the registered `block` only when the firing [ADataGenerator.mod] matches + * the [Mod] the handler was created for. + */ + interface GatherDataHandler : Handler + { + operator fun invoke(dataGenerator: ADataGenerator): EventResult + + companion object : HandlerConstructor + { + override fun create(mod: Mod, block: ADataGenerator.() -> Unit): GatherDataHandler + { + return GatherDataHandlerImpl(mod, block) + } + + class GatherDataHandlerImpl internal constructor( + private val mod: Mod, + private val gatherData: ADataGenerator.() -> Unit + ) : + GatherDataHandler + { + override operator fun invoke(dataGenerator: ADataGenerator): EventResult + { + if (this.mod != dataGenerator.mod) + return EventResult.pass() + dataGenerator.gatherData() + return EventResult.interruptDefault() + } + } + } + } + + /** + * DSL receiver passed to [REGISTER_GAME_TEST] listeners for declaring gametest classes. + * + * Classes registered via [server]/[client] are only collected on the matching + * [AGameTestPlatform] [AGameTestSide], unless [all] is `true`; classes registered via + * [common] are always collected. Collected classes are handed off to + * [AGameTestPlatform.register]. + * + * @param all When `true`, [server] and [client] blocks are collected on both sides. + */ + class ArchieGameTestBuilder(private val all: Boolean = false) + { + /** All classes collected so far across [server], [client], and [common] blocks. */ + val classes: MutableList> = mutableListOf() + + /** Declares gametest classes that should only be registered on the server side. */ + fun server(block: Environment.Server.() -> Unit) + { + Environment.Server(all).apply(block).also { classes.addAll(it.classes) } + } + + /** Declares gametest classes that should only be registered on the client side. */ + fun client(block: Environment.Client.() -> Unit) + { + Environment.Client(all).apply(block).also { classes.addAll(it.classes) } + } + + /** Declares gametest classes that should always be registered, regardless of side. */ + fun common(block: Environment.Common.() -> Unit) + { + Environment.Common().apply(block).also { classes.addAll(it.classes) } + } + + /** Scopes [register] calls to classes that should be collected only when [predicate] holds. */ + sealed class Environment(private val predicate: () -> Boolean) + { + /** Classes registered in this environment scope. */ + val classes: MutableList> = mutableListOf() + class Server(all: Boolean) : Environment({ all || AGameTestPlatform.side == AGameTestSide.SERVER }) + class Client(all: Boolean) : Environment({ all || AGameTestPlatform.side == AGameTestSide.CLIENT }) + class Common : Environment({ true }) + + /** Adds [clazz] to [classes] if this environment's [predicate] currently holds. */ + fun register(clazz: Class) + { + if (!predicate()) return + classes.add(clazz) + } + + /** Reified convenience for [register] using [T]'s [Class]. */ + inline fun register() + { + register(T::class.java) + } + } + } + + /** + * Handler for [REGISTER_GAME_TEST]. Implementations are produced via + * [HandlerConstructor.create] and forward to the registered `block` only when the firing + * [mod] matches the [Mod] the handler was created for, collecting declared test classes via + * an [ArchieGameTestBuilder] and registering each with [AGameTestPlatform.register]. + */ + interface RegisterGameTestHandler : Handler + { + operator fun invoke(mod: Mod): EventResult + + companion object : HandlerConstructor + { + override fun create( + mod: Mod, + block: ArchieGameTestBuilder.() -> Unit + ): RegisterGameTestHandler + { + return RegisterGameTestHandlerImpl(mod, block) + } + + class RegisterGameTestHandlerImpl internal constructor( + private val mod: Mod, + private val registerGameTests: ArchieGameTestBuilder.() -> Unit + ) : RegisterGameTestHandler + { + override operator fun invoke(mod: Mod): EventResult + { + if (this.mod != mod) + return EventResult.pass() + + ArchieGameTestBuilder().apply(registerGameTests).classes.forEach { clazz -> + AGameTestPlatform.register(clazz, mod) + } + return EventResult.interruptDefault() + } + + + } + } + } + + +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt new file mode 100644 index 000000000..83c95fae7 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt @@ -0,0 +1,25 @@ +package net.kernelpanicsoft.archie.gametest + +import java.nio.file.Path +import java.util.Properties + +/** Cross-loader bridge for dedicated server bootstrap used by client GameTests. */ +expect object ADedicatedServerPlatform { + /** + * Boots a loader-specific dedicated server rooted at [serverDirectory] using + * [serverProperties], waiting up to [timeoutSeconds] for it to finish starting. + * + * @return An opaque, loader-specific handle to pass to [stop]/[port]/[isAlive]. + */ + fun start(serverDirectory: Path, serverProperties: Properties, timeoutSeconds: Long): Any + + /** Shuts down the dedicated server identified by [serverInstance] (as returned by [start]). */ + fun stop(serverInstance: Any) + + /** The port the dedicated server identified by [serverInstance] is listening on. */ + fun port(serverInstance: Any): Int + + /** Whether the dedicated server identified by [serverInstance] is still running. */ + fun isAlive(serverInstance: Any): Boolean +} + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt new file mode 100644 index 000000000..e86cc366f --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt @@ -0,0 +1,78 @@ +package net.kernelpanicsoft.archie.gametest + +import dev.architectury.platform.Mod +import dev.architectury.utils.Env + +/** Logical side used while collecting/running GameTests. */ +enum class AGameTestSide { + SERVER, + CLIENT, +} + +internal fun AGameTestSide.toEnv(): Env = when (this) { + AGameTestSide.SERVER -> Env.SERVER + AGameTestSide.CLIENT -> Env.CLIENT +} + +/** + * Cross-loader GameTest integration point. + * + * Implementations detect whether GameTest mode is active and collect test classes per mod. + */ +expect object AGameTestPlatform +{ + /** `true` when the current process is running under a GameTest task (`runGametest`/`runGametestClient`). */ + val isGameTest: Boolean + + /** Active logical side for this GameTest run (supports launcher/property overrides). */ + val side: AGameTestSide? + + /** Register a test class for [mod] when GameTest bootstrapping occurs. */ + fun register(clazz: Class<*>, mod: Mod) +} + +/** + * Restricts which [AEvents.REGISTER_GAME_TEST]-registered mods a single `runGametest`/ + * `runGametestClient` invocation actually runs, via the [GAMETEST_MOD_ID_FILTER_PROPERTY] + * system property (a comma-separated list of mod ids). + * + * Every mod that has called `AEvents += MOD` shares one JVM-wide [AEvents.MODS] list - which + * matters because a composite build's included builds can *both* end up in that list within + * the same process. Archie-Test's Loom `runs{}` blocks `includeBuild("../Archie")`, and both + * `Archie` and `ArchieTest`'s mod init call `AEvents += MOD`, so launching Archie-Test's own + * `runGametestClient`/`runGametest` previously ran Archie's *entire* GameTest suite a second + * time in the same process, without Archie-Test's own suite being any bigger - only + * distinguishable by the test count not matching the log's actual line count. Every loader's + * `AGameTestPlatformInternal`/`AClientGameTestHarness` server- and client-side test collection + * should call [selectMods] on [AEvents.MODS] before iterating, instead of iterating it directly. + */ +object AGameTestModFilter { + private const val GAMETEST_MOD_ID_FILTER_PROPERTY = "archie.gametest.modid" + + /** + * Filters [mods] down to just the ones named in [GAMETEST_MOD_ID_FILTER_PROPERTY], or returns + * [mods] unchanged if that property isn't set. Each project's own Loom `runs{}` block should + * set this to its own `mod_id` gradle property on its `gametest`/`gametestClient` runs. + * + * @throws IllegalStateException if the property is set but names no mod present in [mods]. + */ + fun selectMods(mods: Collection): List { + val filter = System.getProperty(GAMETEST_MOD_ID_FILTER_PROPERTY)?.trim().orEmpty() + if (filter.isEmpty()) return mods.toList() + + val requestedIds = filter.split(',') + .map { it.trim() } + .filter { it.isNotEmpty() } + .toSet() + + check(requestedIds.isNotEmpty()) { + "No valid mod IDs specified in gametest filter '$GAMETEST_MOD_ID_FILTER_PROPERTY'" + } + + val selected = mods.filter { it.modId in requestedIds } + check(selected.isNotEmpty()) { + "No gametests found for requested mod IDs: ${requestedIds.joinToString(",")} (available: ${mods.joinToString(",") { it.modId }})" + } + return selected + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt new file mode 100644 index 000000000..08367c1bb --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt @@ -0,0 +1,457 @@ +package net.kernelpanicsoft.archie.gametest + +import net.minecraft.client.Minecraft +import java.util.concurrent.Phaser +import java.util.concurrent.Semaphore +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException +import java.util.concurrent.atomic.AtomicReference + +/** + * Tracks which server instance's tick thread currently owns the shared "server" phaser slot. + * + * The `ServerMixin` (per-loader, in `src/main/mixin`) this bridge backs is applied to every + * [net.minecraft.server.MinecraftServer] instance - both the integrated singleplayer server and + * an in-process dedicated GameTest server can exist back-to-back (or briefly overlap during + * teardown/startup). Since only one + * "server" participant can safely register with [ThreadingImpl.phaser] at a time, this + * identifies the owning thread so a late call from an already-superseded instance can't + * deregister or arrive on behalf of a different, currently-active instance. + */ +private val serverRegisteredThread = AtomicReference(null) + +/** + * Shared client gametest threading bridge inspired by Fabric's ThreadingImpl. + * + * Uses a Phaser for tick-phase barriers and semaphores for task handoff from + * the gametest thread to client/server threads. + */ +object ThreadingImpl { + private const val THREAD_IMPL_CLASS_NAME = "net.kernelpanicsoft.archie.gametest.ThreadingImpl" + private const val TASK_ON_THIS_THREAD_METHOD_NAME = "runTaskOnThisThread" + private const val TASK_ON_OTHER_THREAD_METHOD_NAME = "runTaskOnOtherThread" + private const val PHASE_MASK = 3 + private const val PHASE_TICK = 0 + private const val PHASE_CLIENT_TASKS = 1 + private const val PHASE_SERVER_TASKS = 2 + private const val PHASE_TEST = 3 + + private val clientSemaphore = Semaphore(0) + private val serverSemaphore = Semaphore(0) + private val testSemaphore = Semaphore(0) + private val phaser = Phaser(0) + + @Volatile + private var clientCanAcceptTasks: Boolean = false + + @Volatile + private var serverCanAcceptTasks: Boolean = false + + @Volatile + private var clientRegistered: Boolean = false + + @Volatile + private var testRegistered: Boolean = false + + @Volatile + private var taskToRun: Runnable? = null + + @Volatile + private var testThread: Thread? = null + + @Volatile + var testFailureException: Throwable? = null + private set + + @Volatile + private var gameCrashed: Boolean = false + + // The phase each tick source last called phaser.arrive() for. onClientTick()/onServerTick() + // fire every real tick regardless of whether the test thread has caught up and arrived for + // the current phase yet - Phaser requires each registered party to arrive at most once per + // phase, so without this guard, two ticks landing before the test thread's next arrival (more + // likely under CI's slower/more contended scheduling - never reproduced on a fast local + // machine) throws "Attempted arrival of unregistered party" on the second one. Reading the + // phase this call actually arrived for straight off arrive()'s return value (rather than a + // separate phaser.phase read beforehand) avoids a TOCTOU gap between checking and arriving. + // Reset to -1 on (re-)registration in onClientRunStart()/onServerRunStart() - the phase + // counter doesn't reset when a party deregisters, so a stale value surviving into a new + // registration could wrongly skip that new party's first required arrival and stall forever. + @Volatile + private var clientLastArrivedPhase: Int = -1 + + @Volatile + private var serverLastArrivedPhase: Int = -1 + + @JvmStatic + fun runTestThread(testRunner: () -> Unit) { + check(testThread == null) { "There is already a test thread running" } + testFailureException = null + clientCanAcceptTasks = false + serverCanAcceptTasks = false + + val thread = Thread { + if (!testRegistered) { + synchronized(this) { + if (!testRegistered) { + phaser.register() + testRegistered = true + } + } + } + + try { + testRunner() + } catch (failure: Throwable) { + testFailureException = failure + } finally { + synchronized(this) { + if (testRegistered) { + testRegistered = false + phaser.arriveAndDeregister() + } + } + val capturedFailure = testFailureException + testThread = null + if (capturedFailure != null) { + Minecraft.getInstance().execute { + throw capturedFailure + } + } + } + } + thread.name = "Archie Client GameTest Thread" + thread.isDaemon = true + testThread = thread + thread.start() + } + + @JvmStatic + fun checkOnGametestThread(methodName: String) { + check(isOnGametestThread()) { + "$methodName can only be called from the client gametest thread" + } + } + + @JvmStatic + fun isOnGametestThread(): Boolean = Thread.currentThread() === testThread + + @JvmStatic + fun onClientRunStart() { + gameCrashed = false + if (!clientRegistered) { + synchronized(this) { + if (!clientRegistered) { + phaser.register() + clientRegistered = true + // A fresh registration must not inherit a previous instance's arrival + // history - the phaser's phase counter doesn't reset just because the + // prior party deregistered, so a stale value here could make this new + // party's first tick wrongly believe it already arrived for the current + // phase, permanently stalling that phase (see clientLastArrivedPhase kdoc). + clientLastArrivedPhase = -1 + } + } + } + } + + @JvmStatic + fun onClientRunStop() { + clientCanAcceptTasks = false + serverCanAcceptTasks = false + + synchronized(this) { + if (clientRegistered) { + clientRegistered = false + phaser.arriveAndDeregister() + } + } + + // Force-release the server slot regardless of which instance holds it - the client is + // shutting down entirely, so nothing should be left registered afterward. + if (serverRegisteredThread.getAndSet(null) != null) { + synchronized(this) { + phaser.arriveAndDeregister() + } + } + } + + @JvmStatic + fun onServerRunStart() { + val current = Thread.currentThread() + if (serverRegisteredThread.compareAndSet(null, current)) { + synchronized(this) { + phaser.register() + // See the matching comment in onClientRunStart() - a new server instance + // (e.g. an integrated singleplayer server starting after an earlier dedicated + // GameTest server already registered, arrived, and deregistered) must not + // inherit the previous instance's last-arrived phase. + serverLastArrivedPhase = -1 + } + } + // If another server instance's thread already holds the slot (e.g. an integrated + // server that hasn't finished tearing down yet), this instance simply won't + // participate in tick-phase sync until that one releases it - see onServerTick(). + } + + @JvmStatic + fun onServerRunStop() { + serverCanAcceptTasks = false + + val current = Thread.currentThread() + if (serverRegisteredThread.compareAndSet(current, null)) { + synchronized(this) { + phaser.arriveAndDeregister() + } + } + // If this thread never held the slot, it never registered either - nothing to release. + } + + @JvmStatic + fun setGameCrashed() { + gameCrashed = true + onClientRunStop() + } + + @JvmStatic + fun onClientTick() { + if (testThread == null && !testRegistered) return + + if (!clientRegistered) { + synchronized(this) { + if (!clientRegistered) { + phaser.register() + clientRegistered = true + } + } + } + + clientCanAcceptTasks = true + + if (clientSemaphore.tryAcquire()) { + taskToRun?.run() + } + + if (clientRegistered && phaser.phase != clientLastArrivedPhase) { + clientLastArrivedPhase = phaser.arrive() + } + } + + @JvmStatic + fun preRunTasks() { + if (!isThreadingActive()) return + } + + @JvmStatic + fun postRunTasks() { + if (!isThreadingActive()) return + + clientCanAcceptTasks = true + + while (clientSemaphore.tryAcquire()) { + val task = taskToRun ?: break + task.run() + } + } + + @JvmStatic + fun onServerTick() { + if (testThread == null && !testRegistered) return + + val current = Thread.currentThread() + if (serverRegisteredThread.compareAndSet(null, current)) { + synchronized(this) { + phaser.register() + } + } + + if (serverRegisteredThread.get() !== current) { + // Another server instance already owns the shared slot (e.g. this is a dedicated + // GameTest server ticking while the integrated server hasn't finished tearing + // down yet). Don't touch the semaphore/phaser on its behalf. + return + } + + serverCanAcceptTasks = true + + if (serverSemaphore.tryAcquire()) { + taskToRun?.run() + } + + if (phaser.phase != serverLastArrivedPhase) { + serverLastArrivedPhase = phaser.arrive() + } + } + + @Suppress("unused") + @JvmStatic + fun runOnClient(action: () -> Unit) { + checkOnGametestThread("runOnClient") + ensureDispatchPhase() + check(clientCanAcceptTasks) { "runOnClient called when no client is running" } + runTaskOnOtherThread(action, clientSemaphore) + } + + @Suppress("unused") + @JvmStatic + fun runOnServer(action: () -> Unit) { + checkOnGametestThread("runOnServer") + ensureDispatchPhase() + check(serverCanAcceptTasks) { + "runOnServer called when no server is running " + + "(serverRegisteredThread=${serverRegisteredThread.get()?.name}, " + + "testRegistered=$testRegistered, testThread=${testThread?.name}, phase=${getCurrentPhase()})" + } + runTaskOnOtherThread(action, serverSemaphore) + } + + private fun ensureDispatchPhase() { + // Intentionally no-op for the current client harness bridge. + // Dispatch relies on non-blocking client/server loop integration. + } + + private fun runTaskOnOtherThread(action: () -> Unit, targetSemaphore: Semaphore) { + val thrown = AtomicReference(null) + taskToRun = Runnable { runTaskOnThisThread(action, thrown) } + + targetSemaphore.release() + + try { + val acquired = testSemaphore.tryAcquire(10, TimeUnit.SECONDS) + check(acquired) { + "Timed out waiting for cross-thread task completion " + + "(phase=${getCurrentPhase()}, nextPhase=${getNextPhase()}, " + + "clientCanAcceptTasks=$clientCanAcceptTasks, serverCanAcceptTasks=$serverCanAcceptTasks, " + + "target=${if (targetSemaphore === clientSemaphore) "client" else "server"}, " + + "taskPending=${taskToRun != null}, testThreadAlive=${testThread?.isAlive == true})" + } + } catch (e: InterruptedException) { + throw RuntimeException(e) + } + + val error = thrown.get() + if (error != null) { + joinAsyncStackTrace(error) + throw error + } + } + + private fun runTaskOnThisThread(action: () -> Unit, thrown: AtomicReference) { + try { + action() + } catch (e: Throwable) { + thrown.set(e) + } finally { + taskToRun = null + testSemaphore.release() + } + } + + private fun joinAsyncStackTrace(error: Throwable) { + if (System.getProperty("fabric.client.gametest.disableJoinAsyncStackTraces") != null) { + return + } + + val otherThreadStackTrace = error.stackTrace ?: return + var otherThreadIndex = otherThreadStackTrace.size - 1 + while (otherThreadIndex >= 0) { + val element = otherThreadStackTrace[otherThreadIndex] + if (THREAD_IMPL_CLASS_NAME == element.className && TASK_ON_THIS_THREAD_METHOD_NAME == element.methodName) { + break + } + otherThreadIndex-- + } + + if (otherThreadIndex == -1) { + return + } + + val thisThreadStackTrace = Thread.currentThread().stackTrace + var thisThreadIndex = 0 + while (thisThreadIndex < thisThreadStackTrace.size) { + val element = thisThreadStackTrace[thisThreadIndex] + if (THREAD_IMPL_CLASS_NAME == element.className && TASK_ON_OTHER_THREAD_METHOD_NAME == element.methodName) { + break + } + thisThreadIndex++ + } + + if (thisThreadIndex == thisThreadStackTrace.size) { + return + } + + val joinedStackTrace = arrayOfNulls( + (otherThreadIndex + 1) + 1 + (thisThreadStackTrace.size - thisThreadIndex), + ) + System.arraycopy(otherThreadStackTrace, 0, joinedStackTrace, 0, otherThreadIndex + 1) + joinedStackTrace[otherThreadIndex + 1] = StackTraceElement("Async Stack Trace", ".", null, 1) + System.arraycopy( + thisThreadStackTrace, + thisThreadIndex, + joinedStackTrace, + otherThreadIndex + 2, + thisThreadStackTrace.size - thisThreadIndex, + ) + @Suppress("UNCHECKED_CAST") + error.stackTrace = joinedStackTrace as Array + } + + @JvmStatic + fun awaitTicks(ticks: Int, timeoutMillis: Long): Boolean { + if (gameCrashed) return false + if (ticks <= 0) return true + + if (!testRegistered) { + synchronized(this) { + if (!testRegistered) { + phaser.register() + testRegistered = true + } + } + } + + val deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis.coerceAtLeast(1L)) + repeat(ticks) { + val phase = advanceToNextTickPhase() + val remainingNanos = deadline - System.nanoTime() + if (remainingNanos <= 0L) return false + + try { + phaser.awaitAdvanceInterruptibly(phase, remainingNanos, TimeUnit.NANOSECONDS) + } catch (_: TimeoutException) { + return false + } catch (_: InterruptedException) { + Thread.currentThread().interrupt() + return false + } + } + + return true + } + + @Suppress("unused") + private fun getCurrentPhase(): Int = (phaser.phase - 1) and PHASE_MASK + + @Suppress("unused") + private fun getNextPhase(): Int = phaser.phase and PHASE_MASK + + @Suppress("unused") + private fun enterPhase(phase: Int) { + while (getNextPhase() != phase) { + phaser.arriveAndAwaitAdvance() + } + + // After aligning to the requested next phase, participate in that + // phase barrier as well. Without this, callers can observe the phase + // but not synchronize with peer threads at the same boundary. + phaser.arriveAndAwaitAdvance() + } + + private fun advanceToNextTickPhase(): Int { + check(PHASE_TICK == 0 && PHASE_CLIENT_TASKS == 1 && PHASE_SERVER_TASKS == 2 && PHASE_TEST == 3) + return phaser.arrive() + } + + private fun isThreadingActive(): Boolean = testThread != null || testRegistered +} + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/AUIScopeManager.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/AUIScopeManager.kt new file mode 100644 index 000000000..86122601f --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/AUIScopeManager.kt @@ -0,0 +1,17 @@ +package net.kernelpanicsoft.archie.gui + +import kotlinx.coroutines.CoroutineScope + +/** + * Global registry of active [CoroutineScope]s created by open GUI screens. + * + * Each [ComposeScreen] or [ComposeContainerScreen] registers its `composeScope` here on + * startup so that external systems (e.g. the event bus) can broadcast work to all live + * GUI coroutines without holding direct references to individual screens. + * + * Scopes are removed automatically when their screen closes. + */ +object AUIScopeManager { + /** The set of all currently active GUI coroutine scopes. */ + val scopes = mutableSetOf() +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeBlockContainerMenu.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeBlockContainerMenu.kt new file mode 100644 index 000000000..002d3e7cb --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeBlockContainerMenu.kt @@ -0,0 +1,74 @@ +package net.kernelpanicsoft.archie.gui + +import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager +import net.kernelpanicsoft.archie.gui.blockentity.ComposeBlockEntityState +import net.kernelpanicsoft.archie.gui.blockentity.getOrCreateBlockEntityState +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.entity.player.Player +import net.minecraft.world.inventory.MenuType +import net.minecraft.world.level.block.entity.BlockEntity + +/** + * Base class for [BlockEntity]-backed Compose container menus. + * + * See [ComposeContainerMenuBase] for slot pre-registration/positioning behavior, shared with + * [net.kernelpanicsoft.archie.gui.item.ComposeItemContainerMenu] - this class only adds the + * [BlockEntity]-specific pieces: holding [tile], deriving [blockEntityState] from its position, + * and registering it with [BlockEntityStateManager]. + * + * ### Subclassing + * ```kotlin + * class MyMenu(id: Int, inventory: Inventory, tile: MyTile) : + * ComposeBlockContainerMenu(MY_MENU_TYPE, id, inventory, tile) { + * + * override fun registerSlotHandlers() { + * handler("inventory", tile.items) // ties the "inventory" slot group to the storage + * } + * } + * ``` + * + * @param T The [BlockEntity] type that owns the storage. + * @param SELF The concrete menu subclass (self-referential for the [MenuType]). + * @param type The registered [MenuType] for this menu. + * @param id The container id assigned by the server. + * @param playerInventory The opening player's inventory. + * @param tile The block entity instance. + */ +abstract class ComposeBlockContainerMenu>( + type: MenuType, + id: Int, + playerInventory: Inventory, + protected val tile: T, +) : ComposeContainerMenuBase(type, id, playerInventory) { + + val blockEntityState: ComposeBlockEntityState = getOrCreateBlockEntityState(tile.blockPos) + + init + { + // Must run here, in this class's own init - not from ComposeContainerMenuBase's, which + // would dispatch into onMenuOpened() before `tile` (this class's own constructor + // property) is actually assigned. See onMenuOpened's KDoc. + onMenuOpened() + } + + override fun onMenuOpened() + { + BlockEntityStateManager.registerBlockEntity(tile) + if (!level.isClientSide) + { + BlockEntityStateManager.addTrackedPlayer(tile, player as ServerPlayer) + } + } + + override fun onMenuClosed(player: Player) + { + BlockEntityStateManager.unregisterBlockEntity(tile) + if (!level.isClientSide) + { + BlockEntityStateManager.removeTrackedPlayer(tile, player as ServerPlayer) + } + } + + override fun stillValid(player: Player): Boolean = true +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerMenuBase.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerMenuBase.kt new file mode 100644 index 000000000..9bc0e1068 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerMenuBase.kt @@ -0,0 +1,483 @@ +package net.kernelpanicsoft.archie.gui + +import earth.terrarium.common_storage_lib.item.impl.vanilla.AbstractVanillaContainer +import earth.terrarium.common_storage_lib.resources.item.ItemResource +import earth.terrarium.common_storage_lib.storage.base.CommonStorage +import net.kernelpanicsoft.archie.gui.layout.IntRect +import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel +import net.kernelpanicsoft.archie.transfer.ArchieItemMenuSlot +import net.kernelpanicsoft.archie.transfer.ArchieItemStorage +import net.kernelpanicsoft.archie.transfer.VanillaMenuSlot +import net.minecraft.world.Container +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.entity.player.Player +import net.minecraft.world.inventory.AbstractContainerMenu +import net.minecraft.world.inventory.ClickType +import net.minecraft.world.inventory.MenuType +import net.minecraft.world.inventory.Slot +import net.minecraft.world.item.ItemStack +import net.minecraft.world.level.Level +import java.util.function.Predicate + +/** + * Holder-agnostic base for Compose-backed container menus: everything about slot layout, + * registration, and vanilla-menu plumbing that doesn't care whether the menu is backed by a + * [net.minecraft.world.level.block.entity.BlockEntity] ([ComposeBlockContainerMenu]) or an + * [net.minecraft.world.item.ItemStack] ([net.kernelpanicsoft.archie.gui.item.ComposeItemContainerMenu]). + * + * Slots are pre-registered at construction time with placeholder pixel positions so that + * [AbstractContainerMenu.initializeContents] (triggered by the server's slot-sync packet) + * always finds the correct number of slots. When the Compose layout runs and reports actual + * on-screen positions via [updateSlotData], existing slot objects have their pixel coordinates + * updated in-place rather than the slot list being rebuilt from scratch. + * + * @param SELF The concrete menu subclass (self-referential for the [MenuType]). + * @param type The registered [MenuType] for this menu. + * @param id The container id assigned by the server. + * @param playerInventory The opening player's inventory. + */ +abstract class ComposeContainerMenuBase>( + type: MenuType, + id: Int, + protected val playerInventory: Inventory, +) : AbstractContainerMenu(type, id) { + + /** + * The most recently reported [SlotData] from the Compose layout. + * On the server this is the authoritative source of slot group sizes. + * On the client it is received from the server via a [net.kernelpanicsoft.archie.networking.NetworkChannel]. + */ + var slotData: SlotData = SlotData() + private set + + /** `true` once [repositionSlots] has run at least once and slot pixel positions are valid. */ + var ready: Boolean = false + private set + + /** Parallel to [slots], stores which compose layer produced each vanilla slot. */ + private var slotLayerDepthByIndex: IntArray = IntArray(0) + private var slotClipBoundsByIndex: Array = emptyArray() + + /** + * The enabled group ids [rebuildSlots] last ran with, in [SlotData.groups] iteration order. + * Used by [applySlotData] to tell a genuine shape change (e.g. switching tabs to a group + * backed by different storage) from a mere reposition (scrolling, resizing) that must not + * discard existing [Slot] identity. + */ + private var registeredGroupIds: List = emptyList() + + /** + * The screen's `leftPos` offset — set by [ComposeContainerScreen] so that absolute + * Compose coordinates can be converted to slot-relative coordinates that vanilla's + * item rendering expects (vanilla renders items at `leftPos + slot.x`). + */ + var screenLeftPos: Int = 0 + + /** + * The screen's `topPos` offset — set by [ComposeContainerScreen] so that absolute + * Compose coordinates can be converted to slot-relative coordinates that vanilla's + * item rendering expects (vanilla renders items at `topPos + slot.y`). + */ + var screenTopPos: Int = 0 + + /** Maps slot-group id → the storage that backs it. */ + private val slotHandlers: MutableMap> = mutableMapOf() + private val slotFilters: MutableMap> = mutableMapOf() + + protected val player: Player = playerInventory.player + protected val level: Level = player.level() + + /** + * Register whatever state-tracking this menu's holder needs here. **Not called + * automatically** - each concrete subclass must call this from its own `init {}` block, + * after its own constructor-parameter properties (e.g. `tile`) are assigned. Calling it from + * *this* class's own `init {}` instead would dispatch into the subclass's override before + * those properties exist yet (Kotlin/JVM run a subclass's own property initializers only + * after its superclass's constructor - including this class's `init {}` - has fully + * returned), silently observing them as null despite their non-null declared type. + */ + protected abstract fun onMenuOpened() + + /** Called from [removed] - unregister whatever [onMenuOpened] registered here. */ + protected abstract fun onMenuClosed(player: Player) + + /** + * Excludes the player-inventory slot at container-relative [index] (0-35, matching + * [Inventory]'s own numbering: hotbar 0-8, main 9-35) from placement/pickup - frozen in + * place rather than removed from the slot list, to avoid reworking [addPlayerSlots]'s + * hardcoded 36-slot/3x9+9 assumptions elsewhere. Used by + * [net.kernelpanicsoft.archie.gui.item.ComposeItemContainerMenu] to freeze the backpack's + * own slot in the player's inventory while its GUI is open (also special-cased in + * [quickMoveStack], so shift-clicking it doesn't duplicate its contents into itself). + */ + protected open fun isPlayerSlotExcluded(index: Int): Boolean = false + + // ── Slot registration ────────────────────────────────────────────────── + + /** + * Implement this to call [handler] for each slot group your menu exposes. + * + * This is called: + * 1. Once at construction time so the slots list is pre-populated with the correct count. + * 2. Again every time [updateSlotData] fires with updated positions. + */ + protected abstract fun registerSlotHandlers() + + /** + * Registers a generic [CommonStorage] handler for the named slot group. + */ + protected fun handler(group: String, storage: CommonStorage, filter: Predicate = Predicate { true }) { + slotHandlers[group] = storage + slotFilters[group] = filter + } + + // ── Called by the Compose layout ─────────────────────────────────────── + + /** + * Called by the [Slot] composable once its absolute screen position is known. + * + * On the **first** call (before any slots exist) the full slot list is built. + * On **subsequent** calls (re-layout / window resize) existing slot objects have their + * pixel coordinates updated in-place so [initializeContents] is never broken. + * + * @param data The updated [SlotData] from the Compose layout. + */ + fun updateSlotData(data: SlotData) { + slotData = data + applySlotData() + broadcastFullState() + // Notify server of the new layout so it can validate slot indices + ArchieNetworkChannel.toServer(data) + } + + fun slotLayerDepth(slotIndex: Int): Int = slotLayerDepthByIndex.getOrElse(slotIndex) { 0 } + fun slotClipBounds(slotIndex: Int): IntRect? = slotClipBoundsByIndex.getOrNull(slotIndex) + + /** + * Whether the slot at [slotIndex] currently overlaps its group's clip bounds (if it has + * any, i.e. it sits inside a [net.kernelpanicsoft.archie.gui.composables.containers.Scrollable]). + * + * Used as this slot's [Slot.isActive] - the same vanilla hook that hides the Donkey/Mule + * armor slot - so a slot scrolled out of view stops rendering its item icon and stops being + * hoverable/clickable, without needing to remove it from [slots] and break its identity. + * A slot with no clip bounds (not inside a scrollable) is always visible. + */ + fun isSlotVisible(slotIndex: Int): Boolean { + val clip = slotClipBoundsByIndex.getOrNull(slotIndex) ?: return true + val slot = slots.getOrNull(slotIndex) ?: return true + val absX = screenLeftPos + slot.x + val absY = screenTopPos + slot.y + return absX + 16 > clip.minX && absX < clip.maxX && absY + 16 > clip.minY && absY < clip.maxY + } + + /** + * Re-derives every [Slot]'s pixel position from the current [slotData] without touching + * slot identity or [slotHandlers]. Call after [screenLeftPos]/[screenTopPos] change - + * slot coordinates have the screen offset baked into their subtraction (see + * [repositionSlots]), so they go stale whenever the screen recenters, independent of + * whether Compose's own slot layout changed. + */ + fun refreshSlotPositions() { + if (slots.isEmpty()) return + repositionSlots() + rebuildSlotMetadataMaps() + } + + /** + * Applies the current [slotData]: rebuilds the vanilla [Slot] list from scratch only when + * the set of enabled groups actually changed (or on first layout); otherwise repositions + * the existing slots in place. + * + * Every layout pass - including a single frame of scrolling - re-reports the full + * [SlotData], so rebuilding unconditionally here would discard and recreate every [Slot] + * object on every scroll tick / resize, breaking anything holding a reference to one + * (drag-in-progress, vanilla's own hovered-slot tracking, quick-move). + */ + private fun applySlotData() { + val enabledGroupIds = slotData.groups.entries.filter { it.value.enabled }.map { it.key } + if (slots.isEmpty() || enabledGroupIds != registeredGroupIds) { + rebuildSlots() + registeredGroupIds = enabledGroupIds + } else { + repositionSlots() + } + rebuildSlotMetadataMaps() + } + + /** + * Full slot-list (re)construction: registers all menu slots and player slots. + * Only called by [applySlotData] when the enabled group set actually changed. + */ + private fun rebuildSlots() { + registerSlotHandlers() + this.slots.clear() + this.lastSlots.clear() + this.remoteSlots.clear() + addMenuSlots() + addPlayerSlots() + repositionSlots() + } + + /** + * Subsequent calls: update pixel coordinates of already-registered slots in-place. + * Converts absolute Compose screen coordinates to slot-relative coordinates by + * subtracting [screenLeftPos]/[screenTopPos], because vanilla renders items at + * `leftPos + slot.x` and `topPos + slot.y`. + * Slot *count* must not change between layouts. + */ + private fun repositionSlots() { + var slotIndex = 0 + + // Reposition menu slots + slotData.groups.forEach { (id, group) -> + if (group.enabled) + { + slotHandlers[id]?.let { _ -> + for (row in 0 until group.size.height) + { + for (col in 0 until group.size.width) + { + if (slotIndex < slots.size) + { + val mcSlot = slots[slotIndex] + mcSlot.x = group.pos.x + 1 + col * 18 - screenLeftPos + mcSlot.y = group.pos.y + 1 + row * 18 - screenTopPos + slotIndex++ + } + } + } + } + } + } + + // Reposition player slots (main inventory 3×9, then hotbar 1×9) + slotData.playerGroup.let { pg -> + for (row in 0 until 3) { + for (col in 0 until 9) { + if (slotIndex < slots.size) { + slots[slotIndex].x = pg.pos.x + 1 + col * 18 - screenLeftPos + slots[slotIndex].y = pg.pos.y + 1 + row * 18 - screenTopPos + slotIndex++ + } + } + } + for (col in 0 until 9) { + if (slotIndex < slots.size) { + slots[slotIndex].x = pg.pos.x + 1 + col * 18 - screenLeftPos + slots[slotIndex].y = pg.pos.y + 1 + 58 - screenTopPos + slotIndex++ + } + } + } + ready = true + } + + private fun rebuildSlotMetadataMaps() { + val depths = ArrayList(slots.size) + val clips = ArrayList(slots.size) + + slotData.groups.forEach { (id, group) -> + if (!group.enabled) return@forEach + if (slotHandlers[id] == null) return@forEach + val clip = group.clip + repeat(group.size.width * group.size.height) { + depths += group.layerDepth + clips += clip + } + } + + val playerClip = slotData.playerGroup.clip + repeat(36) { + depths += slotData.playerGroup.layerDepth + clips += playerClip + } + + while (depths.size < slots.size) { + depths += 0 + clips += null + } + slotLayerDepthByIndex = depths.toIntArray() + slotClipBoundsByIndex = clips.toTypedArray() + } + + // ── Internal slot helpers ────────────────────────────────────────────── + + private fun addMenuSlots() { + slotData.groups.forEach { (id, group) -> + if (!group.enabled) return@forEach + slotHandlers[id]?.let { handler -> + // Use slot-relative coords (subtract screen offset so vanilla adds it back correctly) + slotGrid(group.pos.x - screenLeftPos, group.pos.y - screenTopPos, group.size.width, group.size.height, handler, slotFilters[id] ?: Predicate { true }) + } + } + } + + private fun addPlayerSlots() { + slotData.playerGroup.let { pg -> + val relX = pg.pos.x - screenLeftPos + val relY = pg.pos.y - screenTopPos + // 3 rows of 9 (main inventory: playerInventory indices 9–35) + for (row in 0 until 3) { + for (col in 0 until 9) { + playerSlot(col + row * 9 + 9, relX + col * 18, relY + row * 18) + } + } + // Hotbar (playerInventory indices 0–8), 58px below main inventory + for (col in 0 until 9) { + playerSlot(col, relX + col * 18, relY + 58) + } + } + } + + /** + * Adds one player-inventory slot at container-relative [containerIndex], frozen against + * placement/pickup if [isPlayerSlotExcluded] says so - see its KDoc. + */ + private fun playerSlot(containerIndex: Int, x: Int, y: Int) { + val excluded = isPlayerSlotExcluded(containerIndex) + addSlot(object : Slot(playerInventory, containerIndex, x, y) + { + override fun mayPlace(itemStack: ItemStack): Boolean = !excluded + override fun mayPickup(player: Player): Boolean = !excluded + override fun isActive(): Boolean = isSlotVisible(index) + }) + } + + // ── Slot grid helper ─────────────────────────────────────────────────── + + data class SlotGridLocation(val slot: Int, val x: Int, val y: Int) + + protected fun slotGrid(x: Int, y: Int, width: Int, height: Int, block: SlotGridLocation.() -> Unit) { + for (row in 0 until height) { + for (col in 0 until width) { + SlotGridLocation(col + row * width, x + col * 18, y + row * 18).block() + } + } + } + + protected fun slotGrid(x: Int, y: Int, width: Int, height: Int, container: Container, filter: Predicate = Predicate { true }) { + slotGrid(x, y, width, height) { slot(container, filter, slot, this.x, this.y) } + } + + protected fun slotGrid(x: Int, y: Int, width: Int, height: Int, storage: CommonStorage, filter: Predicate = Predicate { true }) { + slotGrid(x, y, width, height) { slot(storage, filter, slot, this.x, this.y) } + } + + protected fun slot(mcSlot: Slot) { addSlot(mcSlot) } + + protected fun slot(storage: CommonStorage, filter: Predicate = Predicate { true }, slot: Int, x: Int, y: Int) { + if (slot !in 0 until storage.size()) return + when (storage) + { + is ArchieItemStorage -> slot(storage, filter, slot, x, y) + is AbstractVanillaContainer -> slot(storage, filter, slot, x, y) + } + } + + protected fun slot(container: Container, filter: Predicate = Predicate { true }, slot: Int, x: Int, y: Int) { + addSlot(object : Slot(container, slot, x, y) + { + override fun mayPlace(itemStack: ItemStack): Boolean = filter.test(itemStack) + override fun isActive(): Boolean = isSlotVisible(index) + }) + } + + protected fun slot(storage: ArchieItemStorage, filter: Predicate = Predicate { true }, slot: Int, x: Int, y: Int) { + addSlot(ArchieItemMenuSlot(storage, filter, slot, x, y, this)) + } + + protected fun slot(storage: AbstractVanillaContainer, filter: Predicate = Predicate { true }, slot: Int, x: Int, y: Int) { + addSlot(VanillaMenuSlot(storage, filter, slot, x, y, this)) + } + + // ── AbstractContainerMenu overrides ──────────────────────────────────── + + /** + * Shift-click handling: menu slots move into the player inventory/hotbar, and player + * slots move into the menu, falling back between main inventory and hotbar when the menu + * has no room. Slot ranges are derived from [slots].size rather than hardcoded, since the + * number of menu slots varies with which groups are enabled. + * + * The player-inventory slot excluded via [isPlayerSlotExcluded] (if any) is special-cased + * to return [ItemStack.EMPTY] immediately - shift-clicking a backpack's own slot while its + * GUI is open must not be able to move the backpack into itself. + */ + override fun quickMoveStack(player: Player, index: Int): ItemStack { + val slot = slots.getOrNull(index) ?: return ItemStack.EMPTY + if (slot.container === playerInventory && isPlayerSlotExcluded(slot.containerSlot)) return ItemStack.EMPTY + if (!slot.hasItem()) return ItemStack.EMPTY + + val stackInSlot = slot.item + val copied = stackInSlot.copy() + + val totalSlots = slots.size + val playerSlotCount = 36 + val playerStart = (totalSlots - playerSlotCount).coerceAtLeast(0) + val playerEndExclusive = totalSlots + val menuStart = 0 + val menuEndExclusive = playerStart + val hotbarSize = 9 + val hotbarStart = (playerEndExclusive - hotbarSize).coerceAtLeast(playerStart) + val inventoryStart = playerStart + val inventoryEndExclusive = hotbarStart + + val moved = when { + // From menu -> player inventory/hotbar + index in menuStart until menuEndExclusive -> + moveItemStackTo(stackInSlot, playerStart, playerEndExclusive, true) + + // From player main inventory -> menu first, then hotbar fallback + index in inventoryStart until inventoryEndExclusive -> { + val movedToMenu = menuEndExclusive > menuStart && moveItemStackTo(stackInSlot, menuStart, menuEndExclusive, false) + movedToMenu || moveItemStackTo(stackInSlot, hotbarStart, playerEndExclusive, false) + } + + // From hotbar -> menu first, then main inventory fallback + index in hotbarStart until playerEndExclusive -> { + val movedToMenu = menuEndExclusive > menuStart && moveItemStackTo(stackInSlot, menuStart, menuEndExclusive, false) + movedToMenu || moveItemStackTo(stackInSlot, inventoryStart, inventoryEndExclusive, false) + } + + else -> false + } + + if (!moved) return ItemStack.EMPTY + + if (stackInSlot.isEmpty) slot.set(ItemStack.EMPTY) else slot.setChanged() + slot.onTake(player, stackInSlot) + return copied + } + + override fun clicked(slotId: Int, button: Int, clickType: ClickType, player: Player) { + super.clicked(slotId, button, clickType, player) + broadcastChanges() + } + + override fun removed(player: Player) + { + super.removed(player) + onMenuClosed(player) + } + + // ── Networking ───────────────────────────────────────────────────────── + + companion object { + + /** + * Registers the serverbound [SlotData] packet handler that keeps the server's slot + * positions/clip bounds in sync with whichever [ComposeContainerMenuBase] the sending + * player has open. Call once during network channel setup. + */ + fun register() { + ArchieNetworkChannel.serverbound(SlotData::class) { data, context -> + val menu = context.player.containerMenu + if (menu is ComposeContainerMenuBase<*>) { + menu.slotData = data + // Match slot positions on the server to the client layout + menu.applySlotData() + menu.broadcastFullState() + } + } + } + } + } diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerScreen.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerScreen.kt new file mode 100644 index 000000000..2212be8ab --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerScreen.kt @@ -0,0 +1,482 @@ +package net.kernelpanicsoft.archie.gui + +import androidx.compose.runtime.BroadcastFrameClock +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.ProvidableCompositionLocal +import androidx.compose.runtime.Recomposer +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.snapshots.Snapshot +import com.mojang.blaze3d.platform.InputConstants +import kotlinx.coroutines.* +import net.kernelpanicsoft.archie.gui.access.SlotHighlightClipProvider +import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthProvider +import net.kernelpanicsoft.archie.gui.blockentity.LocalBlockEntityState +import net.kernelpanicsoft.archie.gui.composables.containers.RootContainer +import net.kernelpanicsoft.archie.gui.item.ComposeItemContainerMenu +import net.kernelpanicsoft.archie.gui.item.LocalItemState +import net.kernelpanicsoft.archie.gui.layer.LayerStackManager +import net.kernelpanicsoft.archie.gui.layer.LocalLayerManager +import net.kernelpanicsoft.archie.gui.layout.IntCoordinates +import net.kernelpanicsoft.archie.gui.layout.IntRect +import net.kernelpanicsoft.archie.gui.layout.LayoutNode +import net.kernelpanicsoft.archie.gui.modifiers.Constraints +import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType +import net.kernelpanicsoft.archie.gui.util.extension.processCharEvent +import net.kernelpanicsoft.archie.gui.util.extension.processDragEvent +import net.kernelpanicsoft.archie.gui.util.extension.processKeyEvent +import net.kernelpanicsoft.archie.gui.util.extension.processPointerEvent +import net.kernelpanicsoft.archie.gui.util.extension.processScrollEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen +import net.minecraft.network.chat.Component +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.inventory.Slot +import org.lwjgl.glfw.GLFW +import kotlin.coroutines.CoroutineContext + +/** Provides the current [ComposeContainerScreen] to any composable in its tree. */ +val LocalContainerScreen: ProvidableCompositionLocal> = + compositionLocalOf { throw IllegalStateException("Screen has not been provided") } + +/** Provides the current [ComposeContainerMenuBase] to any composable in its tree. */ +val LocalContainerMenu: ProvidableCompositionLocal> = + compositionLocalOf { throw IllegalStateException("Screen has not been provided") } + +/** + * A Compose-driven [AbstractContainerScreen] with layer support, async recomposition, and + * vanilla [Slot] rendering kept in sync with the Compose-reported [SlotGroup] layout. + * + * Behaves like [ComposeScreen] but additionally bridges vanilla's container/slot machinery: + * the base layer (layer 0) is rendered from [renderBg] so it draws under vanilla's slots, and + * any additional layers (modals) render on top from [render] via [renderSlot]/[slotClipRect] + * clipping so scrolled-out-of-view slots don't paint over unrelated content. + * + * Extend this class and call [start] inside your `init()` override, the same way as + * [ComposeScreen]. Works uniformly for both [ComposeBlockContainerMenu] (BlockEntity-backed) and + * [ComposeItemContainerMenu] (ItemStack-backed) subclasses - nothing here is holder-specific. + * + * @param T The concrete [ComposeContainerMenuBase] subclass driving this screen. + * @param menu The container menu instance for this screen. + * @param playerInventory The opening player's inventory. + * @param title The screen title passed to the vanilla [AbstractContainerScreen] constructor. + * @param asynchronous When `true` (default), recomposition runs off the main thread and + * the result is joined at the start of the next frame for smooth, non-blocking updates. + * Set to `false` to force synchronous recomposition (simpler but may stutter). + */ +abstract class ComposeContainerScreen>( + menu: T, playerInventory: Inventory, title: Component, + val asynchronous: Boolean = true, +) : AbstractContainerScreen(menu, playerInventory, title), + CoroutineScope, + SlotLayerDepthProvider, + SlotHighlightClipProvider, + ComposeIdleAware, + LayerManagerProvider +{ + companion object { + private const val BASE_LAYER_Z = 100f + private const val LAYER_Z_STEP = 200f + private const val SLOT_LAYER_OFFSET = 120f + + /** The base Z offset used when rendering the layer at [layerDepth], deepest layers on top. */ + fun layerBaseZ(layerDepth: Int): Float = BASE_LAYER_Z + layerDepth * LAYER_Z_STEP + } + + + private var hasFrameWaiters = false + private val clock = BroadcastFrameClock { hasFrameWaiters = true } + + // See ComposeScreen's identical fields for why this is captured once and untyped against + // kotlinx-coroutines-test. + private val testPump = ComposeTestClockOverride.pump + + private val composeScope = CoroutineScope(ComposeTestClockOverride.dispatcher ?: Dispatchers.Default) + clock + final override val coroutineContext: CoroutineContext = composeScope.coroutineContext + + final override lateinit var layerManager: LayerStackManager + private set + private lateinit var recomposer: Recomposer + private var recomposeJob: Job? = null + + private var applyScheduled = false + private val snapshotHandle = Snapshot.registerGlobalWriteObserver { + if (!applyScheduled) { + applyScheduled = true + composeScope.launch { + applyScheduled = false + Snapshot.sendApplyNotifications() + } + } + } + + private var lastMouseX = 0.0 + private var lastMouseY = 0.0 + + override fun isComposeIdle(): Boolean = + !applyScheduled && !hasFrameWaiters && recomposeJob?.isActive != true + + /** [titleLabelX]/[titleLabelY] expressed as an absolute-screen [IntCoordinates] pair. */ + var titleLabelPos: IntCoordinates + get() = IntCoordinates(titleLabelX, titleLabelY) + set(value) { + titleLabelX = value.x - leftPos + titleLabelY = value.y - topPos + } + + /** [inventoryLabelX]/[inventoryLabelY] expressed as an absolute-screen [IntCoordinates] pair. */ + var inventoryLabelPos: IntCoordinates + get() = IntCoordinates(inventoryLabelX, inventoryLabelY) + set(value) { + inventoryLabelX = value.x - leftPos + inventoryLabelY = value.y - topPos + } + + /** + * Initialises the Compose runtime and pushes the base layer with [content]. + * + * Must be called once from [init]. Subsequent calls replace the content. + * + * @param content The root composable content for this screen. + */ + protected fun start(content: @Composable () -> Unit) { + recomposer = Recomposer(coroutineContext) + layerManager = LayerStackManager(recomposer) + + AUIScopeManager.scopes += composeScope + launch { recomposer.runRecomposeAndApplyChanges() } + + layerManager.push { _ -> + CompositionLocalProvider( + LocalContainerScreen provides this, + LocalContainerMenu provides menu, + LocalSlotData provides menu.slotData, + // Only one of these is non-null for any given menu - LocalBlockEntityState / + // LocalItemState are both nullable-by-default composition locals precisely so + // composables reaching for the "wrong" one for this menu's holder kind get a + // clear null rather than a bogus fallback value. + LocalBlockEntityState provides (menu as? ComposeBlockContainerMenu<*, *>)?.blockEntityState, + LocalItemState provides (menu as? ComposeItemContainerMenu<*>)?.itemState, + LocalLayerManager provides layerManager, + ) { + RootContainer { + content() + } + } + } + } + + // ── Rendering ───────────────────────────────────────────────────────── + + /** + * Measures and renders all active layers. + * + * In async mode the previous recompose job is joined before rendering, then a new + * job is launched if there are pending frame waiters. + */ + open fun renderNodes(baseLayer: Boolean, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + // See ComposeScreen.renderNodes for why this runs first. + testPump?.invoke() + if (asynchronous) { + recomposeJob?.let { job -> + runBlocking { + job.join() + } + recomposeJob = null + } + } else if (hasFrameWaiters) { + hasFrameWaiters = false + clock.sendFrame(System.nanoTime()) + } + + // layerManager.layers is a SnapshotStateList that can be structurally mutated (modal + // push/dismiss) from the recomposer coroutine on another thread while this method runs on + // the render thread. Reading it inside a snapshot gives a frozen, consistent view for the + // whole size-check-then-index sequence below, instead of racing the live list. A mutable + // (not read-only) snapshot is required because measure() can itself write state (e.g. + // ScrollableState.setChildSize), and those writes must be applied back afterward. + val layersSnapshot = Snapshot.takeMutableSnapshot() + try { + layersSnapshot.enter { + val layerIndices = if (baseLayer) { + if (layerManager.layers.isEmpty()) return@enter + 0..0 + } else { + if (layerManager.layers.size <= 1) return@enter + 1 until layerManager.layers.size + } + + for (layerIndex in layerIndices) { + val layer = layerManager.layers[layerIndex] + val rootNode = layer.rootNode + rootNode.measure(Constraints(maxWidth = width, maxHeight = height)) + rootNode.render(0, 0, guiGraphics, mouseX, mouseY, partialTick, layerBaseZ(layerIndex)) + } + + layerManager.screenSize.let { (width, height) -> + imageWidth = width + imageHeight = height + } + layerManager.screenPos.let { (x, y) -> + if (x == 0 && y == 0) + return@let + leftPos = x + topPos = y + menu.screenLeftPos = leftPos + menu.screenTopPos = topPos + } + menu.refreshSlotPositions() + + if (asynchronous && hasFrameWaiters) { + hasFrameWaiters = false + recomposeJob = composeScope.launch { + clock.sendFrame(System.nanoTime()) + } + } + } + layersSnapshot.apply().check() + } finally { + layersSnapshot.dispose() + } + } + + override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + super.render(guiGraphics, mouseX, mouseY, partialTick) + renderTooltip(guiGraphics, mouseX, mouseY) + if (layerManager.layers.size > 1) + { + renderNodes(false, guiGraphics, mouseX, mouseY, partialTick) + } + } + + override fun isHovering( + x: Int, + y: Int, + width: Int, + height: Int, + mouseX: Double, + mouseY: Double + ): Boolean + { + if (layerManager.layers.size != 1) return false + + if (width == 16 && height == 16) { + val slotIndex = menu.slots.indexOfFirst { it.x == x && it.y == y } + val clip = if (slotIndex >= 0) menu.slotClipBounds(slotIndex) else null + if (clip != null) { + val absX = leftPos + x + val absY = topPos + y + val visible = IntRect(absX, absY, absX + width, absY + height).intersect(clip) ?: return false + return mouseX >= visible.minX - 1 && mouseX < visible.maxX + 1 && + mouseY >= visible.minY - 1 && mouseY < visible.maxY + 1 + } + } + + return super.isHovering(x, y, width, height, mouseX, mouseY) + } + + override fun renderBg(guiGraphics: GuiGraphics, partialTick: Float, mouseX: Int, mouseY: Int) { + renderNodes(true, guiGraphics, mouseX, mouseY, partialTick) + } + + /** + * Hook point for slot rendering customisation. + * + * By default, slots are clipped against the container bounds so partially visible + * slots still render correctly when Compose repositions them. + */ + override fun renderSlot(guiGraphics: GuiGraphics, slot: Slot) { + val clip = slotClipRect(slot) ?: return + + guiGraphics.enableScissor(clip.minX, clip.minY, clip.maxX, clip.maxY) + try { + super.renderSlot(guiGraphics, slot) + } finally { + guiGraphics.disableScissor() + } + } + + /** + * Called by [net.kernelpanicsoft.archie.mixin.client.gui.AbstractContainerScreenMixin] + * (via [net.kernelpanicsoft.archie.gui.access.SlotHighlightClipProvider]) to clip the + * hover-highlight overlay the same way [renderSlot] clips the item icon - vanilla only + * exposes a static `renderSlotHighlight(GuiGraphics, x, y, blitOffset)` with no per-slot + * override point, so this has to be reached from a mixin redirect instead of `override`. + */ + override fun slotHighlightClipRect(x: Int, y: Int): IntRect? { + val slot = menu.slots.firstOrNull { it.x == x && it.y == y } ?: return null + return slotClipRect(slot) + } + + override fun slotRenderLayerOffset(slot: Slot): Float? = slotRenderLayerZ(slot) + + /** + * Z layer used when rendering a specific vanilla [slot]. + * + * Slots render above the compose content of the layer that owns them, while + * still remaining below content from higher layers. + */ + protected open fun slotRenderLayerZ(slot: Slot): Float { + val slotIndex = menu.slots.indexOf(slot).takeIf { it >= 0 } ?: return layerBaseZ(0) + SLOT_LAYER_OFFSET + val layerDepth = menu.slotLayerDepth(slotIndex) + return layerBaseZ(layerDepth) + SLOT_LAYER_OFFSET + } + + protected open fun slotRenderLayerZ(): Float = layerBaseZ(0) + SLOT_LAYER_OFFSET + + /** + * Computes the clip rectangle for [slot] in absolute screen coordinates. + * + * Intersects the overall container bounds with the slot's own group clip (if it sits + * inside a [net.kernelpanicsoft.archie.gui.composables.containers.Scrollable] viewport), + * so a slot scrolled out of view is actually clipped instead of rendering on top of + * whatever else occupies that screen area. + * + * Returns `null` when the slot does not intersect the (possibly narrower) clip area. + */ + protected open fun slotClipRect(slot: Slot): IntRect? { + val containerClip = IntRect(leftPos, topPos, leftPos + imageWidth, topPos + imageHeight) + val slotIndex = menu.slots.indexOf(slot).takeIf { it >= 0 } + val groupClip = slotIndex?.let { menu.slotClipBounds(it) } + val effectiveClip = groupClip?.let { containerClip.intersect(it) } ?: containerClip + + val slotMinX = leftPos + slot.x + val slotMinY = topPos + slot.y + val slotRect = IntRect(slotMinX, slotMinY, slotMinX + 16, slotMinY + 16) + + return effectiveClip.intersect(slotRect) + } + + private var composeDisposed = false + + override fun onClose() { + GLFW.glfwSetCursor(minecraft!!.window.window, 0L) + super.onClose() + disposeCompose() + } + + // See ComposeScreen.removed()'s doc comment - vanilla's Minecraft.setScreen() calls removed() + // on the *old* screen for every transition, not just onClose()'s explicit-close path, and + // without this override this screen's entire Compose runtime leaked on any such swap. + override fun removed() { + super.removed() + disposeCompose() + } + + private fun disposeCompose() { + if (composeDisposed) return + composeDisposed = true + recomposeJob?.cancel("GUI closing") + recomposer.close() + snapshotHandle.dispose() + layerManager.layers.forEach { it.dispose() } + AUIScopeManager.scopes -= composeScope + composeScope.cancel() + } + + private fun getTopNode(): LayoutNode? = layerManager.top?.rootNode + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + val topNode = getTopNode() ?: return super.mouseClicked(mouseX, mouseY, button) + processPointerEvent(topNode, mouseX, mouseY, PointerEventType.GLOBAL_PRESS, true) + val event = processPointerEvent(topNode, mouseX, mouseY, PointerEventType.PRESS) + return event.bypassSuper || super.mouseClicked(mouseX, mouseY, button) + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + val topNode = getTopNode() ?: return super.mouseReleased(mouseX, mouseY, button) + processPointerEvent(topNode, mouseX, mouseY, PointerEventType.GLOBAL_RELEASE, true) + val event = processPointerEvent(topNode, mouseX, mouseY, PointerEventType.RELEASE) + return event.bypassSuper || super.mouseReleased(mouseX, mouseY, button) + } + + override fun mouseMoved(mouseX: Double, mouseY: Double) { + val topNode = getTopNode() ?: return super.mouseMoved(mouseX, mouseY) + processPointerEvent(topNode, mouseX, mouseY, PointerEventType.MOVE) + + processPointerEvent( + topNode, + mouseX, + mouseY, + PointerEventType.ENTER + ) { + it.isBounded(mouseX.toInt(), mouseY.toInt()) && !it.isBounded( + lastMouseX.toInt(), + lastMouseY.toInt() + ) + } + + processPointerEvent( + topNode, + mouseX, + mouseY, + PointerEventType.EXIT + ) { + !it.isBounded(mouseX.toInt(), mouseY.toInt()) && it.isBounded( + lastMouseX.toInt(), + lastMouseY.toInt() + ) + } + + lastMouseX = mouseX + lastMouseY = mouseY + super.mouseMoved(mouseX, mouseY) + } + + override fun mouseScrolled( + mouseX: Double, + mouseY: Double, + scrollX: Double, + scrollY: Double + ): Boolean { + val topNode = getTopNode() ?: return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY) + processScrollEvent(topNode, mouseX, mouseY, scrollX, scrollY, PointerEventType.GLOBAL_SCROLL, true) + val event = + processScrollEvent(topNode, mouseX, mouseY, scrollX, scrollY, PointerEventType.SCROLL) + return event.bypassSuper || super.mouseScrolled(mouseX, mouseY, scrollX, scrollY) + } + + override fun mouseDragged( + mouseX: Double, + mouseY: Double, + button: Int, + dragX: Double, + dragY: Double + ): Boolean { + val topNode = + getTopNode() ?: return super.mouseDragged(mouseX, mouseY, button, dragX, dragY) + val event = + processDragEvent(topNode, mouseX, mouseY, button, dragX, dragY, PointerEventType.DRAG) + return event.bypassSuper || super.mouseDragged(mouseX, mouseY, button, dragX, dragY) + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + val topNode = getTopNode() ?: return super.keyPressed(keyCode, scanCode, modifiers) + // Ctrl+Shift+D toggles the debug overlay. A bitwise check (not `modifiers == 3`) is + // required here - GLFW also sets bits for Caps Lock/Num Lock in `modifiers` when those + // are active, so an exact-equality check against just the Ctrl+Shift bitmask silently + // never matches on those systems. + val ctrlShiftMask = GLFW.GLFW_MOD_CONTROL or GLFW.GLFW_MOD_SHIFT + if (keyCode == InputConstants.KEY_D && (modifiers and ctrlShiftMask) == ctrlShiftMask) { + topNode.debug = !topNode.debug + } + if (topNode.debug && keyCode == InputConstants.KEY_LSHIFT) topNode.extraDebug = true + + val event = processKeyEvent(topNode, keyCode, scanCode, modifiers) + return event.bypassSuper || super.keyPressed(keyCode, scanCode, modifiers) + } + + override fun charTyped(codePoint: Char, modifiers: Int): Boolean { + val topNode = getTopNode() ?: return super.charTyped(codePoint, modifiers) + val event = processCharEvent(topNode, codePoint, modifiers) + return event.bypassSuper || super.charTyped(codePoint, modifiers) + } + + override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + val baseNode = layerManager.layers.firstOrNull()?.rootNode + if (baseNode != null && baseNode.debug && keyCode == InputConstants.KEY_LSHIFT) { + baseNode.extraDebug = false + } + return super.keyReleased(keyCode, scanCode, modifiers) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt new file mode 100644 index 000000000..9a27044e3 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt @@ -0,0 +1,335 @@ +package net.kernelpanicsoft.archie.gui + +import androidx.compose.runtime.* +import androidx.compose.runtime.snapshots.Snapshot +import com.mojang.blaze3d.platform.InputConstants +import kotlinx.coroutines.* +import net.kernelpanicsoft.archie.gui.layer.LayerStackManager +import net.kernelpanicsoft.archie.gui.layer.LocalLayerManager +import net.kernelpanicsoft.archie.gui.layout.* +import net.kernelpanicsoft.archie.gui.modifiers.Constraints +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.fillMaxSize +import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType +import net.kernelpanicsoft.archie.gui.util.extension.processCharEvent +import net.kernelpanicsoft.archie.gui.util.extension.processDragEvent +import net.kernelpanicsoft.archie.gui.util.extension.processKeyEvent +import net.kernelpanicsoft.archie.gui.util.extension.processPointerEvent +import net.kernelpanicsoft.archie.gui.util.extension.processScrollEvent +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.gui.screens.Screen +import net.minecraft.network.chat.Component +import org.lwjgl.glfw.GLFW +import kotlin.coroutines.CoroutineContext + +/** Provides the current [ComposeScreen] to any composable in its tree. */ +val LocalScreen: ProvidableCompositionLocal = + compositionLocalOf { throw IllegalStateException("Screen has not been provided") } + +/** + * Implemented by Compose-driven screens that recompose asynchronously, so test harnesses can + * poll for a settled frame (no pending or in-flight recomposition) before asserting on rendered + * output - e.g. before taking a screenshot right after simulating a click. + */ +internal interface ComposeIdleAware { + /** `true` when there is no snapshot-write notification, frame request, or recompose job pending. */ + fun isComposeIdle(): Boolean +} + +/** Implemented by hosts (screens) that own a [LayerStackManager] for their layer stack. */ +interface LayerManagerProvider +{ + /** The layer stack owned by this host. */ + val layerManager: LayerStackManager +} + +/** + * Test-only override, installed by the client GameTest harness (`AClientGameTestHarness.kt`), + * that swaps [ComposeScreen]'s real-time coroutine dispatcher for a virtual one so a composable's + * `delay(...)` (e.g. a dialog's close animation) resolves deterministically against a scheduler + * the harness drives itself, instead of racing real wall-clock time, real thread scheduling, and + * the harness's tick-based polling - the same class of flakiness Jetpack Compose's own test + * tooling (`ComposeTestRule`/`runComposeUiTest`) avoids by construction, backing composition with + * a virtual clock/dispatcher that `waitForIdle()` drives forward instead of polling real + * concurrency. `null` unless a client GameTest explicitly installs one; the running game never + * touches this. + * + * Deliberately untyped against `kotlinx-coroutines-test` (`CoroutineDispatcher` is a + * kotlinx-coroutines-core type; `pump` is a plain lambda) - that dependency is dev/test-only + * (`compileOnly` in `common`, `runtimeLibrary` - present for local runs, never bundled - in the + * loader modules; see `common/build.gradle.kts`). [ComposeScreen] is loaded by every screen in + * the mod, so its own class file must never reference a symbol that isn't resolvable in a real + * player's game; only [AClientGameTestHarness]'s method bodies (never invoked outside + * `AGameTestPlatform.isGameTest`) construct the actual `StandardTestDispatcher`/ + * `TestCoroutineScheduler` instances installed here. + */ +internal object ComposeTestClockOverride { + /** The dispatcher to back new [ComposeScreen]s' coroutine scope with, in place of [kotlinx.coroutines.Dispatchers.Default]. */ + @Volatile + var dispatcher: CoroutineDispatcher? = null + + /** + * Called once per real rendered frame by every live [ComposeScreen] while installed - drains + * all outstanding virtual-time work (recomposition, effects, `delay(...)`) synchronously on + * the render thread. Set alongside [dispatcher] to `{ scheduler.advanceUntilIdle() }`. + */ + @Volatile + var pump: (() -> Unit)? = null +} + +/** + * A Compose-driven Minecraft [Screen] base class with layer support, async recomposition, + * and full pointer/keyboard input dispatch. + * + * Extend this class and call [start] inside your `init()` override: + * + * ```kotlin + * class MyScreen : ComposeScreen(Component.literal("My Screen")) { + * override fun init() { + * super.init() + * start { + * Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + * Text(Component.literal("Hello!")) + * } + * } + * } + * } + * ``` + * + * @param title The screen title passed to the vanilla [Screen] constructor. + * @param asynchronous When `true` (default), recomposition runs off the main thread and + * the result is joined at the start of the next frame for smooth, non-blocking updates. + * Set to `false` to force synchronous recomposition (simpler but may stutter). + */ +abstract class ComposeScreen( + title: Component, + val asynchronous: Boolean = true, +) : Screen(title), CoroutineScope, ComposeIdleAware, LayerManagerProvider { + + private var hasFrameWaiters = false + private val clock = BroadcastFrameClock { hasFrameWaiters = true } + + // Captured once at construction (not read live from ComposeTestClockOverride elsewhere) so + // this screen keeps working consistently even if a later test installs/clears the override + // while this screen is still disposing. + private val testPump = ComposeTestClockOverride.pump + + private val composeScope = CoroutineScope(ComposeTestClockOverride.dispatcher ?: Dispatchers.Default) + clock + final override val coroutineContext: CoroutineContext = composeScope.coroutineContext + + final override lateinit var layerManager: LayerStackManager + private set + private lateinit var recomposer: Recomposer + private var recomposeJob: Job? = null + + private var applyScheduled = false + private val snapshotHandle = Snapshot.registerGlobalWriteObserver { + if (!applyScheduled) { + applyScheduled = true + composeScope.launch { + applyScheduled = false + Snapshot.sendApplyNotifications() + } + } + } + + // Not 0.0 - a node can legitimately sit at the literal origin (e.g. the first item in a + // top-left-aligned Column), and mouseMoved()'s ENTER condition (`nowBounded && !wasBounded`) + // would then read the initial "no prior position" sentinel as if the mouse had already been + // sitting inside that node before any real movement, silently suppressing its very first + // ENTER event. No real screen coordinate is ever negative, so this can never coincide. + private var lastMouseX = Double.NEGATIVE_INFINITY + private var lastMouseY = Double.NEGATIVE_INFINITY + + // `Recomposer.hasPendingWork` is Compose's own atomically-maintained "is there recomposition, + // apply-changes, or effect work outstanding" signal - the same one Compose's own test tooling + // (ComposeTestRule.waitForIdle()) uses. Reimplementing this by hand via applyScheduled/ + // hasFrameWaiters/recomposeJob had a real gap: recomposeJob only wraps `clock.sendFrame(...)`, + // and resuming a dispatched withFrameNanos continuation doesn't block until that continuation's + // *own* subsequent work (the actual recompose + apply-changes) finishes - it's dispatched, not + // synchronous. So recomposeJob could complete (and isComposeIdle() report idle) while the + // Recomposer was still mid-flight applying the very change a test's click()/hover() just + // triggered, letting an assertion race a stale pre-interaction render. hasPendingWork has no + // such gap since the Recomposer updates it itself as part of the same state transition. + override fun isComposeIdle(): Boolean = !recomposer.hasPendingWork + + /** + * Initialises the Compose runtime and pushes the base layer with [content]. + * + * Must be called once from [init]. Subsequent calls replace the content. + * + * @param content The root composable content for this screen. + */ + protected fun start(content: @Composable () -> Unit) { + recomposer = Recomposer(coroutineContext) + layerManager = LayerStackManager(recomposer) + + AUIScopeManager.scopes += composeScope + launch { recomposer.runRecomposeAndApplyChanges() } + + layerManager.push { _ -> + CompositionLocalProvider( + LocalScreen provides this, + LocalLayerManager provides layerManager, + ) { + Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { + content() + } + } + } + } + + // ── Rendering ───────────────────────────────────────────────────────── + + /** + * Measures and renders all active layers. + * + * In async mode the previous recompose job is joined before rendering, then a new + * job is launched if there are pending frame waiters. + */ + open fun renderNodes(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + // Under a client GameTest's virtual dispatcher (see ComposeTestClockOverride), nothing + // launched on composeScope runs on its own - it only progresses when the scheduler backing + // it is advanced. Doing that here, once per real rendered frame, deterministically flushes + // recomposition/effects/delay() before the join below, instead of racing real threads. + testPump?.invoke() + if (asynchronous) { + recomposeJob?.let { runBlocking { it.join() } } + recomposeJob = null + } else if (hasFrameWaiters) { + hasFrameWaiters = false + clock.sendFrame(System.nanoTime()) + } + + val layersSnapshot = Snapshot.takeMutableSnapshot() + try { + layersSnapshot.enter { + var zOffset = 0f + for (layer in layerManager.layers) { + val root = layer.rootNode + root.measure(Constraints(maxWidth = width, maxHeight = height)) + root.render(0, 0, guiGraphics, mouseX, mouseY, partialTick, zOffset) + zOffset = root.getMaxZ(zOffset) + 10f + } + } + layersSnapshot.apply().check() + } finally { + layersSnapshot.dispose() + } + + if (asynchronous && hasFrameWaiters) { + hasFrameWaiters = false + recomposeJob = composeScope.launch { clock.sendFrame(System.nanoTime()) } + } + setInitialFocus() + } + + override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + super.render(guiGraphics, mouseX, mouseY, partialTick) + renderNodes(guiGraphics, mouseX, mouseY, partialTick) + } + + // ── Lifecycle ───────────────────────────────────────────────────────── + + private var composeDisposed = false + + override fun onClose() { + GLFW.glfwSetCursor(minecraft!!.window.window, 0L) + super.onClose() + disposeCompose() + } + + // vanilla's Minecraft.setScreen() calls removed() on the *old* screen for every screen + // transition - including a caller directly swapping to a new screen, which never goes + // through onClose() at all (that only fires when a screen closes itself, e.g. Escape). + // Without this override, every such swap - including the GameTest harness moving from one + // test's screen straight to the next's - leaked this screen's entire Compose runtime + // (Recomposer, composeScope and all its coroutines) running forever in the background. + // Confirmed the mechanism (not yet reproduced standalone): a CI-only crash deep inside + // Compose's own SlotTable/Recomposer internals surfaced as a suppressed exception logged + // between two unrelated, otherwise-passing tests - consistent with a leaked prior screen's + // recomposer still running concurrently against Compose-runtime state a newer screen's + // recomposition is also touching. + override fun removed() { + super.removed() + disposeCompose() + } + + private fun disposeCompose() { + if (composeDisposed) return + composeDisposed = true + recomposeJob?.cancel("GUI closing") + recomposer.close() + snapshotHandle.dispose() + layerManager.layers.forEach { it.dispose() } + AUIScopeManager.scopes -= composeScope + composeScope.cancel() + } + + // ── Input ───────────────────────────────────────────────────────────── + + private fun topNode() = layerManager.top?.rootNode + + override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { + val top = topNode() ?: return super.mouseClicked(mouseX, mouseY, button) + processPointerEvent(top, mouseX, mouseY, PointerEventType.GLOBAL_PRESS, global = true) + val event = processPointerEvent(top, mouseX, mouseY, PointerEventType.PRESS) + return event.bypassSuper || super.mouseClicked(mouseX, mouseY, button) + } + + override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { + val top = topNode() ?: return super.mouseReleased(mouseX, mouseY, button) + processPointerEvent(top, mouseX, mouseY, PointerEventType.GLOBAL_RELEASE, global = true) + val event = processPointerEvent(top, mouseX, mouseY, PointerEventType.RELEASE) + return event.bypassSuper || super.mouseReleased(mouseX, mouseY, button) + } + + override fun mouseMoved(mouseX: Double, mouseY: Double) { + val top = topNode() ?: return super.mouseMoved(mouseX, mouseY) + processPointerEvent(top, mouseX, mouseY, PointerEventType.MOVE) + processPointerEvent(top, mouseX, mouseY, PointerEventType.ENTER) { + it.isBounded(mouseX.toInt(), mouseY.toInt()) && !it.isBounded(lastMouseX.toInt(), lastMouseY.toInt()) + } + processPointerEvent(top, mouseX, mouseY, PointerEventType.EXIT) { + !it.isBounded(mouseX.toInt(), mouseY.toInt()) && it.isBounded(lastMouseX.toInt(), lastMouseY.toInt()) + } + lastMouseX = mouseX; lastMouseY = mouseY + super.mouseMoved(mouseX, mouseY) + } + + override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean { + val top = topNode() ?: return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY) + processScrollEvent(top, mouseX, mouseY, scrollX, scrollY, PointerEventType.GLOBAL_SCROLL, global = true) + val event = processScrollEvent(top, mouseX, mouseY, scrollX, scrollY, PointerEventType.SCROLL) + return event.bypassSuper || super.mouseScrolled(mouseX, mouseY, scrollX, scrollY) + } + + override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, dragX: Double, dragY: Double): Boolean { + val top = topNode() ?: return super.mouseDragged(mouseX, mouseY, button, dragX, dragY) + val event = processDragEvent(top, mouseX, mouseY, button, dragX, dragY, PointerEventType.DRAG) + return event.bypassSuper || super.mouseDragged(mouseX, mouseY, button, dragX, dragY) + } + + override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + val top = topNode() ?: return super.keyPressed(keyCode, scanCode, modifiers) + val base = layerManager.layers.firstOrNull()?.rootNode + if (base != null) { + if (keyCode == InputConstants.KEY_LSHIFT && modifiers == 3) base.debug = !base.debug + if (base.debug && keyCode == InputConstants.KEY_LSHIFT) base.extraDebug = true + } + val event = processKeyEvent(top, keyCode, scanCode, modifiers) + return event.bypassSuper || super.keyPressed(keyCode, scanCode, modifiers) + } + + override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { + val base = layerManager.layers.firstOrNull()?.rootNode + if (base != null && base.debug && keyCode == InputConstants.KEY_LSHIFT) base.extraDebug = false + return super.keyReleased(keyCode, scanCode, modifiers) + } + + override fun charTyped(codePoint: Char, modifiers: Int): Boolean { + val top = topNode() ?: return super.charTyped(codePoint, modifiers) + val event = processCharEvent(top, codePoint, modifiers) + return event.bypassSuper || super.charTyped(codePoint, modifiers) + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/Slot.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/Slot.kt new file mode 100644 index 000000000..4c41abe30 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/Slot.kt @@ -0,0 +1,295 @@ +package net.kernelpanicsoft.archie.gui + +import androidx.compose.runtime.* +import kotlinx.serialization.Serializable +import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates +import net.kernelpanicsoft.archie.gui.layer.LocalLayerDepth +import net.kernelpanicsoft.archie.gui.layout.* +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.onGloballyPositioned +import net.kernelpanicsoft.archie.gui.modifiers.position.padding +import net.kernelpanicsoft.archie.gui.modifiers.sizeIn +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.theme.LocalTheme +import net.kernelpanicsoft.archie.gui.theme.ThemeVariants +import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState +import net.kernelpanicsoft.archie.gui.composables.containers.Scrollable +import net.minecraft.client.gui.GuiGraphics + +/** + * Per-slot-group layout data reported back from the Compose layout to [ComposeContainerMenuBase]. + * + * Stores the group's absolute screen position, dimensions, slot positions, and clip bounds. + */ +@Serializable +data class SlotGroup( + var pos: IntCoordinates = IntCoordinates(0, 0), + var size: IntSize = IntSize(0, 0), + var enabled: Boolean = true, + var layerDepth: Int = 0, + var slots: MutableSet = mutableSetOf(), + var clip: IntRect? = null, +) + +/** + * Aggregated slot layout data for an entire [ComposeContainerScreen]. + * + * Contains named groups for block-entity slots and a separate [playerGroup] for the + * player inventory rows. + */ +@Serializable +data class SlotData( + val groups: MutableMap = mutableMapOf(), + val playerGroup: SlotGroup = SlotGroup(size = IntSize(9, 3)), +) { + /** All slot coordinates across all groups (does not include [playerGroup]). */ + val slots: Set get() = groups.values.filter { it.enabled }.flatMap { it.slots }.toSet() +} + +/** Provides the [SlotData] to all composables within a [ComposeContainerScreen]. */ +val LocalSlotData = compositionLocalOf { SlotData() } + +/** Provides the current [SlotGroup] to [Slot] composables inside a [Slots] container. */ +val LocalSlotGroup = compositionLocalOf { SlotGroup() } + +/** + * A layout-synchronous (non-Compose-state) holder for a [Scrollable]'s + * current clip bounds. + * + * The [Scrollable] updates [bounds] directly from its `onGloballyPositioned`/`onSizeChanged` + * callbacks, which fire every layout pass regardless of composition state. A descendant [Slot] + * reads [bounds] live, from its own `onGloballyPositioned` callback, at the same layout-pass + * granularity as [SlotGroup.pos]. Using [androidx.compose.runtime.mutableStateOf] here instead + * would only propagate the new value on the *next* recomposition - a composition-cycle lag + * behind position tracking that let a slot's clip bounds go stale exactly when the surrounding + * layout had just finished settling into a new position. + */ +class SlotClipSource { + private var origin: IntCoordinates = IntCoordinates(0, 0) + private var size: Size = Size(0, 0) + + var bounds: IntRect? = null + private set + + fun updateOrigin(newOrigin: IntCoordinates) { + origin = newOrigin + recompute() + } + + fun updateSize(newSize: Size) { + size = newSize + recompute() + } + + private fun recompute() { + bounds = if (size.width <= 0 || size.height <= 0) null else IntRect.fromPositionAndSize(origin, size) + } +} + +/** Provides the active [SlotClipSource] (if any) from the nearest ancestor scroll/clip container. */ +val LocalSlotClipBounds = compositionLocalOf { null } + +/** + * Defines a named region of inventory slots within a [ComposeContainerScreen]. + * + * This composable tracks its absolute on-screen position and populates the enclosing + * [SlotData] with the group's location and dimensions so that [ComposeContainerMenuBase] can + * register the corresponding vanilla [net.minecraft.world.inventory.Slot]s. + * + * @param id The name that matches the `handler(id, storage)` call in your menu. + * @param width The number of slot columns in this group. Defaults to 1. + * @param height The number of slot rows in this group. Defaults to 1. + * @param content The composable [Slot] grid inside this region. + * @return The [SlotGroup] that will be populated once the layout runs. + */ +@Composable +fun Slots( + id: String, + width: Int = 1, + height: Int = 1, + content: @Composable () -> Unit = { + Column { + repeat(height) { + Row { + repeat(width) { + Slot() + } + } + } + } + }, +): SlotGroup { + val layerDepth = LocalLayerDepth.current + val clipSource = LocalSlotClipBounds.current + val group = remember(id) { SlotGroup(size = IntSize(width = width, height = height)) } + group.size = IntSize(width = width, height = height) + group.layerDepth = layerDepth + val data = LocalSlotData.current + data.groups[id] = group + group.clip = clipSource?.bounds + + DisposableEffect(data, id, group) { + data.groups[id] = group + group.enabled = true + onDispose { + group.enabled = false + } + } + + // Clear slots so re-layout starts fresh each composition pass + group.slots.clear() + + Box( + modifier = Modifier.onGloballyPositioned { coords -> + group.pos = coords + group.layerDepth = layerDepth + // Live read, not a composition-time snapshot - see SlotClipSource. + group.clip = clipSource?.bounds + data.groups[id] = group + } + ) { + CompositionLocalProvider(LocalSlotGroup provides group) { + content() + } + } + return group +} + +/** + * Renders a single inventory slot graphic and records its absolute screen position. + * + * Triggers [ComposeContainerMenuBase.updateSlotData] once **all** named groups and the player + * group have reported their positions for this layout pass. + * + * @param modifier Additional modifiers applied to the slot layout node. + */ +@Composable +fun Slot(texture: String = "slot", modifier: Modifier = Modifier) { + val data = LocalSlotData.current + val group = LocalSlotGroup.current + val menu = LocalContainerMenu.current + val theme = LocalTheme.current + val composableTheme = theme.getComposableTheme(texture) + val state = composableTheme.getState(TextureStates.DEFAULT, ThemeVariants.DEFAULT) + var lastPos by remember { mutableStateOf(IntCoordinates(0, 0)) } + Layout( + name = "Slot", + measurePolicy = { _, _, constraints -> + MeasureResult(constraints.minWidth, constraints.minHeight) {} + }, + renderer = object : Renderer { + override fun render( + node: UINode, x: Int, y: Int, + guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, + ) { + guiGraphics.drawThemeState(state, x, y, node.width, node.height) + + super.render(node, x, y, guiGraphics, mouseX, mouseY, partialTick) + } + }, + modifier = Modifier + .sizeIn(minWidth = 18, minHeight = 18) + .onGloballyPositioned { pos -> + if (pos == lastPos) return@onGloballyPositioned // Skip if position hasn't changed since last report + if (group.slots.contains(lastPos)) + group.slots.remove(lastPos) + group.slots.add(pos) + lastPos = pos + tryUpdateMenu(data, menu) + } + .then(modifier), + ) +} + +/** + * Renders the standard 4-row player inventory (3 main rows + hotbar) as [Slot] composables. + * + * The 58-pixel gap between the main inventory and the hotbar matches the pixel offset used + * by [ComposeContainerMenuBase.addPlayerSlots] so positions reported to the menu are consistent. + */ +@Composable +fun PlayerSlots() { + val data = LocalSlotData.current + val layerDepth = LocalLayerDepth.current + val clipSource = LocalSlotClipBounds.current + data.playerGroup.layerDepth = layerDepth + data.playerGroup.clip = clipSource?.bounds + + // Clear so re-layout starts fresh + data.playerGroup.slots.clear() + + Box( + modifier = Modifier.onGloballyPositioned { coords -> + data.playerGroup.pos = coords + data.playerGroup.layerDepth = layerDepth + // Live read, not a composition-time snapshot - see SlotClipSource. + data.playerGroup.clip = clipSource?.bounds + } + ) { + Column { + // 3 rows of 9 (main inventory) + repeat(3) { + Row { + repeat(9) { + PlayerSlot() + } + } + } + // Hotbar (1 row of 9) + Row(modifier = Modifier.padding(top = 4)) { + repeat(9) { PlayerSlot() } + } + } + } +} + +/** A single player-inventory slot cell that tracks its position in [SlotData.playerGroup]. */ +@Composable +private fun PlayerSlot(texture: String = "slot", modifier: Modifier = Modifier) { + val data = LocalSlotData.current + val menu = LocalContainerMenu.current + val theme = LocalTheme.current + val composableTheme = theme.getComposableTheme(texture) + val state = composableTheme.getState(TextureStates.DEFAULT, ThemeVariants.DEFAULT) + var lastPos by remember { mutableStateOf(IntCoordinates(0, 0)) } + Layout( + name = "PlayerSlot", + measurePolicy = { _, _, constraints -> + MeasureResult(constraints.minWidth, constraints.minHeight) {} + }, + renderer = object : Renderer { + override fun render( + node: UINode, x: Int, y: Int, + guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, + ) { + guiGraphics.drawThemeState(state, x, y, node.width, node.height) + + super.render(node, x, y, guiGraphics, mouseX, mouseY, partialTick) + } + }, + modifier = modifier + .sizeIn(minWidth = 18, minHeight = 18) + .onGloballyPositioned { pos -> + if (pos == lastPos) return@onGloballyPositioned // Skip if position hasn't changed since last report + if (data.playerGroup.slots.contains(lastPos)) + data.playerGroup.slots.remove(lastPos) + data.playerGroup.slots.add(pos) + lastPos = pos + tryUpdateMenu(data, menu) + }, + ) +} + +/** + * Fires [ComposeContainerMenuBase.updateSlotData] only when every named slot group AND the + * player group have all reported their slot positions for this layout pass. + * + * This prevents partial updates where only some groups are positioned. + */ +private fun tryUpdateMenu(data: SlotData, menu: ComposeContainerMenuBase<*>) { + val namedGroupsFull = data.groups.values.all { g -> g.slots.size >= g.size.width * g.size.height } + val playerGroupFull = data.playerGroup.slots.size >= 36 + if (namedGroupsFull && playerGroupFull) { + menu.updateSlotData(data) + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotHighlightClipProvider.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotHighlightClipProvider.kt new file mode 100644 index 000000000..ad1e2396f --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotHighlightClipProvider.kt @@ -0,0 +1,17 @@ +package net.kernelpanicsoft.archie.gui.access + +import net.kernelpanicsoft.archie.gui.layout.IntRect + +/** + * Allows container screens to clip vanilla's static `renderSlotHighlight(GuiGraphics, x, y, + * blitOffset)` call to a sub-rect of the slot's own 16x16 bounds. Returning `null` skips the + * highlight draw entirely (fully clipped away); returning the full unclipped rect renders it + * normally. + */ +interface SlotHighlightClipProvider { + /** + * Returns the sub-rect (in slot-local pixel coordinates) to clip the slot highlight to at + * screen position [x], [y], or `null` to skip the highlight entirely. + */ + fun slotHighlightClipRect(x: Int, y: Int): IntRect? +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthProvider.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthProvider.kt new file mode 100644 index 000000000..f2a41b048 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthProvider.kt @@ -0,0 +1,12 @@ +package net.kernelpanicsoft.archie.gui.access + +import net.minecraft.world.inventory.Slot + +/** + * Allows container screens to tweak the Z depth used when vanilla renders a [Slot]. + * Returning `null` keeps Minecraft's default blit offset. + */ +interface SlotLayerDepthProvider { + fun slotRenderLayerOffset(slot: Slot): Float? = null +} + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/animation/Animation.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/animation/Animation.kt new file mode 100644 index 000000000..71af0cec7 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/animation/Animation.kt @@ -0,0 +1,94 @@ +package net.kernelpanicsoft.archie.gui.animation + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableFloatStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import androidx.compose.runtime.withFrameNanos +import kotlin.math.roundToInt +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +/** Describes a time-based interpolation used by Archie GUI animations. */ +fun interface Easing { + /** Maps a linear progress [fraction] in `0f..1f` to an eased progress value. */ + fun transform(fraction: Float): Float +} + +/** + * Common easing curves for small UI interactions. + * + * The curves are intentionally lightweight so they can run smoothly in frequent recompositions. + */ +object Easings { + /** No easing; progress is directly proportional to elapsed time. */ + val Linear = Easing { it } + + /** Starts fast and decelerates into the target value, with no overshoot. */ + val OutCubic = Easing { t -> + val inv = 1f - t + 1f - inv * inv * inv + } + + /** Like [OutCubic] but overshoots the target slightly before settling. */ + val OutBack = Easing { t -> + val c1 = 1.70158f + val c3 = c1 + 1f + val shifted = t - 1f + 1f + c3 * shifted * shifted * shifted + c1 * shifted * shifted + } +} + +/** + * Timing parameters for float/int animations. + * + * @param durationMillis How long the animation takes to reach its target value. + * @param easing The curve applied to progress over that duration. + */ +data class AnimationSpec( + val durationMillis: Duration = 220.milliseconds, + val easing: Easing = Easings.OutCubic, +) + +/** Animates a float value toward [targetValue] using [spec]. */ +@Composable +fun animateFloat(targetValue: Float, spec: AnimationSpec = AnimationSpec()): Float { + var value by remember { mutableFloatStateOf(targetValue) } + + LaunchedEffect(targetValue, spec.durationMillis, spec.easing) { + val duration = spec.durationMillis + if (duration <= 0.milliseconds) { + value = targetValue + return@LaunchedEffect + } + + val start = value + val delta = targetValue - start + if (delta == 0f) return@LaunchedEffect + + val startTime = withFrameNanos { it } + var frameTime = startTime + do { + val elapsedNanos = frameTime - startTime + val rawProgress = (elapsedNanos / (duration.inWholeMilliseconds * 1_000_000f)).coerceIn(0f, 1f) + val eased = spec.easing.transform(rawProgress) + value = start + delta * eased + frameTime = withFrameNanos { it } + } while (rawProgress < 1f) + + value = targetValue + } + + return value +} + +/** Animates an integer by interpolating as float and rounding to the nearest pixel. */ +@Composable +fun animateInt(targetValue: Int, spec: AnimationSpec = AnimationSpec()): Int { + val animatedFloat = animateFloat(targetValue.toFloat(), spec) + return animatedFloat.roundToInt() +} + + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateComposables.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateComposables.kt new file mode 100644 index 000000000..c84610858 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateComposables.kt @@ -0,0 +1,44 @@ +package net.kernelpanicsoft.archie.gui.blockentity + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.compositionLocalOf +import kotlinx.serialization.serializer + +/** + * Provides the current block entity state to composables in the composition tree. + * + * Use with `LocalBlockEntityState.current` to access the state, or use the + * [observeProperty] helper for convenience. + */ +val LocalBlockEntityState = compositionLocalOf { null } + +/** + * Observes a property on the block entity in the current composition context. + * + * Returns a [MutableState] that automatically triggers recomposition when the property changes. + * Must be called where [LocalBlockEntityState] has been provided (e.g. inside a block entity's + * screen composition) — otherwise it throws. + * + * ### Example + * ```kotlin + * @Composable + * fun MyComponent() { + * val powerState = observeProperty("power") + * Text("Power: ${powerState.value}") + * } + * ``` + * + * @param propertyName The name of the property to observe. + * @param T The expected type of the property. + * @return A [MutableState] of type T reflecting the property's current value. + * @throws RuntimeException if no [ComposeBlockEntityState] is available in the current composition. + */ +@Composable +inline fun observeProperty( + propertyName: String, + initialValue: T? = null, +): MutableState { + val state = LocalBlockEntityState.current ?: throw RuntimeException("No block entity state available in composition") + return state.observeProperty(propertyName, serializer(), initialValue) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateContainer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateContainer.kt new file mode 100644 index 000000000..b51c0b60b --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateContainer.kt @@ -0,0 +1,171 @@ +package net.kernelpanicsoft.archie.gui.blockentity + +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.serializerOrNull +import net.kernelpanicsoft.archie.serialization.Sync +import net.minecraft.core.BlockPos +import net.minecraft.world.level.block.entity.BlockEntity +import kotlin.reflect.KClass +import kotlin.reflect.full.hasAnnotation +import kotlin.reflect.full.memberProperties +import kotlin.reflect.full.safeCast +import kotlin.reflect.jvm.isAccessible + +/** + * Wraps a block entity and tracks which properties have changed since the last sync. + * + * This class provides dirty tracking for efficient server-to-client synchronization. + * Only modified properties are included in generated state packets. + * + * @param blockEntity The block entity to monitor for changes. + */ +@OptIn(InternalSerializationApi::class) +class BlockEntityStateContainer( + val blockEntity: BlockEntity, +) { + /** The block position of the wrapped block entity. */ + val pos: BlockPos get() = blockEntity.blockPos + + /** Map of property names to their current values. */ + private val propertyValues = mutableMapOf() + + /** Serializers used to encode dirty properties into a [BlockEntityStatePacket], keyed by property name. */ + internal val propertySerializers = mutableMapOf>() + + @Suppress("UNCHECKED_CAST") + private fun anySerializer(serializer: KSerializer): KSerializer = serializer as KSerializer + + @Suppress("UNCHECKED_CAST") + private fun packetSerializer(propertyName: String): KSerializer = + propertySerializers[propertyName] as KSerializer + + init { + blockEntity::class.memberProperties.forEach { property -> + if (property.hasAnnotation()) + { + property.isAccessible = true + (property.returnType.classifier as KClass).serializerOrNull()?.let { serializer -> + propertySerializers[property.name] = serializer + } + } + } + } + + /** Set of property names that have changed since the last sync. */ + private val dirtyProperties = mutableSetOf() + + /** Server tick when this container was last synced. */ + var lastSyncTick: Long = 0 + + /** Whether any properties have changed. */ + val isDirty: Boolean get() = dirtyProperties.isNotEmpty() + + /** + * Records a property value and marks it dirty if it changed. + * + * @param propertyName The name of the property. + * @param value The new value. + * @return True if the value changed, false if it's the same as before. + */ + fun updateProperty(propertyName: String, value: T): Boolean { + + val oldValue = propertyValues[propertyName] + val changed = oldValue != value + if (changed) { + propertyValues[propertyName] = value + dirtyProperties.add(propertyName) + } + return changed + } + + /** + * Registers a serializer for [propertyName] if one isn't already known. + * + * Only needed for properties whose type can't be resolved automatically via + * [kotlinx.serialization.serializerOrNull] (see the `init` block). + * + * @param propertyName The name of the property. + * @param serializer The serializer to use when encoding this property. + */ + fun setPropertySerializer(propertyName: String, serializer: KSerializer) { + propertySerializers.putIfAbsent(propertyName, anySerializer(serializer)) + } + + /** + * Gets the current value of a property. + * + * @param propertyName The name of the property. + * @return The property value, or null if not tracked. + */ + fun getProperty(propertyName: String): Any? = propertyValues[propertyName] + + /** + * Gets the current value of a property with a type cast. + * + * @param propertyName The name of the property. + * @param T The expected type of the property. + * @return The property value cast to type T, or null if not found/wrong type. + */ + fun getProperty(propertyName: String, type: KClass): T? = + type.safeCast(propertyValues[propertyName]) + + /** + * Generates a state packet containing all dirty properties. + * + * @param serverTick The current server tick. + * @return A packet with all dirty property updates, or null if no changes. + */ + fun generatePacket(serverTick: Long): BlockEntityStatePacket? { + if (dirtyProperties.isEmpty()) return null + + val updates = dirtyProperties.associateWith { propertyName -> + propertyValues[propertyName].toSerializedValue(packetSerializer(propertyName)) + } + + return BlockEntityStatePacket( + pos = pos, + updates = updates, + timestamp = serverTick, + ) + } + + /** + * Clears the dirty flag, marking all properties as synced. + * + * Should be called after successfully sending a state packet to clients. + * + * @param serverTick The server tick when the sync completed. + */ + fun clearDirty(serverTick: Long) { + dirtyProperties.clear() + lastSyncTick = serverTick + } + + /** + * Gets all dirty property names. + * + * Useful for debugging or logging. + * + * @return An immutable set of property names that have changed. + */ + fun getDirtyProperties(): Set = dirtyProperties.toSet() + + /** + * Gets a snapshot of all tracked properties. + * + * @return An immutable map of all property names and values. + */ + fun getAllProperties(): Map = propertyValues.toMap() + + /** + * Resets all tracking, clearing dirty flags and property values. + * + * Useful when the block entity is unloaded or the container is no longer needed. + */ + fun reset() { + propertyValues.clear() + dirtyProperties.clear() + lastSyncTick = 0 + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateManager.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateManager.kt new file mode 100644 index 000000000..0edb46899 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateManager.kt @@ -0,0 +1,162 @@ +package net.kernelpanicsoft.archie.gui.blockentity + +import dev.architectury.event.events.common.TickEvent +import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel +import net.minecraft.core.BlockPos +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.level.block.entity.BlockEntity +import java.util.concurrent.ConcurrentHashMap + +/** + * Server-side manager for tracking and syncing block entity state to clients. + * + * This manager: + * - Maintains a registry of block entities by position + * - Tracks which properties have changed on each block entity + * - Sends state packets to players who are observing each block entity + * - Cleans up state when block entities are unloaded + */ +object BlockEntityStateManager { + /** Registry of tracked block entities keyed by (level, pos) */ + private val trackedEntities = ConcurrentHashMap() + + /** Registry of players tracking each block entity, keyed by (level, pos) */ + private val trackedPlayers = ConcurrentHashMap>() + + /** + * Registers the server tick listener that drives [syncDirtyEntities] every tick. + * + * Must be called once during mod init. + */ + fun init() { + TickEvent.SERVER_POST.register { + syncDirtyEntities(it.tickCount.toLong()) + } + + } + + /** + * Registers a block entity for state tracking. + * + * Should be called when a container menu is opened for a block entity. + * + * @param blockEntity The block entity to track. + * @return The state container for this block entity. + */ + fun registerBlockEntity(blockEntity: BlockEntity): BlockEntityStateContainer { + val key = getKey(blockEntity) + val container = trackedEntities.computeIfAbsent(key) { + BlockEntityStateContainer(blockEntity) + } + return container + } + + /** + * Unregisters a block entity from state tracking. + * + * Should be called when a container menu is closed. + * + * @param blockEntity The block entity to untrack. + */ + fun unregisterBlockEntity(blockEntity: BlockEntity) { + val key = getKey(blockEntity) + trackedEntities.remove(key)?.reset() + trackedPlayers.remove(key) + } + + /** + * Adds a player to the tracking list for a block entity. + * + * The player will receive state packets when the block entity changes. + * + * @param blockEntity The block entity. + * @param player The player to add. + */ + fun addTrackedPlayer(blockEntity: BlockEntity, player: ServerPlayer) { + val key = getKey(blockEntity) + trackedPlayers.computeIfAbsent(key) { mutableSetOf() }.add(player) + } + + /** + * Removes a player from the tracking list for a block entity. + * + * @param blockEntity The block entity. + * @param player The player to remove. + */ + fun removeTrackedPlayer(blockEntity: BlockEntity, player: ServerPlayer) { + val key = getKey(blockEntity) + trackedPlayers[key]?.remove(player) + if (trackedPlayers[key]?.isEmpty() == true) { + trackedPlayers.remove(key) + } + } + + /** + * Gets the state container for a block entity, if it exists. + * + * @param blockEntity The block entity. + * @return The state container, or null if not registered. + */ + fun getContainer(blockEntity: BlockEntity): BlockEntityStateContainer? { + return trackedEntities[getKey(blockEntity)] + } + + /** + * Syncs all dirty block entities to their tracked players. + * + * Should be called once per server tick via a tick event. + * + * @param currentTick The current server tick. + * @param networkChannel The network channel to send packets through. + */ + fun syncDirtyEntities( + currentTick: Long, + networkChannel: (BlockEntityStatePacket, List) -> Unit = DEFAULT_NETWORK_SENDER, + ) { + trackedEntities.forEach { (key, container) -> + val packet = container.generatePacket(currentTick) ?: return@forEach + val players = trackedPlayers[key] ?: emptySet() + if (players.isNotEmpty()) { + networkChannel(packet, players.toList()) + container.clearDirty(currentTick) + } + } + } + + /** + * Clears all tracking data. + * + * Useful for cleanup on server shutdown. + */ + fun clear() { + trackedEntities.values.forEach { it.reset() } + trackedEntities.clear() + trackedPlayers.clear() + } + + /** + * Gets a unique key for a block entity based on level and position. + * + * @param blockEntity The block entity. + * @return A unique key string. + */ + private fun getKey(blockEntity: BlockEntity): String { + val levelName = blockEntity.level?.hashCode() ?: 0 + return "${levelName}_${blockEntity.blockPos.x}_${blockEntity.blockPos.y}_${blockEntity.blockPos.z}" + } + + /** + * Default network sender used by [syncDirtyEntities]; sends [BlockEntityStatePacket]s to the + * given players via [ArchieNetworkChannel]. + */ + private val DEFAULT_NETWORK_SENDER: (BlockEntityStatePacket, List) -> Unit = { packet, players -> + ArchieNetworkChannel.toPlayers(players, packet) + } +} + +/** + * Convenience extension to get or create a state container for a block entity. + */ +fun BlockEntity.getStateContainer(): BlockEntityStateContainer = + BlockEntityStateManager.getContainer(this) ?: BlockEntityStateManager.registerBlockEntity(this) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacket.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacket.kt new file mode 100644 index 000000000..62cf55ea8 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacket.kt @@ -0,0 +1,102 @@ +package net.kernelpanicsoft.archie.gui.blockentity + +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import net.kernelpanicsoft.archie.serialization.SerializationManager +import net.kernelpanicsoft.archie.serialization.serializers.SBlockPos +import net.minecraft.core.BlockPos + +/** + * A network packet that carries block entity state changes from server to client. + * + * This packet is used to synchronize block entity property changes with connected clients, + * enabling reactive UI updates in Compose-based screens. + * + * @property pos The block position of the block entity being updated. + * @property updates A map of property names to their serialized values. + * @property timestamp Server tick when this packet was created (for ordering/deduplication). + */ +@Serializable +data class BlockEntityStatePacket( + val pos: SBlockPos, + val updates: Map = emptyMap(), + val timestamp: Long = 0, +) { + /** + * A serialized property value that can be transmitted over the network. + * + * Supports common types (Int, String, Boolean, Float, Double, etc.) as well as + * complex types that need NBT serialization. + */ + @Serializable + sealed class SerializedValue { + @Serializable + data class IntValue(val value: Int) : SerializedValue() + + @Serializable + data class StringValue(val value: String) : SerializedValue() + + @Serializable + data class BooleanValue(val value: Boolean) : SerializedValue() + + @Serializable + data class FloatValue(val value: Float) : SerializedValue() + + @Serializable + data class DoubleValue(val value: Double) : SerializedValue() + + @Serializable + data class LongValue(val value: Long) : SerializedValue() + + @Serializable + data class ByteValue(val value: Byte) : SerializedValue() + + + + + + @Serializable + data class CBORValue(val value: ByteArray) : SerializedValue() + { + override fun equals(other: Any?): Boolean + { + if (this === other) return true + if (javaClass != other?.javaClass) return false + + other as CBORValue + + return value.contentEquals(other.value) + } + + override fun hashCode(): Int + { + return value.contentHashCode() + } + } + + @Serializable + object NullValue : SerializedValue() + } + + companion object { + /** + * Creates a new packet with a single property update. + * + * @param pos The block position. + * @param propertyName The name of the property being updated. + * @param value The new value. + * @param timestamp The server tick. + */ + fun singleUpdate( + pos: BlockPos, + propertyName: String, + value: SerializedValue, + timestamp: Long = 0, + ): BlockEntityStatePacket = BlockEntityStatePacket( + pos = pos, + updates = mapOf(propertyName to value), + timestamp = timestamp, + ) + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt new file mode 100644 index 000000000..ceb795ab5 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt @@ -0,0 +1,118 @@ +package net.kernelpanicsoft.archie.gui.blockentity + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.ExperimentalSerializationApi +import net.kernelpanicsoft.archie.block.entity.NBTBlockEntity +import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel +import net.kernelpanicsoft.archie.serialization.SerializationManager +import net.minecraft.core.BlockPos +import net.minecraft.server.level.ServerLevel +import java.util.concurrent.ConcurrentHashMap + +/** + * Client-side registry of block entity states. + * + * Stores Compose state objects for active block entities so they can be updated + * when network packets arrive. + */ +private val clientBlockEntityStates = ConcurrentHashMap() + +/** + * Gets or creates a Compose state for a block entity by position. + * + * @param pos The block position. + * @return The [ComposeBlockEntityState] for that position. + */ +fun getOrCreateBlockEntityState(pos: BlockPos): ComposeBlockEntityState { + val key = "${pos.x}_${pos.y}_${pos.z}" + return clientBlockEntityStates.computeIfAbsent(key) { + ComposeBlockEntityState(pos) + } +} + +/** + * Removes a block entity state from the client registry. + * + * @param pos The block position. + */ +fun removeBlockEntityState(pos: BlockPos) { + val key = "${pos.x}_${pos.y}_${pos.z}" + clientBlockEntityStates.remove(key) +} + +/** + * Registers block entity state packets with [ArchieNetworkChannel]. + * + * Handles both directions: applying incoming [BlockEntityStatePacket]s to the client-side + * [ComposeBlockEntityState] registry, and applying incoming [BlockEntityUpdatePacket]s (client + * edits) to the server-side [BlockEntityStateManager]-tracked [NBTBlockEntity]. + */ +object BlockEntityStatePacketRegistry { + /** Registers the clientbound and serverbound packet handlers described above. */ + fun register() { + ArchieNetworkChannel.clientbound { packet, context -> + // Update the client-side state with new values from the packet + val state = getOrCreateBlockEntityState(packet.pos) + packet.updates.forEach { (propertyName, value) -> + state.updateProperty(propertyName, value) + } + } + + ArchieNetworkChannel.serverbound { packet, context -> + val player = context.player + val level = player.level() as? ServerLevel ?: return@serverbound + val blockEntity = level.getBlockEntity(packet.pos) as? NBTBlockEntity ?: return@serverbound + val container = BlockEntityStateManager.getContainer(blockEntity) ?: return@serverbound + + packet.updates.forEach { (propertyName, serializedValue) -> + val serializer = container.propertySerializers[propertyName] + if (serializer != null) { + val deserializedValue = serializedValue.deserialize(serializer) + container.updateProperty(propertyName, deserializedValue) + blockEntity.updateProperty(propertyName, serializer as KSerializer, deserializedValue as Any) + } + } + } + } +} + +/** + * Extension function to convert SerializedValue back to its original Kotlin type. + */ +@OptIn(ExperimentalSerializationApi::class) +internal fun BlockEntityStatePacket.SerializedValue.deserialize(serializer: KSerializer? = null): Any? = when (this) { + is BlockEntityStatePacket.SerializedValue.IntValue -> this.value + is BlockEntityStatePacket.SerializedValue.StringValue -> this.value + is BlockEntityStatePacket.SerializedValue.BooleanValue -> this.value + is BlockEntityStatePacket.SerializedValue.FloatValue -> this.value + is BlockEntityStatePacket.SerializedValue.DoubleValue -> this.value + is BlockEntityStatePacket.SerializedValue.LongValue -> this.value + is BlockEntityStatePacket.SerializedValue.ByteValue -> this.value + is BlockEntityStatePacket.SerializedValue.NullValue -> null + is BlockEntityStatePacket.SerializedValue.CBORValue -> { + if (serializer != null) { + SerializationManager.cbor.decodeFromByteArray(serializer, this.value) + } else { + // If no serializer is provided, we can't deserialize CBOR, so return the raw bytes or null + this.value + } + } +} + +/** + * Extension function to convert common Kotlin types to [BlockEntityStatePacket.SerializedValue]. + */ +@Suppress("UNCHECKED_CAST") +@OptIn(ExperimentalSerializationApi::class) +fun T?.toSerializedValue(serializer: KSerializer): BlockEntityStatePacket.SerializedValue = when (this) { + null -> BlockEntityStatePacket.SerializedValue.NullValue + is Int -> BlockEntityStatePacket.SerializedValue.IntValue(this) + is String -> BlockEntityStatePacket.SerializedValue.StringValue(this) + is Boolean -> BlockEntityStatePacket.SerializedValue.BooleanValue(this) + is Float -> BlockEntityStatePacket.SerializedValue.FloatValue(this) + is Double -> BlockEntityStatePacket.SerializedValue.DoubleValue(this) + is Long -> BlockEntityStatePacket.SerializedValue.LongValue(this) + is Byte -> BlockEntityStatePacket.SerializedValue.ByteValue(this) + is ByteArray -> BlockEntityStatePacket.SerializedValue.CBORValue(this) + else -> BlockEntityStatePacket.SerializedValue.CBORValue(SerializationManager.cbor.encodeToByteArray(serializer, this)) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityUpdatePacket.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityUpdatePacket.kt new file mode 100644 index 000000000..1a7836c07 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityUpdatePacket.kt @@ -0,0 +1,37 @@ +package net.kernelpanicsoft.archie.gui.blockentity + +import kotlinx.serialization.Serializable +import net.kernelpanicsoft.archie.serialization.serializers.SBlockPos +import net.minecraft.core.BlockPos + +/** + * A network packet that carries block entity state updates from client to server. + * + * This packet is used to send client-side modifications of block entity properties back to the server. + * + * @property pos The block position of the block entity being updated. + * @property updates A map of property names to their serialized values. + */ +@Serializable +data class BlockEntityUpdatePacket( + val pos: SBlockPos, + val updates: Map, +) { + companion object { + /** + * Creates a new packet with a single property update. + * + * @param pos The block position. + * @param propertyName The name of the property being updated. + * @param value The new value. + */ + fun singleUpdate( + pos: BlockPos, + propertyName: String, + value: BlockEntityStatePacket.SerializedValue, + ): BlockEntityUpdatePacket = BlockEntityUpdatePacket( + pos = pos, + updates = mapOf(propertyName to value), + ) + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/ComposeBlockEntityState.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/ComposeBlockEntityState.kt new file mode 100644 index 000000000..f35fa9681 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/ComposeBlockEntityState.kt @@ -0,0 +1,148 @@ +package net.kernelpanicsoft.archie.gui.blockentity + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel +import net.minecraft.core.BlockPos + +/** + * Client-side state holder for a block entity's synchronized properties. + * + * Each property is wrapped in a Compose [MutableState], allowing composables to react + * to changes automatically through recomposition. + * + * @param pos The block position of the block entity. + */ +class ComposeBlockEntityState( + val pos: BlockPos, +) { + /** Map of property names to their Compose state values */ + val propertyStates = mutableMapOf>() + + /** Serializers used to encode/decode each observed property, keyed by property name. */ + val propertySerializers = mutableMapOf>() + + @Suppress("UNCHECKED_CAST") + private fun anySerializer(serializer: KSerializer): KSerializer = serializer as KSerializer + + @Suppress("UNCHECKED_CAST") + private fun typedSerializer(propertyName: String): KSerializer? = propertySerializers[propertyName] as? KSerializer + + @Suppress("UNCHECKED_CAST") + private fun getOrCreateState(propertyName: String, initialValue: T?): MutableState { + return propertyStates.computeIfAbsent(propertyName) { + PropertyState(this, propertyName, mutableStateOf(initialValue)) as MutableState + } as MutableState + } + + /** + * Gets or creates a Compose state for a property with a specific type. + * + * @param propertyName The name of the property. + * @param initialValue The initial value (optional, defaults to null). + * @param T The expected type of the property. + * @return A [MutableState] of type T that can be observed in composables. + */ + fun observeProperty( + propertyName: String, + serializer: KSerializer, + initialValue: T? = null, + ): MutableState { + propertySerializers[propertyName] = anySerializer(serializer) + return getOrCreateState(propertyName, initialValue) + } + + /** + * A [MutableState] delegate that forwards writes to [ComposeBlockEntityState.sendUpdatedProperty], + * so setting [value] from a composable both updates local state and pushes the change to the server. + */ + class PropertyState(private val state: ComposeBlockEntityState, private val propertyName: String, internal val mutableState: MutableState) : MutableState by mutableState + { + override var value: T + get() = mutableState.value + set(value) + { + mutableState.value = value + state.sendUpdatedProperty(propertyName, value) + } + } + + /** + * Updates a property value from a network packet. + * + * If the property doesn't exist yet, it will be created. + * + * @param propertyName The name of the property. + * @param value The new serialized value from the network packet. + */ + fun updateProperty(propertyName: String, value: BlockEntityStatePacket.SerializedValue) { + val deserializedValue = value.deserialize(propertySerializers[propertyName]) + val state = propertyStates.computeIfAbsent(propertyName) { + mutableStateOf(deserializedValue) + } + state.value = deserializedValue + } + + /** + * Updates a property value and sends the change to the server. + * + * This method should be called when a client-side interaction changes a property. + * + * @param propertyName The name of the property. + * @param value The new value. + */ + fun sendUpdatedProperty(propertyName: String, value: T) { + val serializer = typedSerializer(propertyName) ?: run { + println("No serializer found for property $propertyName. Cannot send update to server.") + return + } + + val serializedValue = value.toSerializedValue(serializer) + val packet = BlockEntityUpdatePacket.singleUpdate(pos, propertyName, serializedValue) + ArchieNetworkChannel.toServer(packet) + } + + + + /** + * Gets the current value of a property. + * + * @param propertyName The name of the property. + * @return The property value, or null if not tracked. + */ + fun getProperty(propertyName: String): Any? { + return propertyStates[propertyName]?.value + } + + /** + * Gets the current value of a property with type casting. + * + * @param propertyName The name of the property. + * @param T The expected type. + * @return The property value cast to T, or null if not found/wrong type. + */ + @Suppress("UNCHECKED_CAST") + fun getPropertyTyped(propertyName: String): T? { + return propertyStates[propertyName]?.value as? T + } + + /** + * Clears all tracked properties. + * + * Useful when the block entity is unloaded or the state is no longer needed. + */ + fun clear() { + propertyStates.clear() + } + + /** + * Gets all currently tracked properties. + * + * @return A map of property names to their current values. + */ + fun getAllProperties(): Map { + return propertyStates.mapValues { (_, state) -> state.value } + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Divider.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Divider.kt new file mode 100644 index 000000000..ba6ca4344 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Divider.kt @@ -0,0 +1,54 @@ +package net.kernelpanicsoft.archie.gui.composables.basic + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.appearance.BackgroundModifier +import net.kernelpanicsoft.archie.gui.modifiers.fillMaxHeight +import net.kernelpanicsoft.archie.gui.modifiers.fillMaxWidth +import net.kernelpanicsoft.archie.gui.modifiers.height +import net.kernelpanicsoft.archie.gui.modifiers.width +import net.kernelpanicsoft.archie.gui.util.KColor + +/** + * Draws a thin horizontal or vertical separator line. + * + * @param color The line's fill color, as an ARGB int. + * @param thickness The line's thickness in pixels along its short axis. + * @param vertical When `true`, the line fills its height and is [thickness] pixels wide; + * when `false` (default), it fills its width and is [thickness] pixels tall. + */ +@Composable +fun Divider( + modifier: Modifier = Modifier, + color: Int = KColor.GRAY.argb, + thickness: Int = 1, + vertical: Boolean = false, +) { + val axisModifier = if (vertical) { + Modifier.width(thickness).fillMaxHeight() + } else { + Modifier.height(thickness).fillMaxWidth() + } + + Spacer(modifier = axisModifier.then(BackgroundModifier(color, color)).then(modifier)) +} + +/** Convenience horizontal divider. */ +@Stable +@Composable +fun HorizontalDivider( + modifier: Modifier = Modifier, + color: Int = KColor.GRAY.argb, + thickness: Int = 1, +) = Divider(modifier = modifier, color = color, thickness = thickness) + +/** Convenience vertical divider. */ +@Stable +@Composable +fun VerticalDivider( + modifier: Modifier = Modifier, + color: Int = KColor.GRAY.argb, + thickness: Int = 1, +) = Divider(modifier = modifier, color = color, thickness = thickness, vertical = true) + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt new file mode 100644 index 000000000..9259def4d --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt @@ -0,0 +1,45 @@ +package net.kernelpanicsoft.archie.gui.composables.basic + +import androidx.compose.runtime.Composable +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.theme.ThemeVariants +import net.kernelpanicsoft.archie.transfer.ArchieEnergyStorage + +/** + * A themed energy-level indicator (looked up in the current theme as `"energy_bar"`), filled + * with a solid color up to `energy / capacity`. + * + * Shares [ProgressBar]'s rendering core but defaults to a bottom-up fill and an energy-flavored + * color, matching how most tech mods orient a power gauge. + * + * @param energy The current stored amount (see [ArchieEnergyStorage.getStoredAmount]). + * @param capacity The maximum capacity (see [ArchieEnergyStorage.getCapacity]); a non-positive + * value renders as empty rather than dividing by zero. + * @param modifier Additional modifiers applied to the outer container. + * @param direction Which edge the fill grows from. + * @param fillColor ARGB color of the filled portion. + * @param variant The theme variant used for the track texture. + */ +@Composable +fun EnergyBar( + energy: Long, + capacity: Long, + modifier: Modifier = Modifier, + direction: ProgressDirection = ProgressDirection.BOTTOM_TO_TOP, + fillColor: Int = 0xFFFF5C33.toInt(), + variant: String = ThemeVariants.DEFAULT, +) +{ + val fraction = if (capacity <= 0L) 0f else (energy.toDouble() / capacity.toDouble()).toFloat().coerceIn(0f, 1f) + ThemedFillBar("energy_bar", fraction, modifier, direction, fillColor, variant) +} + +/** Convenience overload reading directly from an [ArchieEnergyStorage]. */ +@Composable +fun EnergyBar( + storage: ArchieEnergyStorage, + modifier: Modifier = Modifier, + direction: ProgressDirection = ProgressDirection.BOTTOM_TO_TOP, + fillColor: Int = 0xFFFF5C33.toInt(), + variant: String = ThemeVariants.DEFAULT, +) = EnergyBar(storage.getStoredAmount(), storage.getCapacity(), modifier, direction, fillColor, variant) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt new file mode 100644 index 000000000..da29985a0 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt @@ -0,0 +1,91 @@ +package net.kernelpanicsoft.archie.gui.composables.basic + +import androidx.compose.runtime.Composable +import dev.architectury.fluid.FluidStack +import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.MeasureResult +import net.kernelpanicsoft.archie.gui.layout.Renderer +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.sizeIn +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.render.AFluidRenderPlatform +import net.kernelpanicsoft.archie.gui.theme.LocalTheme +import net.kernelpanicsoft.archie.gui.theme.ThemeVariants +import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.kernelpanicsoft.archie.gui.util.extension.scissor +import net.minecraft.client.gui.GuiGraphics + +private const val FLUID_TANK_MIN_WIDTH = 18 +private const val FLUID_TANK_MIN_HEIGHT = 54 +private const val FLUID_TANK_INSET = 1 + +/** + * A themed fluid-level indicator (looked up in the current theme as `"fluid_tank"`): a tank + * frame sprite with the real fluid texture and tint (via [AFluidRenderPlatform]) filling it + * bottom-up to `fluid.amount / capacity`. + * + * The fluid sprite is stretched to the tank's interior and clipped with a scissor rather than + * tiled per-block, so it won't repeat at a pixel-perfect 16px grid - a reasonable tradeoff for a + * UI meter over the complexity of manual tiled-quad rendering. See [AFluidRenderPlatform] for + * why this needs a platform bridge at all: Fabric and NeoForge expose a fluid's client + * appearance through unrelated APIs. + * + * @param fluid The fluid and amount to display; an empty stack renders just the tank frame. + * @param capacity The tank's total capacity; a non-positive value renders as empty rather than + * dividing by zero. + * @param modifier Additional modifiers applied to the outer container. + * @param variant The theme variant used for the tank frame texture. + */ +@Composable +fun FluidTank( + fluid: FluidStack, + capacity: Long, + modifier: Modifier = Modifier, + variant: String = ThemeVariants.DEFAULT, +) +{ + val theme = LocalTheme.current.getComposableTheme("fluid_tank") + val sizeModifier = Modifier.sizeIn(minWidth = FLUID_TANK_MIN_WIDTH, minHeight = FLUID_TANK_MIN_HEIGHT) + + Layout( + name = "FluidTank", + measurePolicy = { _, _, constraints -> MeasureResult(constraints.minWidth, constraints.minHeight) {} }, + modifier = sizeModifier.then(modifier), + renderer = object : Renderer + { + override fun render( + node: UINode, x: Int, y: Int, + guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, + ) = guiGraphics { + val state = theme.getState(TextureStates.DEFAULT, variant) + drawThemeState(state, x, y, node.width, node.height) + + if (fluid.isEmpty || capacity <= 0L) return@guiGraphics + + val fraction = (fluid.amount.toDouble() / capacity.toDouble()).coerceIn(0.0, 1.0).toFloat() + val sprite = AFluidRenderPlatform.getStillSprite(fluid.fluid) ?: return@guiGraphics + + val innerX = x + FLUID_TANK_INSET + val innerY = y + FLUID_TANK_INSET + val innerW = (node.width - FLUID_TANK_INSET * 2).coerceAtLeast(0) + val innerH = (node.height - FLUID_TANK_INSET * 2).coerceAtLeast(0) + val fillH = (innerH * fraction).toInt() + val fillY = innerY + innerH - fillH + + if (innerW <= 0 || fillH <= 0) return@guiGraphics + + val tint = AFluidRenderPlatform.getTintColor(fluid.fluid) + val a = ((tint ushr 24) and 0xFF) / 255f + val r = ((tint ushr 16) and 0xFF) / 255f + val g = ((tint ushr 8) and 0xFF) / 255f + val b = (tint and 0xFF) / 255f + + scissor(innerX, fillY, innerX + innerW, fillY + fillH) { + blit(innerX, innerY, innerW, innerH, 0, sprite, r, g, b, a) + } + } + }, + ) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Icon.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Icon.kt new file mode 100644 index 000000000..0a1d67533 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Icon.kt @@ -0,0 +1,43 @@ +package net.kernelpanicsoft.archie.gui.composables.basic + +import androidx.compose.runtime.Composable +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.size +import net.minecraft.resources.ResourceLocation + +/** + * Convenience wrapper around [Texture] for fixed-size icon sprites. + * + * @param texture The sprite sheet/texture location. + * @param size The icon's rendered width and height, in pixels. + * @param uOffset The sprite's left edge within [texture], in texture pixels. + * @param vOffset The sprite's top edge within [texture], in texture pixels. + * @param u The sprite's source width within [texture]. Defaults to [size]. + * @param v The sprite's source height within [texture]. Defaults to [size]. + * @param textureWidth The full width of [texture], in pixels. + * @param textureHeight The full height of [texture], in pixels. + */ +@Composable +fun Icon( + texture: ResourceLocation, + size: Int = 16, + uOffset: Float = 0f, + vOffset: Float = 0f, + u: Int = size, + v: Int = size, + textureWidth: Int = 256, + textureHeight: Int = 256, + modifier: Modifier = Modifier, +) { + Texture( + loc = texture, + uOffset = uOffset, + vOffset = vOffset, + u = u, + v = v, + textureWidth = textureWidth, + textureHeight = textureHeight, + modifier = Modifier.size(size, size).then(modifier), + ) +} + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt new file mode 100644 index 000000000..ba7aa018d --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt @@ -0,0 +1,108 @@ +package net.kernelpanicsoft.archie.gui.composables.basic + +import androidx.compose.runtime.Composable +import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.MeasureResult +import net.kernelpanicsoft.archie.gui.layout.Renderer +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.sizeIn +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.theme.LocalTheme +import net.kernelpanicsoft.archie.gui.theme.ThemeVariants +import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.minecraft.client.gui.GuiGraphics + +internal const val FILL_BAR_MIN_WIDTH = 90 +internal const val FILL_BAR_MIN_HEIGHT = 16 + +/** Which edge of a [ProgressBar]/[net.kernelpanicsoft.archie.gui.composables.basic.EnergyBar] the fill grows from. */ +enum class ProgressDirection +{ + LEFT_TO_RIGHT, + RIGHT_TO_LEFT, + TOP_TO_BOTTOM, + BOTTOM_TO_TOP, +} + +/** + * Shared rendering core for [ProgressBar] and [net.kernelpanicsoft.archie.gui.composables.basic.EnergyBar]: + * a themed track sprite (looked up as [themeName] in the current theme) filled with a solid + * color up to [progress]. + */ +@Composable +internal fun ThemedFillBar( + themeName: String, + progress: Float, + modifier: Modifier, + direction: ProgressDirection, + fillColor: Int, + variant: String, +) +{ + val clamped = progress.coerceIn(0f, 1f) + val theme = LocalTheme.current.getComposableTheme(themeName) + val sizeModifier = Modifier.sizeIn(minWidth = FILL_BAR_MIN_WIDTH, minHeight = FILL_BAR_MIN_HEIGHT) + + Layout( + name = themeName, + measurePolicy = { _, _, constraints -> MeasureResult(constraints.minWidth, constraints.minHeight) {} }, + modifier = sizeModifier.then(modifier), + renderer = object : Renderer + { + override fun render( + node: UINode, x: Int, y: Int, + guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, + ) = guiGraphics { + val state = theme.getState(TextureStates.DEFAULT, variant) + drawThemeState(state, x, y, node.width, node.height) + + var fx = x + var fy = y + var fw = node.width + var fh = node.height + when (direction) + { + ProgressDirection.LEFT_TO_RIGHT -> fw = (node.width * clamped).toInt() + ProgressDirection.RIGHT_TO_LEFT -> + { + fw = (node.width * clamped).toInt() + fx = x + node.width - fw + } + + ProgressDirection.TOP_TO_BOTTOM -> fh = (node.height * clamped).toInt() + ProgressDirection.BOTTOM_TO_TOP -> + { + fh = (node.height * clamped).toInt() + fy = y + node.height - fh + } + } + if (fw > 0 && fh > 0) fill(fx, fy, fx + fw, fy + fh, fillColor) + } + }, + ) +} + +/** + * A themed linear progress indicator: an empty-track sprite from the current theme (looked up as + * `"progress_bar"`), filled with a solid color up to [progress]. + * + * There's no built-in animation or recomposition trigger here - drive [progress] from an + * observed block entity field (see [net.kernelpanicsoft.archie.gui.blockentity.observeProperty]) + * for a live machine-processing indicator. + * + * @param progress Fraction complete, clamped to `0f..1f`. + * @param modifier Additional modifiers applied to the outer container. + * @param direction Which edge the fill grows from. + * @param fillColor ARGB color of the filled portion. + * @param variant The theme variant used for the track texture. + */ +@Composable +fun ProgressBar( + progress: Float, + modifier: Modifier = Modifier, + direction: ProgressDirection = ProgressDirection.LEFT_TO_RIGHT, + fillColor: Int = 0xFF6BA8FF.toInt(), + variant: String = ThemeVariants.DEFAULT, +) = ThemedFillBar("progress_bar", progress, modifier, direction, fillColor, variant) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Spacer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Spacer.kt new file mode 100644 index 000000000..e70879874 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Spacer.kt @@ -0,0 +1,37 @@ +package net.kernelpanicsoft.archie.gui.composables.basic + +import androidx.compose.runtime.Composable +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.MeasureResult +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.fillMaxSize + +/** + * An invisible layout composable that expands to fill available space. + * + * `Spacer` is the idiomatic way to push siblings apart inside [net.kernelpanicsoft.archie.gui.layout.Row] + * or [net.kernelpanicsoft.archie.gui.layout.Column] arrangements. By default it stretches + * to consume all remaining space in its parent. + * + * ### Example + * ```kotlin + * Row { + * Text(Component.literal("Left")) + * Spacer() // pushes "Right" to the far end + * Text(Component.literal("Right")) + * } + * ``` + * + * @param modifier Additional modifiers; most commonly used to constrain the spacer to a + * fixed size with `Modifier.size(width, height)`. + */ +@Composable +fun Spacer(modifier: Modifier = Modifier) { + Layout( + name = "Spacer", + measurePolicy = { _, _, constraints -> + MeasureResult(constraints.minWidth, constraints.minHeight) {} + }, + modifier = modifier.fillMaxSize(), + ) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Text.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Text.kt new file mode 100644 index 000000000..ab8cae180 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Text.kt @@ -0,0 +1,99 @@ +package net.kernelpanicsoft.archie.gui.composables.basic + +import androidx.compose.runtime.Composable +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.MeasureResult +import net.kernelpanicsoft.archie.gui.layout.Renderer +import net.kernelpanicsoft.archie.gui.layout.Size +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.theme.LocalTheme +import net.kernelpanicsoft.archie.gui.util.KColor +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.kernelpanicsoft.archie.gui.util.extension.pose +import net.kernelpanicsoft.archie.util.minecraftClient +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.Font +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component + +/** + * Returns the rendered pixel size (width × height) of [text] at the given [scale]. + * + * Useful for sizing containers to exactly fit their text content before composition. + * + * @param text The [Component] whose rendered dimensions are measured. + * @param scale Font scale factor (1.0 = native size). + * @param font The [Font] to measure with; defaults to Minecraft's standard font. + */ +fun getTextSize( + text: Component, + scale: Float = 1f, + font: Font = minecraftClient.font, +): Size = Size((font.width(text) * scale).toInt(), (font.lineHeight * scale).toInt()) + +/** + * Renders a [Component] using Minecraft's font renderer. + * + * Supports optional uniform scaling, a custom font face, and an ARGB text color. + * The node automatically sizes itself to the minimum dimensions needed to display the + * text at the requested scale. + * + * ### Example + * ```kotlin + * Text( + * text = Component.literal("Hello, Archie!"), + * fontScale = 1.5f, + * color = KColor.YELLOW, + * ) + * ``` + * + * @param text The text component to render. + * @param fontScale Uniform scale applied to the font. Default `1f` (native size). + * @param font The [Font] used for rendering and size measurement. + * @param color Text color. Defaults to the current [LocalTheme]'s light text color. + * @param dropShadow Whether to render the vanilla text drop shadow. Default `true`. + * @param modifier Additional modifiers applied to the layout node. + */ +@Composable +fun Text( + text: Component, + fontScale: Float = 1f, + font: Font = minecraftClient.font, + color: KColor = LocalTheme.current.lightTextColor, + dropShadow: Boolean = true, + modifier: Modifier = Modifier, +) { + Layout( + name = "Text", + measurePolicy = { _, _, constraints -> + val textSize = getTextSize(text, fontScale, font) + MeasureResult( + textSize.width.coerceIn(constraints.minWidth, constraints.maxWidth), + textSize.height.coerceIn(constraints.minHeight, constraints.maxHeight), + ) {} + }, + renderer = object : Renderer { + override fun render( + node: UINode, + x: Int, y: Int, + guiGraphics: GuiGraphics, + mouseX: Int, mouseY: Int, + partialTick: Float, + ) = guiGraphics { + if (fontScale != 1f) + { + pose { + scale(fontScale, fontScale, fontScale) + translate(x / fontScale, y / fontScale, 0f) + drawString(font, text, 0, 0, color.argb, dropShadow) + } + } else + { + drawString(font, text, x, y, color.argb, dropShadow) + } + } + }, + modifier = modifier, + ) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Texture.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Texture.kt new file mode 100644 index 000000000..5b473356b --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Texture.kt @@ -0,0 +1,55 @@ +package net.kernelpanicsoft.archie.gui.composables.basic + +import androidx.compose.runtime.Composable +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.MeasureResult +import net.kernelpanicsoft.archie.gui.layout.Renderer +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.DebugModifier +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.resources.ResourceLocation + +/** + * Renders a sprite or texture region using UV coordinates. + * + * The composable sizes itself to the minimum constraints provided by its parent and blits + * the specified source region from [loc] into the node's bounds. + * + * @param loc Resource location of the texture or atlas sprite. + * @param uOffset Horizontal UV start offset within the source image (in texture pixels). + * @param vOffset Vertical UV start offset within the source image (in texture pixels). + * @param u Width of the source region in texture pixels. + * @param v Height of the source region in texture pixels. + * @param textureWidth Total width of the source image in pixels. + * @param textureHeight Total height of the source image in pixels. + * @param modifier Additional modifiers applied to the layout node. + */ +@Composable +fun Texture( + loc: ResourceLocation, + uOffset: Float, + vOffset: Float, + u: Int, + v: Int, + textureWidth: Int, + textureHeight: Int, + modifier: Modifier = Modifier, +) { + Layout( + name = "Texture", + measurePolicy = { _, _, constraints -> + MeasureResult(constraints.minWidth, constraints.minHeight) {} + }, + renderer = object : Renderer { + override fun render( + node: UINode, x: Int, y: Int, + guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, + ) = guiGraphics { + blit(loc, x, y, node.width, node.height, uOffset, vOffset, u, v, textureWidth, textureHeight) + } + }, + modifier = Modifier.then(DebugModifier(strs = listOf(loc.toString()))).then(modifier), + ) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Collapsible.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Collapsible.kt new file mode 100644 index 000000000..721d8d5ff --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Collapsible.kt @@ -0,0 +1,166 @@ +package net.kernelpanicsoft.archie.gui.composables.containers + +import androidx.compose.runtime.* +import com.mojang.math.Axis +import net.kernelpanicsoft.archie.gui.animation.AnimationSpec +import net.kernelpanicsoft.archie.gui.animation.Easings +import net.kernelpanicsoft.archie.gui.animation.animateFloat +import net.kernelpanicsoft.archie.gui.composables.basic.Spacer +import net.kernelpanicsoft.archie.gui.composables.basic.Text +import net.kernelpanicsoft.archie.gui.layout.* +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.appearance.BackgroundModifier +import net.kernelpanicsoft.archie.gui.modifiers.fillMaxHeight +import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType +import net.kernelpanicsoft.archie.gui.modifiers.input.onPointerEvent +import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier +import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingValues +import net.kernelpanicsoft.archie.gui.modifiers.width +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.util.KColor +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.kernelpanicsoft.archie.gui.util.extension.pose +import net.kernelpanicsoft.archie.util.minecraftClient +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.Style +import kotlin.math.roundToInt +import kotlin.time.Duration.Companion.milliseconds + +private const val COLLAPSIBLE_VISIBILITY_EPSILON = 0.01f + +/** + * A container that can be expanded or collapsed by clicking its header. + * + * The header displays [title] with an animated arrow indicator that rotates 90° when the + * section is open. A vertical separator bar is shown to the left of the expanded content. + * + * ### Example + * ```kotlin + * Collapsible(title = Component.literal("Advanced Settings")) { + * // content shown when expanded + * Text(Component.literal("Option A")) + * } + * ``` + * + * @param title The text displayed in the collapsible header. + * @param modifier Modifiers applied to the outer [Column] container. + * @param initiallyExpanded Whether the section starts in the expanded state. + * @param onToggled Called when the expanded state changes; receives the new state. + * @param content The composable content shown when expanded. + */ +@Composable +fun Collapsible( + title: Component, + modifier: Modifier = Modifier, + initiallyExpanded: Boolean = false, + onToggled: (isExpanded: Boolean) -> Unit = {}, + content: @Composable () -> Unit, +) { + var expanded by remember { mutableStateOf(initiallyExpanded) } + val expandProgress = animateFloat( + targetValue = if (expanded) 1f else 0f, + spec = AnimationSpec(durationMillis = 220.milliseconds, easing = Easings.OutCubic), + ) + + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4)) { + Row( + modifier = Modifier.onPointerEvent(PointerEventType.PRESS) { _, e -> + expanded = !expanded + onToggled(expanded) + e.consume() + }, + verticalAlignment = Alignment.CenterVertically, + horizontalArrangement = Arrangement.spacedBy(5), + ) { + CollapsibleArrow(isExpanded = expanded) + Text(text = title.copy().withStyle(Style.EMPTY.withUnderlined(true))) + } + + if (expanded || expandProgress > COLLAPSIBLE_VISIBILITY_EPSILON) { + Layout( + name = "CollapsibleContent", + measurePolicy = { _, measurables, constraints -> + if (measurables.isEmpty()) return@Layout MeasureResult(0, 0) {} + + val placeable = measurables.first().measure( + constraints.copy(minHeight = 0, maxHeight = Int.MAX_VALUE), + ) + val visibleHeight = (placeable.height * expandProgress) + .roundToInt() + .coerceAtLeast(0) + + MeasureResult(placeable.width, visibleHeight) { + placeable.placeAt(0, 0) + } + }, + renderer = object : Renderer { + override fun render( + node: UINode, + x: Int, + y: Int, + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float, + ) = guiGraphics { + enableScissor(x, y, x + node.width, y + node.height) + } + + override fun renderAfterChildren( + node: UINode, + x: Int, + y: Int, + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float, + ) = guiGraphics { + disableScissor() + } + }, + ) { + Row(horizontalArrangement = Arrangement.spacedBy(5)) { + Spacer( + modifier = Modifier + .then(PaddingModifier(PaddingValues(left = 5))) + .width(1) + .fillMaxHeight() + .then(BackgroundModifier(KColor.GRAY.argb, KColor.GRAY.argb)) + ) + Box(modifier = Modifier.then(PaddingModifier(PaddingValues(left = 5)))) { + content() + } + } + } + } + } +} + +/** Animated arrow icon that rotates when the collapsible section opens or closes. */ +@Composable +private fun CollapsibleArrow(isExpanded: Boolean) { + val rotation = animateFloat( + targetValue = if (isExpanded) 90f else 0f, + spec = AnimationSpec(durationMillis = 260.milliseconds, easing = Easings.OutBack), + ) + + Layout( + name = "CollapsibleArrow", + measurePolicy = { _, _, _ -> MeasureResult(8, 8) {} }, + renderer = object : Renderer { + override fun render( + node: UINode, x: Int, y: Int, + guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, + ) = guiGraphics { + pose { + translate(x + node.width / 2f, y + node.height / 2f, 0f) + mulPose(Axis.ZP.rotationDegrees(rotation)) + translate(-(x + node.width / 2f), -(y + node.height / 2f), 0f) + drawString(minecraftClient.font, ">", x + 1, y, KColor.WHITE.argb) + } + } + }, + ) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/ContainerPanel.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/ContainerPanel.kt new file mode 100644 index 000000000..a720431f9 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/ContainerPanel.kt @@ -0,0 +1,102 @@ +package net.kernelpanicsoft.archie.gui.composables.containers + +import androidx.compose.runtime.Composable +import net.kernelpanicsoft.archie.gui.LocalContainerScreen +import net.kernelpanicsoft.archie.gui.PlayerSlots +import net.kernelpanicsoft.archie.gui.layout.Box +import net.kernelpanicsoft.archie.gui.layout.Column +import net.kernelpanicsoft.archie.gui.layout.offset +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.onGloballyPositioned +import net.kernelpanicsoft.archie.gui.modifiers.position.offset +import net.kernelpanicsoft.archie.gui.modifiers.position.padding +import net.kernelpanicsoft.archie.gui.modifiers.width +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen + +private const val DEFAULT_CONTENT_WIDTH = 9 * 18 + +/** + * A complete container screen layout following the vanilla chest-screen pattern. + * + * Combines the screen contents (top section) with the player inventory (bottom section), + * properly positioned and spaced. Automatically sets the game's label positions + * ([AbstractContainerScreen.titleLabelX]/[AbstractContainerScreen.titleLabelY] and [AbstractContainerScreen.inventoryLabelX]/[AbstractContainerScreen.inventoryLabelY]) based on the layout. + * + * The game then renders the labels using the title and inventory label components. + * + * ### Layout Structure + * ``` + * ┌─────────────────────────────────┐ + * │ [Screen Contents] │ <- titleLabelX/Y set here + * ├─────────────────────────────────┤ <- contentSpacing + * │ [Player Inventory 3×9] │ <- inventoryLabelX/Y set here + * │ [spacing] │ + * │ [Hotbar 1×9] │ + * └─────────────────────────────────┘ + * ``` + * + * @param contentWidth Width of the panel's content area, in pixels. Defaults to 9 slots wide. + * @param modifier Additional modifiers applied to the outer container. + * @param content The screen contents composable (container inventory, custom widgets, etc.). + */ +@Composable +fun ContainerPanel( + contentWidth: Int = DEFAULT_CONTENT_WIDTH, + modifier: Modifier = Modifier, + content: @Composable () -> Unit, +) { + val screen = LocalContainerScreen.current + + Panel(contentWidth = contentWidth, modifier = modifier) { + Column { + // Screen contents (container inventory) + Box( + modifier = Modifier + .padding(top = 10) + .onGloballyPositioned { coords -> + screen.titleLabelPos = coords + } + ) { + content() + } + // Player inventory section + Box( + modifier = Modifier + .padding(top = 14) + .onGloballyPositioned { coords -> + screen.inventoryLabelPos = coords + offset(x = 1, y = 3) + } + ) { + // Player inventory slots (3×9 main inventory + 1×9 hotbar) + PlayerSlots() + } + } + } +} + +/** + * A [ContainerPanel] whose contents are switched between tabs, following the same + * container/player-inventory layout as [ContainerPanel]. + * + * @param contentWidth Width of the panel's content area, in pixels. Defaults to 9 slots wide. + * @param modifier Additional modifiers applied to the outer [TabPanel]. + * @param builder Declares the tabs; see [TabContainerScope]. + */ +@Composable +fun TabContainerPanel( + contentWidth: Int = DEFAULT_CONTENT_WIDTH, + modifier: Modifier = Modifier, + builder: TabContainerScope.() -> Unit +) { + TabPanel( + modifier = modifier.width(contentWidth + 16), + contentWrapper = { content -> + ContainerPanel( + contentWidth = contentWidth, + modifier = Modifier.offset(y = -12), + content = content, + ) + }, + builder = builder + ) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Panel.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Panel.kt new file mode 100644 index 000000000..18e169d39 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Panel.kt @@ -0,0 +1,43 @@ +package net.kernelpanicsoft.archie.gui.composables.containers + +import androidx.compose.runtime.Composable +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.layout.Box +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.position.padding +import net.kernelpanicsoft.archie.gui.modifiers.width +import net.kernelpanicsoft.archie.gui.theme.ThemeVariants + +/** + * A padded themed [Surface] used as a general-purpose container for grouped UI content. + * + * @param contentAlignment Alignment of [content] within the panel. + * @param contentWidth When non-null, the panel's inner content area is fixed to this width + * (in pixels); the panel itself is sized to fit that plus [contentPadding] on both sides. + * @param texture The themed texture/style key drawn as the panel's background. See [Surface]. + * @param variant The theme variant of [texture] to use. See [ThemeVariants]. + * @param contentPadding Padding (in pixels) inserted between the panel edge and [content]. + */ +@Composable +fun Panel( + modifier: Modifier = Modifier, + contentAlignment: Alignment = Alignment.TopStart, + contentWidth: Int? = null, + texture: String = "surface", + variant: String = ThemeVariants.DEFAULT, + contentPadding: Int = 8, + content: @Composable () -> Unit, +) { + val resolvedModifier = if (contentWidth != null) modifier.width(contentWidth + contentPadding*2) else modifier + Surface( + modifier = resolvedModifier, + texture = texture, + variant = variant, + contentAlignment = contentAlignment, + ) { + Box(modifier = Modifier.padding(contentPadding), contentAlignment = contentAlignment) { + content() + } + } +} + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/RootContainer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/RootContainer.kt new file mode 100644 index 000000000..ec29b1db3 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/RootContainer.kt @@ -0,0 +1,36 @@ +package net.kernelpanicsoft.archie.gui.composables.containers + +import androidx.compose.runtime.Composable +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.layout.Box +import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy +import net.kernelpanicsoft.archie.gui.layout.EmptyRenderer +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.fillMaxSize + +/** + * The top-level layout node for a screen's content, centered within the full screen bounds. + * + * [ComposeScreen][net.kernelpanicsoft.archie.gui.ComposeScreen] and + * [ComposeContainerScreen][net.kernelpanicsoft.archie.gui.ComposeContainerScreen] wrap their + * `start` content in this composable so [content] is measured/placed like a [Box] (children + * stacked and top-start-aligned by default) while the whole subtree stays centered on screen. + * + * @param modifier Additional modifiers applied to the content layout node. + */ +@Composable +fun RootContainer( + modifier: Modifier = Modifier, + content: @Composable () -> Unit +) { + Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { + Layout( + name = "RootContainer", + measurePolicy = BoxMeasurePolicy(Alignment.TopStart), + renderer = EmptyRenderer, + modifier = modifier, + content = content + ) + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Scrollable.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Scrollable.kt new file mode 100644 index 000000000..a343d5367 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Scrollable.kt @@ -0,0 +1,267 @@ +package net.kernelpanicsoft.archie.gui.composables.containers + +import androidx.compose.runtime.* +import net.kernelpanicsoft.archie.gui.LocalSlotClipBounds +import net.kernelpanicsoft.archie.gui.SlotClipSource +import net.kernelpanicsoft.archie.gui.layout.* +import net.kernelpanicsoft.archie.gui.modifiers.Constraints +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.onGloballyPositioned +import net.kernelpanicsoft.archie.gui.modifiers.onSizeChanged +import net.kernelpanicsoft.archie.gui.modifiers.input.* +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.util.KColor +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.util.Mth +import org.lwjgl.glfw.GLFW +import kotlin.math.abs +import kotlin.math.max +import kotlin.math.roundToInt + +private const val SCROLLBAR_THICKNESS = 4 +private const val SCROLL_SENSITIVITY = 15.0 +private const val SCROLLBAR_FADE_DURATION_MS = 1000L +private const val MIN_SCROLLBAR_THUMB_SIZE = 10 +private const val SCROLL_SNAP_EPSILON = 0.1 + +/** + * The axis along which a [Scrollable] container scrolls its content. + */ +enum class ScrollDirection { + VERTICAL, HORIZONTAL; + + /** Returns [horizontal] or [vertical] depending on the direction. */ + fun choose(horizontal: Double, vertical: Double): Double = + if (this == VERTICAL) vertical else horizontal +} + +/** + * Mutable state holder for a [Scrollable] composable. + * + * Create and remember an instance via [rememberScrollableState] and pass it to [Scrollable] + * when you need programmatic control over the scroll position. + */ +@Stable +class ScrollableState { + /** Current target scroll offset in pixels. Animate towards [currentScrollPosition]. */ + var scrollOffset by mutableStateOf(0.0) + /** Smoothly interpolated scroll position used for actual rendering. */ + var currentScrollPosition by mutableStateOf(0.0) + /** Maximum scroll offset (content size − container size). */ + var maxScroll by mutableStateOf(0) + /** Size of the scrollable content in the scroll axis, in pixels. */ + var childSize by mutableStateOf(0) + /** Size of the visible container in the scroll axis, in pixels. */ + var containerSize by mutableStateOf(0) + /** Whether the user is currently dragging the scrollbar thumb. */ + var isDraggingScrollbar by mutableStateOf(false) + /** Timestamp of the last user interaction (used for fade-out animation). */ + var lastInteractTime by mutableStateOf(0L) + + /** Records an interaction so the scrollbar fade-out timer resets. */ + fun onInteraction() { lastInteractTime = System.currentTimeMillis() } + + /** + * Scrolls by [delta] pixels, clamping the result to the valid range. + * + * @param delta Positive values scroll forward (down/right); negative scrolls back. + */ + fun scrollBy(delta: Double) { + scrollOffset = (scrollOffset + delta).coerceIn(0.0, maxScroll.toDouble()) + onInteraction() + } +} + +/** + * Creates and remembers a [ScrollableState] for use with [Scrollable]. + */ +@Composable +fun rememberScrollableState(): ScrollableState = remember { ScrollableState() } + +/** + * A container that allows its single child to be scrolled when the child's content + * exceeds the container's bounds. + * + * A fade-in/out scrollbar thumb is rendered automatically when content overflows. The + * scrollbar supports mouse-drag interaction and responds to the keyboard arrow keys, + * Page Up/Down. + * + * ### Example + * ```kotlin + * Scrollable(modifier = Modifier.size(200, 100)) { + * Column { + * repeat(20) { Text(Component.literal("Item $it")) } + * } + * } + * ``` + * + * @param direction The [ScrollDirection] (vertical or horizontal). + * @param scrollbarColor The fill colour of the scrollbar thumb. + * @param modifier Modifiers applied to the Scrollable layout node. + * @param state External [ScrollableState]; defaults to a locally remembered instance. + * @param content The single scrollable child composable. + */ +@Composable +fun Scrollable( + direction: ScrollDirection = ScrollDirection.VERTICAL, + scrollbarColor: KColor = KColor.DARK_GRAY, + modifier: Modifier = Modifier, + state: ScrollableState = rememberScrollableState(), + content: @Composable () -> Unit, +) { + val clipSource = remember { SlotClipSource() } + + val measurePolicy = remember(direction) { + object : MeasurePolicy { + override fun measure( + scope: MeasureScope, + measurables: List, + constraints: Constraints, + ): MeasureResult { + if (measurables.isEmpty()) return MeasureResult(constraints.minWidth, constraints.minHeight) {} + + val contentConstraints = if (direction == ScrollDirection.VERTICAL) + constraints.copy(minHeight = 0, maxHeight = Int.MAX_VALUE) + else + constraints.copy(minWidth = 0, maxWidth = Int.MAX_VALUE) + + val placeable = measurables.first().measure(contentConstraints) + + val resolvedWidth = if (direction == ScrollDirection.HORIZONTAL) { + resolveScrollableViewportAxis(placeable.width, constraints.minWidth, constraints.maxWidth) + } else { + resolveScrollableContentAxis(placeable.width, constraints.minWidth, constraints.maxWidth) + } + + val resolvedHeight = if (direction == ScrollDirection.VERTICAL) { + resolveScrollableViewportAxis(placeable.height, constraints.minHeight, constraints.maxHeight) + } else { + resolveScrollableContentAxis(placeable.height, constraints.minHeight, constraints.maxHeight) + } + + state.childSize = direction.choose(placeable.width.toDouble(), placeable.height.toDouble()).toInt() + state.containerSize = direction.choose(resolvedWidth.toDouble(), resolvedHeight.toDouble()).toInt() + state.maxScroll = max(0, state.childSize - state.containerSize) + state.scrollOffset = state.scrollOffset.coerceIn(0.0, state.maxScroll.toDouble()) + + return MeasureResult(resolvedWidth, resolvedHeight) { + val scrollPos = state.currentScrollPosition.roundToInt() + if (direction == ScrollDirection.VERTICAL) placeable.placeAt(0, -scrollPos) + else placeable.placeAt(-scrollPos, 0) + } + } + } + } + + CompositionLocalProvider(LocalSlotClipBounds provides clipSource) { + Layout( + name = "Scrollable", + measurePolicy = measurePolicy, + renderer = object : Renderer { + override fun render(node: UINode, x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) = guiGraphics { + enableScissor(x, y, x + node.width, y + node.height) + } + + override fun renderAfterChildren(node: UINode, x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) = guiGraphics { + val lerpFactor = (0.4f * partialTick).coerceIn(0.05f, 1f).toDouble() + val next = state.currentScrollPosition + (state.scrollOffset - state.currentScrollPosition) * lerpFactor + state.currentScrollPosition = if (abs(state.scrollOffset - next) <= SCROLL_SNAP_EPSILON) state.scrollOffset else next + + if (state.maxScroll > 0) { + val timeSinceInteract = System.currentTimeMillis() - state.lastInteractTime + if (!(timeSinceInteract > SCROLLBAR_FADE_DURATION_MS && !state.isDraggingScrollbar)) { + val fadeAlpha = if (state.isDraggingScrollbar) 1f else 1f - (timeSinceInteract.toFloat() / SCROLLBAR_FADE_DURATION_MS) + val alpha = Mth.clamp((fadeAlpha * scrollbarColor.alpha).toInt(), 0, 255) + if (alpha > 0) { + val colorWithAlpha = scrollbarColor.rgb or (alpha shl 24) + val trackSize = state.containerSize + val thumbSize = max(MIN_SCROLLBAR_THUMB_SIZE, (trackSize.toFloat() / state.childSize * trackSize).toInt()) + val scrollPct = if (state.maxScroll > 0) state.currentScrollPosition / state.maxScroll else 0.0 + val thumbPos = scrollPct * (trackSize - thumbSize) + + if (direction == ScrollDirection.VERTICAL) { + val tx = x + node.width - SCROLLBAR_THICKNESS + val ty = y + thumbPos.roundToInt() + fill(tx, ty, tx + SCROLLBAR_THICKNESS, ty + thumbSize, colorWithAlpha) + } else { + val tx = x + thumbPos.roundToInt() + val ty = y + node.height - SCROLLBAR_THICKNESS + fill(tx, ty, tx + thumbSize, ty + SCROLLBAR_THICKNESS, colorWithAlpha) + } + } + } + } + disableScissor() + } + }, + modifier = modifier + .onGloballyPositioned { coords -> + clipSource.updateOrigin(coords) + } + .onSizeChanged { size -> + clipSource.updateSize(size) + } + .onScroll { _, event -> + val rawDelta = if (direction == ScrollDirection.HORIZONTAL && event.scrollX != 0.0) { + -event.scrollX + } else { + -event.scrollY + } + state.scrollBy(rawDelta * SCROLL_SENSITIVITY) + event.consume() + } + .onPointerEvent(PointerEventType.PRESS) { node, event -> + val minX = if (direction == ScrollDirection.VERTICAL) node.x + node.width - SCROLLBAR_THICKNESS else node.x + val minY = if (direction == ScrollDirection.VERTICAL) node.y else node.y + node.height - SCROLLBAR_THICKNESS + val maxX = node.x + node.width + val maxY = node.y + node.height + + if (event.mouseX >= minX && event.mouseX <= maxX && event.mouseY >= minY && event.mouseY <= maxY) { + state.isDraggingScrollbar = true + state.onInteraction() + event.consume() + } + } + .onPointerEvent(PointerEventType.GLOBAL_RELEASE) { _, _ -> state.isDraggingScrollbar = false } + .onDrag { _, event -> + if (!state.isDraggingScrollbar) return@onDrag + val pixelDelta = direction.choose(event.dragX, event.dragY) + val trackSize = state.containerSize + val thumbSize = max(MIN_SCROLLBAR_THUMB_SIZE, (trackSize.toFloat() / state.childSize * trackSize).toInt()) + if (trackSize > thumbSize) { + state.scrollBy(pixelDelta * (state.maxScroll.toFloat() / (trackSize - thumbSize))) + // Keep drag feedback immediate while preserving smoothing for wheel/key input. + state.currentScrollPosition = state.scrollOffset + } + event.consume() + } + .onKeyEvent { _, event -> + val amount = state.containerSize * 0.8 + when (event.keyCode) { + GLFW.GLFW_KEY_DOWN -> if (direction == ScrollDirection.VERTICAL) state.scrollBy(SCROLL_SENSITIVITY) + GLFW.GLFW_KEY_UP -> if (direction == ScrollDirection.VERTICAL) state.scrollBy(-SCROLL_SENSITIVITY) + GLFW.GLFW_KEY_RIGHT -> if (direction == ScrollDirection.HORIZONTAL) state.scrollBy(SCROLL_SENSITIVITY) + GLFW.GLFW_KEY_LEFT -> if (direction == ScrollDirection.HORIZONTAL) state.scrollBy(-SCROLL_SENSITIVITY) + GLFW.GLFW_KEY_PAGE_DOWN -> state.scrollBy(amount) + GLFW.GLFW_KEY_PAGE_UP -> state.scrollBy(-amount) + else -> return@onKeyEvent + } + event.consume() + }, + content = content, + ) + } +} + +internal fun resolveScrollableViewportAxis(childSize: Int, min: Int, max: Int): Int { + // For the scrolling axis, fill the available finite viewport so overflow can scroll. + if (max == Int.MAX_VALUE) return childSize.coerceAtLeast(min) + return max.coerceAtLeast(min) +} + +internal fun resolveScrollableContentAxis(childSize: Int, min: Int, max: Int): Int { + if (max == Int.MAX_VALUE) return childSize.coerceAtLeast(min) + return childSize.coerceIn(min, max) +} + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Surface.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Surface.kt new file mode 100644 index 000000000..44ceb8f7e --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Surface.kt @@ -0,0 +1,77 @@ +package net.kernelpanicsoft.archie.gui.composables.containers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.Renderer +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.debug +import net.kernelpanicsoft.archie.gui.modifiers.sizeIn +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.theme.LocalTheme +import net.kernelpanicsoft.archie.gui.theme.SimpleThemeState +import net.kernelpanicsoft.archie.gui.theme.ThemeVariants +import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.minecraft.client.gui.GuiGraphics + + +/** + * A [net.kernelpanicsoft.archie.gui.layout.Box]-like layout node that paints a themed background texture behind its children. + * + * The texture is resolved from the current [LocalTheme] by [texture] key and [variant], and + * is drawn nine-sliced if the theme defines it as such, otherwise stretched to fit like a + * simple sprite (in which case the surface has a minimum size matching the sprite's own). + * [Panel] builds on top of this to add content padding. + * + * @param contentAlignment Alignment of [content] within the surface, as in [net.kernelpanicsoft.archie.gui.layout.Box]. + * @param modifier Additional modifiers applied to the layout node. + * @param texture The themed texture key to look up via [LocalTheme]. + * @param variant The theme variant of [texture] to use. See [ThemeVariants]. + */ +@Composable +fun Surface( + contentAlignment: Alignment = Alignment.TopStart, + modifier: Modifier = Modifier, + texture: String = "surface", + variant: String = ThemeVariants.DEFAULT, + content: @Composable () -> Unit +) { + val measurePolicy = remember(contentAlignment) { BoxMeasurePolicy(contentAlignment) } + val theme = LocalTheme.current + val composableTheme = theme.getComposableTheme(texture) + val state = composableTheme.getState(TextureStates.DEFAULT, variant) + + Layout( + name = "Surface", + measurePolicy = measurePolicy, + renderer = object : Renderer + { + override fun render( + node: UINode, + x: Int, + y: Int, + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) = guiGraphics { + drawThemeState(state, x, y, node.width, node.height) + } + }, + modifier = Modifier.debug(state.texture.toString()).apply { + if (!composableTheme.isNineslice) { + with(composableTheme.states[TextureStates.DEFAULT] as SimpleThemeState) { + sizeIn( + minWidth = width, + minHeight = height + ) + } + } + } then modifier, + content = content + ) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/TabContainer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/TabContainer.kt new file mode 100644 index 000000000..f9ff390f7 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/TabContainer.kt @@ -0,0 +1,410 @@ +package net.kernelpanicsoft.archie.gui.composables.containers + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import net.kernelpanicsoft.archie.gui.composables.basic.Text +import net.kernelpanicsoft.archie.gui.composables.basic.Texture +import net.kernelpanicsoft.archie.gui.composables.input.ButtonCore +import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates +import net.kernelpanicsoft.archie.gui.composables.theme.WidgetState +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.layout.Arrangement +import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy +import net.kernelpanicsoft.archie.gui.layout.Column +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.Renderer +import net.kernelpanicsoft.archie.gui.layout.Row +import net.kernelpanicsoft.archie.gui.layout.dp +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.sizeIn +import net.kernelpanicsoft.archie.gui.modifiers.position.padding +import net.kernelpanicsoft.archie.gui.modifiers.position.offset +import net.kernelpanicsoft.archie.gui.modifiers.position.zIndex +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.theme.LocalTheme +import net.kernelpanicsoft.archie.gui.theme.SimpleThemeState +import net.kernelpanicsoft.archie.gui.theme.ThemeVariants +import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import net.minecraft.client.gui.GuiGraphics + +/** Built-in themed texture keys for [Tab]/[TabContainer], matching vanilla tab styles. */ +object TabTextures +{ + /** The in-game pause-menu tab style (e.g. Create World screen). */ + const val GAME = "tab_game" + /** The main-menu tab style. */ + const val MENU = "tab_menu" +} + +private const val SELECTED_ELEVATION_PX = 2 +private const val DEFAULT_ICON_SPACING = 4 +private const val DEFAULT_CONTENT_SPACING = 6 +private const val DEFAULT_CONTENT_WIDTH = 9 * 18 + +/** + * Declarative tab bar modeled after the vanilla Create World screen tabs. + * + * @param tabs Ordered list of tab specs to render. + * @param state External state holder controlling the selected tab. + * @param modifier Modifier applied to the outer container (or scrollable wrapper). + * @param onTabSelected Callback invoked after a tab becomes selected. + * @param tabSpacing Horizontal spacing between neighboring tabs, in pixels. + * @param scrollable When true, wraps the tab row in a horizontal [Scrollable] viewport. + * @param scrollState Optional externally managed [ScrollableState] (only used when [scrollable]). + * @param contentSpacing Vertical spacing between the tab row and the selected tab content, in pixels. + */ +@Composable +fun TabContainer( + tabs: List, + state: TabContainerState = rememberTabContainerState(tabs), + modifier: Modifier = Modifier, + onTabSelected: (TabSpec) -> Unit = {}, + tabSpacing: Int = 2, + scrollable: Boolean = true, + scrollState: ScrollableState? = null, + contentSpacing: Int = DEFAULT_CONTENT_SPACING, + elevateSelected: Boolean = false, + tabTexture: String = TabTextures.GAME, +) { + state.ensureSelection(tabs) + + if (tabs.isEmpty()) { + if (scrollable) { + Scrollable( + direction = ScrollDirection.HORIZONTAL, + modifier = modifier, + state = scrollState ?: rememberScrollableState(), + ) {} + } + return + } + + val rowContent: @Composable (Modifier) -> Unit = { rowModifier -> + Row( + modifier = rowModifier, + horizontalArrangement = Arrangement.spacedBy(tabSpacing.dp), + verticalAlignment = Alignment.Bottom, + ) { + tabs.forEach { tab -> + Tab( + spec = tab, + selected = state.isSelected(tab.id), + texture = tabTexture, + elevateSelected = elevateSelected, + onClick = { + if (!tab.enabled) return@Tab + state.select(tab.id) + onTabSelected(tab) + }, + ) + } + } + } + + Column( + modifier = modifier, + verticalArrangement = Arrangement.spacedBy(contentSpacing.dp), + ) { + if (scrollable) { + Scrollable( + direction = ScrollDirection.HORIZONTAL, + state = scrollState ?: rememberScrollableState(), + ) { + rowContent(Modifier.padding(horizontal = 4, vertical = 2)) + } + } else { + rowContent(Modifier) + } + + state.selectedTab(tabs)?.content?.let { content -> + content() + } + } +} + +/** + * DSL overload allowing tabs to be declared inline via [TabContainerScope.tab] without + * manually building a [TabSpec] list. + * + * @param contentWrapper Wraps each tab's content composable, e.g. to add common padding. + * Defaults to rendering the content unwrapped. + * @param builder Declares the tabs, in order, via [TabContainerScope.tab]. + */ +@Composable +fun TabContainer( + modifier: Modifier = Modifier, + state: TabContainerState? = null, + onTabSelected: (TabSpec) -> Unit = {}, + tabSpacing: Int = 2, + scrollable: Boolean = true, + scrollState: ScrollableState? = null, + contentSpacing: Int = DEFAULT_CONTENT_SPACING, + contentWrapper: @Composable ((@Composable (() -> Unit)) -> Unit)? = null, + tabTexture: String = TabTextures.GAME, + builder: TabContainerScope.() -> Unit, +) { + val scope = remember { TabContainerScope(contentWrapper = contentWrapper ?: { content -> content()}) } + scope.reset() + scope.builder() + val tabs = scope.build() + val resolvedState = state ?: rememberTabContainerState(tabs) + + TabContainer( + tabs = tabs, + state = resolvedState, + modifier = modifier, + onTabSelected = onTabSelected, + tabSpacing = tabSpacing, + scrollable = scrollable, + scrollState = scrollState, + contentSpacing = contentSpacing, + tabTexture = tabTexture, + ) +} + +/** + * A [TabContainer] whose selected tab content is wrapped in a [Panel] by default, elevated + * (drawn above neighboring tabs) when selected. Used by [TabContainerPanel]. + * + * @param contentWrapper Wraps each tab's content; defaults to a [Panel] offset to sit flush + * under the tab row. + * @param builder Declares the tabs, in order, via [TabContainerScope.tab]. + */ +@Composable +fun TabPanel( + modifier: Modifier = Modifier, + state: TabContainerState? = null, + onTabSelected: (TabSpec) -> Unit = {}, + tabSpacing: Int = 2, + scrollable: Boolean = true, + scrollState: ScrollableState? = null, + contentWrapper: @Composable ((@Composable (() -> Unit)) -> Unit)? = null, + builder: TabContainerScope.() -> Unit, +) { + val scope = remember { TabContainerScope(contentWrapper = contentWrapper ?: { content -> + Panel(modifier = Modifier.offset(y = -12)) { + content() + } + }) } + scope.reset() + scope.builder() + val tabs = scope.build() + val resolvedState = state ?: rememberTabContainerState(tabs) + + TabContainer( + tabs = tabs, + state = resolvedState, + modifier = modifier, + onTabSelected = onTabSelected, + tabSpacing = tabSpacing, + scrollable = scrollable, + scrollState = scrollState, + tabTexture = TabTextures.GAME, + elevateSelected = true, + ) +} + +/** Restricts the [TabContainerScope.tab] DSL to its own receiver scope. */ +@DslMarker +annotation class TabContainerDsl + +/** Receiver scope for the [TabContainer]/[TabPanel] DSL `builder` lambda. */ +@TabContainerDsl +class TabContainerScope internal constructor(val contentWrapper: @Composable (@Composable () -> Unit) -> Unit = {it()}) { + private val specs = mutableListOf() + + /** Declares a tab with the given [id], [title], and [content]. */ + fun tab( + id: String, + title: Component, + icon: TabIcon? = null, + enabled: Boolean = true, + content: @Composable (() -> Unit), + ) { + specs += TabSpec( + id = id, + title = title, + icon = icon, + enabled = enabled, + content = { contentWrapper(content) }, + ) + } + + /** Declares a tab from a pre-built [TabSpec], bypassing [contentWrapper]. */ + fun tab(spec: TabSpec) { + specs += spec + } + + internal fun reset() = specs.clear() + + internal fun build(): List = specs.toList() +} + +/** Data describing a single Create World style tab. */ +data class TabSpec( + val id: String, + val title: Component, + val icon: TabIcon? = null, + val enabled: Boolean = true, + val content: @Composable (() -> Unit), +) + +/** Sprite descriptor used for optional tab icons. */ +data class TabIcon( + val texture: ResourceLocation, + val uOffset: Float = 0f, + val vOffset: Float = 0f, + val regionWidth: Int = 28, + val regionHeight: Int = 32, + val textureWidth: Int = 256, + val textureHeight: Int = 256, + val displayWidth: Int = regionWidth, + val displayHeight: Int = regionHeight, +) + +/** + * Tracks which tab id is selected in a [TabContainer]. Create via [rememberTabContainerState]. + */ +@Stable +class TabContainerState internal constructor(initialSelectedId: String?) { + private var selectedId by mutableStateOf(initialSelectedId) + + /** The currently selected tab's id, or `null` if nothing is selected yet. */ + val selectedTabId: String? get() = selectedId + + /** Whether [tabId] is the currently selected tab. */ + fun isSelected(tabId: String): Boolean = selectedId == tabId + + /** Selects the tab with the given [tabId]. */ + fun select(tabId: String) { + selectedId = tabId + } + + /** + * Ensures the selection is valid for [tabs]: falls back to the first enabled tab (or the + * very first tab, if none are enabled) when there's no selection or the selected id no + * longer exists/is disabled among [tabs]. + */ + fun ensureSelection(tabs: List) { + if (tabs.isEmpty()) { + selectedId = null + return + } + val current = selectedId + val active = current?.let { id -> tabs.firstOrNull { it.id == id && it.enabled } } + if (active != null) return + selectedId = tabs.firstOrNull { it.enabled }?.id ?: tabs.first().id + } + + /** The index of the selected tab within [tabs], or -1 if none is selected. */ + fun selectedIndex(tabs: List): Int = + tabs.indexOfFirst { it.id == selectedId } + + /** The [TabSpec] currently selected within [tabs], or `null` if none is selected. */ + fun selectedTab(tabs: List): TabSpec? = + tabs.firstOrNull { it.id == selectedId } +} + +/** Creates and remembers a [TabContainerState], initially selecting [initialSelectedId]. */ +@Composable +fun rememberTabContainerState( + tabs: List, + initialSelectedId: String? = tabs.firstOrNull { it.enabled }?.id, +): TabContainerState = remember(initialSelectedId) { TabContainerState(initialSelectedId) } + +/** + * A single clickable tab button, rendering [spec]'s icon/title and switching its themed + * texture state based on [selected]/hover/press. Used internally by [TabContainer]; use that + * (or the DSL/[TabPanel] variants) rather than calling this directly in most cases. + */ +@Composable +fun Tab( + spec: TabSpec, + selected: Boolean, + modifier: Modifier = Modifier, + elevateSelected: Boolean = false, + enabled: Boolean = spec.enabled, + texture: String = TabTextures.GAME, + variant: String = ThemeVariants.DEFAULT, + iconSpacing: Int = DEFAULT_ICON_SPACING, + onClick: (TabSpec) -> Unit, +) { + val theme = LocalTheme.current + val composableTheme = theme.getComposableTheme(texture) + val measurePolicy = remember { BoxMeasurePolicy(Alignment.CenterStart) } + + ButtonCore( + onClick = { onClick(spec) }, + enabled = enabled, + modifier = modifier, + ) { isHovered, isPressed -> + val stateKey = WidgetState.resolve( + composableTheme, variant, + WidgetState.clicked(selected || isPressed), WidgetState.hovered(isHovered), + enabled = enabled, + ) + val state = composableTheme.getState(stateKey, variant) + val offsetModifier = Modifier + .zIndex(if (selected && elevateSelected) 1f else 0f) + .offset(x = 0, y = if (selected && !elevateSelected) -SELECTED_ELEVATION_PX else 0) + .padding(horizontal = 10, vertical = 6) + val sizeModifier = if (!composableTheme.isNineslice) { + val defaultState = composableTheme.states[TextureStates.DEFAULT] as SimpleThemeState + Modifier.sizeIn(minWidth = defaultState.width, minHeight = defaultState.height) + } else Modifier + + Layout( + name = "Tab", + measurePolicy = measurePolicy, + renderer = object : Renderer { + override fun render( + node: UINode, + x: Int, + y: Int, + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float, + ) = guiGraphics { + node.renderState = stateKey + drawThemeState(state, x, y, node.width, node.height) + } + }, + modifier = sizeModifier.then(offsetModifier), + ) { + Row( + modifier = Modifier, + horizontalArrangement = Arrangement.spacedBy(iconSpacing.dp), + verticalAlignment = Alignment.CenterVertically, + ) { + spec.icon?.let { icon -> + Texture( + loc = icon.texture, + uOffset = icon.uOffset, + vOffset = icon.vOffset, + u = icon.regionWidth, + v = icon.regionHeight, + textureWidth = icon.textureWidth, + textureHeight = icon.textureHeight, + modifier = Modifier.sizeIn( + minWidth = icon.displayWidth, + minHeight = icon.displayHeight, + ), + ) + } + Text( + text = spec.title, + color = if (selected) theme.darkTextColor else theme.lightTextColor, + dropShadow = !selected + ) + } + } + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Button.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Button.kt new file mode 100644 index 000000000..1a8ec9093 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Button.kt @@ -0,0 +1,145 @@ +package net.kernelpanicsoft.archie.gui.composables.input + +import androidx.compose.runtime.* +import net.kernelpanicsoft.archie.gui.animation.AnimationSpec +import net.kernelpanicsoft.archie.gui.animation.Easings +import net.kernelpanicsoft.archie.gui.animation.animateInt +import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates +import net.kernelpanicsoft.archie.gui.composables.theme.WidgetState +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.Renderer +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.DebugModifier +import net.kernelpanicsoft.archie.gui.modifiers.sizeIn +import net.kernelpanicsoft.archie.gui.modifiers.position.offset +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.theme.LocalTheme +import net.kernelpanicsoft.archie.gui.theme.SimpleThemeState +import net.kernelpanicsoft.archie.gui.theme.ThemeVariants +import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.minecraft.client.gui.GuiGraphics +import kotlin.time.Duration.Companion.milliseconds + +/** + * A standard themed, clickable button. + * + * Renders the themed [texture] state ([TextureStates.DEFAULT]/[TextureStates.HOVERED]/ + * [TextureStates.CLICKED]/[TextureStates.DISABLED]) behind [content], animating a 1px press + * offset while held. For fully custom visuals, use [ButtonCore] directly instead. + * + * @param onClick Invoked with the receiving [UINode] when the button is pressed. + * @param modifier Additional modifiers applied to the outer clickable container. + * @param enabled When `false`, the disabled state is drawn and pointer events are ignored. + * @param texture The themed texture key to look up via [LocalTheme]. + * @param variant The theme variant of [texture] to use. See [ThemeVariants]. + * @param content The button's foreground content (e.g. a [net.kernelpanicsoft.archie.gui.composables.basic.Text]). + */ +@Composable +fun Button( + onClick: (UINode) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + texture: String = "button", + variant: String = ThemeVariants.DEFAULT, + content: @Composable () -> Unit = {} +) { + val theme = LocalTheme.current + val composableTheme = theme.getComposableTheme(texture) + val measurePolicy = remember { BoxMeasurePolicy(Alignment.Center) } + + ButtonCore( + onClick, + modifier, + enabled + ) { isHovered, isPressed -> + val pressOffset = animateInt( + targetValue = if (isPressed) 1 else 0, + spec = AnimationSpec(durationMillis = 90.milliseconds, easing = Easings.OutCubic), + ) + + Layout( + name = "Button", + content = content, + measurePolicy = measurePolicy, + renderer = object : Renderer + { + override fun render( + node: UINode, + x: Int, + y: Int, + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) = guiGraphics { + val stateKey = WidgetState.resolve( + composableTheme, variant, + WidgetState.clicked(isPressed), WidgetState.hovered(isHovered), + enabled = enabled, + ) + node.renderState = stateKey + val state = composableTheme.getState(stateKey, variant) + + drawThemeState(state, x, y, node.width, node.height) + } + }, + modifier = modifier.apply { + if (!composableTheme.isNineslice) { + with(composableTheme.states[TextureStates.DEFAULT] as SimpleThemeState) { + sizeIn( + minWidth = width, + minHeight = height + ) + } + } + }.offset(x = 0, y = pressOffset) + ) + } +} + + +/** + * A stateless clickable container composable. + * + * `ButtonCore` manages hover and pressed state internally and exposes them to [content] + * via the lambda parameters. It handles cursor changes and the full pointer-event lifecycle, + * but applies no visual styling of its own — that is left entirely to [content]. + * + * Use [ButtonCore] when you need custom button visuals. For a standard themed button, use + * [Button] instead. + * + * ### Example + * ```kotlin + * ButtonCore(onClick = { println("Clicked!") }) { isHovered, isPressed -> + * Box( + * modifier = Modifier.background(if (isHovered) KColor.LIGHT_GRAY else KColor.GRAY) + * .size(80, 20) + * ) { + * Text(Component.literal("Click me")) + * } + * } + * ``` + * + * @param onClick Invoked with the receiving [UINode] when the button is pressed. + * @param modifier Additional modifiers applied to the outer clickable container. + * @param enabled When `false`, pointer events are ignored and no cursor change occurs. + * @param content The button's visual content, receiving `isHovered` and `isPressed` booleans. + */ +@Composable +fun ButtonCore( + onClick: (UINode) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + content: @Composable (isHovered: Boolean, isPressed: Boolean) -> Unit, +) { + Clickable( + onClick = onClick, + enabled = enabled, + modifier = Modifier.then(DebugModifier(strs = listOf("Enabled: $enabled"))).then(modifier), + ) { isHovered, isPressed -> + content(isHovered, isPressed) + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Checkbox.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Checkbox.kt new file mode 100644 index 000000000..82d2a01bd --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Checkbox.kt @@ -0,0 +1,129 @@ +package net.kernelpanicsoft.archie.gui.composables.input + +import androidx.compose.runtime.* +import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates +import net.kernelpanicsoft.archie.gui.composables.theme.WidgetState +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.layout.Box +import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.Renderer +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.debug +import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType +import net.kernelpanicsoft.archie.gui.modifiers.input.onPointerEvent +import net.kernelpanicsoft.archie.gui.modifiers.sizeIn +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.theme.LocalTheme +import net.kernelpanicsoft.archie.gui.theme.SimpleThemeState +import net.kernelpanicsoft.archie.gui.theme.ThemeVariants +import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.minecraft.client.gui.GuiGraphics + +/** + * A standard themed checkbox. + * + * Renders the themed [texture] state - a combined checked+hovered state is used when both + * apply and the theme defines it. Built on top of [CheckboxCore]; use that directly for + * fully custom visuals. + * + * @param checked The current checked state. + * @param modifier Additional modifiers applied to the outer container. + * @param texture The themed texture key to look up via [LocalTheme]. + * @param variant The theme variant of [texture] to use. See [ThemeVariants]. + * @param onCheckedChange Called with the new checked value when the user clicks. + */ +@Composable +fun Checkbox( + checked: Boolean = false, + modifier: Modifier = Modifier, + texture: String = "checkbox", + variant: String = ThemeVariants.DEFAULT, + onCheckedChange: (Boolean) -> Unit, +) { + val theme = LocalTheme.current + val composableTheme = theme.getComposableTheme(texture) + val sizeModifier = if (!composableTheme.isNineslice) { + with(composableTheme.states[TextureStates.DEFAULT] as SimpleThemeState) { + Modifier.sizeIn( + minWidth = width, + minHeight = height + ) + } + } else Modifier + + CheckboxCore( + checked, + sizeModifier.then(modifier), + onCheckedChange + ) { isHovered -> + Layout( + name = "Checkbox", + measurePolicy = BoxMeasurePolicy(Alignment.Center), + renderer = object : Renderer + { + override fun render( + node: UINode, + x: Int, + y: Int, + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float + ) = guiGraphics { + val stateKey = WidgetState.resolve( + composableTheme, variant, + WidgetState.clicked(checked), WidgetState.hovered(isHovered), + ) + node.renderState = stateKey + val state = composableTheme.getState(stateKey, variant) + + drawThemeState(state, x, y, node.width, node.height) + } + }, + modifier = sizeModifier + ) + } +} + +/** + * A stateless, unstyled toggle composable. + * + * `CheckboxCore` manages hover state internally and exposes it to [content]. All visual + * styling (textures, colours, checked indicator) is the responsibility of [content]. Use + * this as the base for custom or theme-driven checkbox implementations. + * + * ### Example + * ```kotlin + * var checked by remember { mutableStateOf(false) } + * CheckboxCore(checked = checked, onCheckedChange = { checked = it }) { isHovered -> + * Box(modifier = Modifier.size(16, 16).background(if (checked) KColor.GREEN else KColor.GRAY)) + * } + * ``` + * + * @param checked The current checked state. + * @param modifier Additional modifiers applied to the outer [Box]. + * @param onCheckedChange Called with the new checked value when the user clicks. + * @param content The visual content; receives `isHovered` for styling. + */ +@Composable +fun CheckboxCore( + checked: Boolean = false, + modifier: Modifier = Modifier, + onCheckedChange: (Boolean) -> Unit, + content: @Composable (isHovered: Boolean) -> Unit, +) { + var hovered by remember { mutableStateOf(false) } + + Box( + modifier = Modifier + .debug("Hovered: $hovered") + .onPointerEvent(PointerEventType.ENTER) { _, e -> hovered = true; e.consume() } + .onPointerEvent(PointerEventType.EXIT) { _, e -> hovered = false; e.consume() } + .onPointerEvent(PointerEventType.PRESS) { _, e -> onCheckedChange(!checked); e.consume() } + .then(modifier), + ) { + content(hovered) + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Clickable.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Clickable.kt new file mode 100644 index 000000000..cbd76eb7e --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Clickable.kt @@ -0,0 +1,92 @@ +package net.kernelpanicsoft.archie.gui.composables.input + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.DisposableEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.layout.Box +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType +import net.kernelpanicsoft.archie.gui.modifiers.input.onPointerEvent +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.util.minecraftClient +import net.minecraft.client.Minecraft +import org.lwjgl.glfw.GLFW + +private object CursorCache { + val handCursor: Long by lazy { GLFW.glfwCreateStandardCursor(GLFW.GLFW_HAND_CURSOR) } +} + +private fun setHandCursor(enabled: Boolean) { + // GLFW calls must happen on the render thread; DisposableEffect callbacks run on the + // recomposition dispatcher, so hop over via Minecraft's thread-safe task queue. + minecraftClient.execute { + val window = minecraftClient.window.window + GLFW.glfwSetCursor(window, if (enabled) CursorCache.handCursor else 0L) + } +} + +/** + * Low-level unstyled clickable container used by higher-level inputs like [ButtonCore]. + * + * Tracks hover/press state and fires [onClick] on press (not release), showing the system + * hand cursor on hover when [showHandCursor] is `true`. Applies no visual styling itself - + * that is entirely up to [content]. + * + * @param onClick Invoked with the receiving [UINode] on press. + * @param modifier Additional modifiers applied to the outer [Box]. + * @param enabled When `false`, pointer events are ignored and no cursor change occurs. + * @param showHandCursor Whether to switch to the hand cursor while hovered. + * @param content The visual content; receives `isHovered`/`isPressed` for styling. + */ +@Composable +fun Clickable( + onClick: (UINode) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + showHandCursor: Boolean = true, + content: @Composable (isHovered: Boolean, isPressed: Boolean) -> Unit, +) { + var hovered by remember { mutableStateOf(false) } + var pressed by remember { mutableStateOf(false) } + + DisposableEffect(enabled, hovered, showHandCursor) { + if ((!enabled || !hovered) && showHandCursor) setHandCursor(false) + onDispose { + if (showHandCursor) setHandCursor(false) + } + } + + Box( + modifier = Modifier + .onPointerEvent(PointerEventType.ENTER) { _, e -> + if (!enabled) return@onPointerEvent + hovered = true + if (showHandCursor) setHandCursor(true) + e.consume() + } + .onPointerEvent(PointerEventType.EXIT) { _, e -> + hovered = false + pressed = false + if (showHandCursor) setHandCursor(false) + if (enabled) e.consume() + } + .onPointerEvent(PointerEventType.PRESS) { node, e -> + if (!enabled) return@onPointerEvent + pressed = true + onClick(node) + e.consume(true) + } + .onPointerEvent(PointerEventType.GLOBAL_RELEASE) { _, _ -> + pressed = false + } + .then(modifier), + contentAlignment = Alignment.Center, + ) { + content(hovered, pressed) + } +} + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/ColorPicker.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/ColorPicker.kt new file mode 100644 index 000000000..1960dda16 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/ColorPicker.kt @@ -0,0 +1,179 @@ +package net.kernelpanicsoft.archie.gui.composables.input + +import androidx.compose.runtime.* +import net.kernelpanicsoft.archie.gui.layout.* +import net.kernelpanicsoft.archie.gui.modifiers.Constraints +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.input.* +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.util.HsvColor +import net.kernelpanicsoft.archie.gui.util.KColor +import net.kernelpanicsoft.archie.gui.util.extension.drawRectOutline +import net.kernelpanicsoft.archie.gui.util.extension.fillGradient +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.minecraft.client.gui.GuiGraphics +import kotlin.math.max + +// ── Internal sub-composables ────────────────────────────────────────────── + +@Composable +private fun SaturationValueArea( + modifier: Modifier = Modifier, + hue: Float, + saturation: Float, + value: Float, + onSaturationValueChanged: (saturation: Float, value: Float) -> Unit, +) { + val onEvent = { node: LayoutNode, event: PointerEvent -> + val newSat = ((event.mouseX - node.absoluteCoords.x) / node.width).toFloat().coerceIn(0f, 1f) + val newVal = (1f - ((event.mouseY - node.absoluteCoords.y) / node.height).toFloat()).coerceIn(0f, 1f) + onSaturationValueChanged(newSat, newVal) + event.consume() + } + + Layout( + name = "SaturationValueArea", + measurePolicy = { _, _, constraints -> MeasureResult(constraints.minWidth, constraints.minHeight) {} }, + renderer = object : Renderer { + override fun render(node: UINode, x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) = guiGraphics { + fillGradient(x, y, node.width, node.height, + KColor.ofHsv(hue, 0f, 1f).argb, KColor.ofHsv(hue, 1f, 1f).argb, + KColor.ofHsv(hue, 0f, 0f).argb, KColor.ofHsv(hue, 1f, 0f).argb) + drawRectOutline(x + (saturation * node.width).toInt() - 2, y + ((1 - value) * node.height).toInt() - 2, 4, 4, KColor.WHITE.argb) + } + }, + modifier = modifier + .onPointerEvent(PointerEventType.PRESS, onEvent) + .onDrag(onDragEvent = onEvent), + ) +} + +@Composable +private fun HueBar(modifier: Modifier = Modifier, hue: Float, onHueChanged: (Float) -> Unit) { + val onEvent = { node: LayoutNode, event: PointerEvent -> + val newHue = (1f - ((event.mouseY - node.absoluteCoords.y) / node.height).toFloat()).coerceIn(0f, 1f) + onHueChanged(newHue); event.consume() + } + Layout( + name = "HueBar", + measurePolicy = { _, _, constraints -> MeasureResult(constraints.minWidth, constraints.minHeight) {} }, + renderer = object : Renderer { + override fun render(node: UINode, x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) = guiGraphics { + for (j in 0 until node.height) { + fill(x, y + j, x + node.width, y + j + 1, KColor.ofHsv(1f - (j.toFloat() / node.height), 1f, 1f).argb) + } + drawRectOutline(x - 1, y + ((1 - hue) * node.height).toInt() - 1, node.width + 2, 3, KColor.WHITE.argb) + } + }, + modifier = modifier.onPointerEvent(PointerEventType.PRESS, onEvent).onDrag(onDragEvent = onEvent), + ) +} + +@Composable +private fun AlphaBar(modifier: Modifier = Modifier, color: HsvColor, onAlphaChanged: (Float) -> Unit) { + val onEvent = { node: LayoutNode, event: PointerEvent -> + val newAlpha = ((event.mouseX - node.absoluteCoords.x) / node.width).toFloat().coerceIn(0f, 1f) + onAlphaChanged(newAlpha); event.consume() + } + Layout( + name = "AlphaBar", + measurePolicy = { _, _, constraints -> MeasureResult(constraints.minWidth, constraints.minHeight) {} }, + renderer = object : Renderer { + override fun render(node: UINode, x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) = guiGraphics { + val checkerSize = 4 + for (cx in 0 until node.width step checkerSize) + for (cy in 0 until node.height step checkerSize) + fill(x + cx, y + cy, x + cx + checkerSize, y + cy + checkerSize, + if ((cx / checkerSize + cy / checkerSize) % 2 == 0) KColor.WHITE.rgb else KColor.LIGHT_GRAY.rgb) + val opaque = color.copy(alpha = 1f).toKColor().argb + val transparent = color.copy(alpha = 0f).toKColor().argb + fillGradient(x, y, node.width, node.height, transparent, opaque, transparent, opaque) + drawRectOutline(x + (color.alpha * node.width).toInt() - 1, y - 1, 3, node.height + 2, KColor.WHITE.argb) + } + }, + modifier = modifier.onPointerEvent(PointerEventType.PRESS, onEvent).onDrag(onDragEvent = onEvent), + ) +} + +// ── Public API ───────────────────────────────────────────────────────────── + +/** + * A fully controlled HSV + alpha colour picker composable. + * + * This composable is **stateless**: it displays the colour provided by [color] and + * reports changes through [onColorChanged]. The caller is responsible for creating + * and hoisting the state, typically via `remember { mutableStateOf(HsvColor(...)) }`. + * + * The picker consists of a saturation-value gradient area, an optional alpha slider, and + * a vertical hue slider. Apply a `Modifier.size(width, height)` to set the picker's + * overall dimensions. + * + * ### Example + * ```kotlin + * var color by remember { mutableStateOf(HsvColor.from(KColor.RED)) } + * ColorPicker( + * color = color, + * modifier = Modifier.size(200, 150), + * onColorChanged = { color = it }, + * ) + * ``` + * + * @param color The current colour value to display. + * @param showAlphaBar Whether to show the horizontal alpha slider. + * @param alphaBarHeight Height of the alpha slider in pixels. + * @param hueBarWidth Width of the vertical hue slider in pixels. + * @param barPadding Gap in pixels between the main SV area and the sliders. + * @param modifier Modifiers applied to the picker container (size required). + * @param onColorChanged Called with the updated [HsvColor] on every user interaction. + */ +@Composable +fun ColorPicker( + color: HsvColor, + showAlphaBar: Boolean = true, + alphaBarHeight: Int = 12, + hueBarWidth: Int = 16, + barPadding: Int = 8, + modifier: Modifier = Modifier, + onColorChanged: (HsvColor) -> Unit, +) { + val updatedCallback by rememberUpdatedState(onColorChanged) + + val measurePolicy = remember(showAlphaBar, alphaBarHeight, hueBarWidth, barPadding) { + MeasurePolicy { _, measurables, constraints -> + if (showAlphaBar) { + check(measurables.size == 3) { "ColorPicker with showAlphaBar=true expects exactly 3 children" } + val (svM, alphaM, hueM) = measurables + val svW = max(0, constraints.maxWidth - hueBarWidth - barPadding) + val svH = max(0, constraints.maxHeight - alphaBarHeight - barPadding) + val svP = svM.measure(Constraints(svW, svW, svH, svH)) + val alphaP = alphaM.measure(Constraints(svW, svW, alphaBarHeight, alphaBarHeight)) + val hueP = hueM.measure(Constraints(hueBarWidth, hueBarWidth, constraints.maxHeight, constraints.maxHeight)) + MeasureResult(constraints.maxWidth, constraints.maxHeight) { + svP.placeAt(0, 0) + alphaP.placeAt(0, svP.height + barPadding) + hueP.placeAt(svP.width + barPadding, 0) + } + } else { + check(measurables.size == 2) { "ColorPicker with showAlphaBar=false expects exactly 2 children" } + val (svM, hueM) = measurables + val svW = max(0, constraints.maxWidth - hueBarWidth - barPadding) + val svP = svM.measure(Constraints(svW, svW, constraints.maxHeight, constraints.maxHeight)) + val hueP = hueM.measure(Constraints(hueBarWidth, hueBarWidth, constraints.maxHeight, constraints.maxHeight)) + MeasureResult(constraints.maxWidth, constraints.maxHeight) { + svP.placeAt(0, 0) + hueP.placeAt(svP.width + barPadding, 0) + } + } + } + } + + Layout(name = "ColorPicker", measurePolicy = measurePolicy, modifier = modifier) { + SaturationValueArea(hue = color.hue, saturation = color.saturation, value = color.value) { s, v -> + updatedCallback(color.copy(saturation = s, value = v)) + } + if (showAlphaBar) { + AlphaBar(color = color) { a -> updatedCallback(color.copy(alpha = a)) } + } + HueBar(hue = color.hue) { h -> updatedCallback(color.copy(hue = h)) } + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Radio.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Radio.kt new file mode 100644 index 000000000..2ef5cd187 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Radio.kt @@ -0,0 +1,159 @@ +package net.kernelpanicsoft.archie.gui.composables.input + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import net.kernelpanicsoft.archie.gui.composables.basic.Text +import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates +import net.kernelpanicsoft.archie.gui.composables.theme.WidgetState +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.layout.Arrangement +import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy +import net.kernelpanicsoft.archie.gui.layout.Column +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.Renderer +import net.kernelpanicsoft.archie.gui.layout.Row +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.sizeIn +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.theme.LocalTheme +import net.kernelpanicsoft.archie.gui.theme.SimpleThemeState +import net.kernelpanicsoft.archie.gui.theme.ThemeVariants +import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component + +/** + * Low-level unstyled radio-button behavior, built on [Clickable]. + * + * Calls [onSelect] on press only when not already [selected] (clicking an already-selected + * radio option is a no-op, matching standard radio-group semantics). Applies no visuals - + * that is up to [content]. + * + * @param selected Whether this option is currently selected. + * @param onSelect Invoked when this (unselected) option is clicked. + * @param modifier Additional modifiers applied to the outer clickable container. + * @param enabled When `false`, pointer events are ignored. + * @param content The visual content; receives hover/press state and [selected]. + */ +@Composable +fun RadioButtonCore( + selected: Boolean, + onSelect: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + content: @Composable (isHovered: Boolean, isPressed: Boolean, selected: Boolean) -> Unit, +) { + Clickable( + onClick = { if (!selected) onSelect() }, + enabled = enabled, + modifier = modifier, + ) { hovered, pressed -> + content(hovered, pressed, selected) + } +} + +/** + * A standard themed radio button with a filled center dot when [selected]. + * + * Renders the themed [texture] state - a combined selected+hovered state is used when both + * apply and the theme defines it. See [RadioGroup] for a labeled option list. + * + * @param selected Whether this option is currently selected. + * @param onSelect Invoked when this (unselected) option is clicked. + * @param modifier Additional modifiers applied to the outer container. + * @param enabled When `false`, pointer events are ignored and the [TextureStates.DISABLED] state is shown. + * @param texture The themed texture key to look up via [LocalTheme]. + * @param variant The theme variant of [texture] to use. See [ThemeVariants]. + */ +@Composable +fun RadioButton( + selected: Boolean, + onSelect: () -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + texture: String = "radio", + variant: String = ThemeVariants.DEFAULT, +) { + val theme = LocalTheme.current + val composableTheme = theme.getComposableTheme(texture) + val measurePolicy = remember { BoxMeasurePolicy(Alignment.Center) } + val sizeModifier = if (!composableTheme.isNineslice) { + with(composableTheme.states[TextureStates.DEFAULT] as SimpleThemeState) { + Modifier.sizeIn(minWidth = width, minHeight = height) + } + } else Modifier + + RadioButtonCore( + selected = selected, + onSelect = onSelect, + enabled = enabled, + modifier = sizeModifier.then(modifier), + ) { hovered, _, currentSelected -> + Layout( + name = "RadioButton", + measurePolicy = measurePolicy, + modifier = sizeModifier, + renderer = object : Renderer { + override fun render( + node: UINode, + x: Int, + y: Int, + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float, + ) = guiGraphics { + val stateKey = WidgetState.resolve( + composableTheme, variant, + WidgetState.clicked(currentSelected), WidgetState.hovered(hovered), + enabled = enabled, + ) + node.renderState = stateKey + val state = composableTheme.getState(stateKey, variant) + + drawThemeState(state, x, y, node.width, node.height) + } + }, + ) + } +} + +/** A single labeled choice within a [RadioGroup]. */ +data class RadioOption( + val value: T, + val label: Component, + val enabled: Boolean = true, +) + +/** + * A vertical list of labeled, mutually exclusive [RadioButton]s. + * + * @param options The selectable options, in display order. + * @param selected The currently selected value, or `null` if none is selected. + * @param onSelected Called with an option's value when it is selected. + * @param modifier Additional modifiers applied to the outer [Column]. + * @param optionSpacing Vertical spacing between options, in pixels. + */ +@Composable +fun RadioGroup( + options: List>, + selected: T?, + onSelected: (T) -> Unit, + modifier: Modifier = Modifier, + optionSpacing: Int = 3, +) { + Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(optionSpacing)) { + options.forEach { option -> + Row(horizontalArrangement = Arrangement.spacedBy(4), verticalAlignment = Alignment.CenterVertically) { + RadioButton( + selected = option.value == selected, + enabled = option.enabled, + onSelect = { onSelected(option.value) }, + ) + Text(option.label, dropShadow = false) + } + } + } +} + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Slider.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Slider.kt new file mode 100644 index 000000000..149d01735 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Slider.kt @@ -0,0 +1,203 @@ +package net.kernelpanicsoft.archie.gui.composables.input + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.Renderer +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType +import net.kernelpanicsoft.archie.gui.modifiers.input.onDrag +import net.kernelpanicsoft.archie.gui.modifiers.input.onPointerEvent +import net.kernelpanicsoft.archie.gui.modifiers.sizeIn +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.composables.theme.WidgetState +import net.kernelpanicsoft.archie.gui.theme.ComposableTheme +import net.kernelpanicsoft.archie.gui.theme.LocalTheme +import net.kernelpanicsoft.archie.gui.theme.ThemeVariants +import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.minecraft.client.gui.GuiGraphics +import kotlin.math.roundToInt + +private const val SLIDER_MIN_WIDTH = 96 +private const val SLIDER_MIN_HEIGHT = 20 +private const val SLIDER_THUMB_WIDTH = 8 +private const val SLIDER_THUMB_HEIGHT = 20 +private const val SLIDER_TRACK_HEIGHT = 2 + +/** Clamps a slider value into the normalized `0f..1f` range. */ +internal fun normalizeSliderValue(value: Float): Float = value.coerceIn(0f, 1f) + +/** Normalizes [value] then rounds it to the nearest of [steps] evenly spaced increments (no snapping when [steps] <= 0). */ +internal fun snapSliderValue(value: Float, steps: Int): Float { + if (steps <= 0) return normalizeSliderValue(value) + val clamped = normalizeSliderValue(value) + val stepSize = 1f / steps.toFloat() + return (clamped / stepSize).roundToInt() * stepSize +} + +/** Clamps a raw thumb x-position so the [thumbWidth]-wide thumb stays within the track bounds. */ +internal fun resolveSliderThumbX(rawThumbX: Int, sliderX: Int, sliderWidth: Int, thumbWidth: Int = SLIDER_THUMB_WIDTH): Int { + val minThumbX = sliderX + val maxThumbX = (sliderX + sliderWidth - thumbWidth).coerceAtLeast(minThumbX) + return rawThumbX.coerceIn(minThumbX, maxThumbX) +} + +private fun resolveSliderStateName(theme: ComposableTheme, variant: String, enabled: Boolean, hovered: Boolean, dragging: Boolean): String = + WidgetState.resolve(theme, variant, WidgetState.clicked(dragging), WidgetState.hovered(hovered), enabled = enabled) + +/** + * Low-level unstyled slider behavior: drag/click-to-position and hover/drag state tracking, + * with no visuals of its own. + * + * @param value The current value, normalized/snapped via [snapSliderValue]. + * @param onValueChange Called with the new normalized value on every drag/click update. + * @param modifier Additional modifiers applied to the outer container. + * @param enabled When `false`, pointer events are ignored. + * @param steps Number of discrete increments to snap to; `0` means continuous. + * @param onValueChangeFinished Called once when a drag interaction ends (on release). + * @param content The visual content; receives hover/drag state and the + * normalized, snapped value to render. + */ +@Composable +fun SliderCore( + value: Float, + onValueChange: (Float) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + steps: Int = 0, + onValueChangeFinished: () -> Unit = {}, + content: @Composable (isHovered: Boolean, isDragging: Boolean, normalizedValue: Float) -> Unit, +) { + val normalizedValue = snapSliderValue(value, steps) + + var hovered by remember { mutableStateOf(false) } + var dragging by remember { mutableStateOf(false) } + + fun updateFromPointer(node: UINode, mouseX: Double) { + val localX = (mouseX - node.x).toFloat() + val fraction = if (node.width <= 1) 0f else localX / node.width.toFloat() + onValueChange(snapSliderValue(fraction, steps)) + } + + Layout( + name = "SliderCore", + measurePolicy = remember { BoxMeasurePolicy(Alignment.CenterStart) }, + modifier = Modifier + .onPointerEvent(PointerEventType.ENTER) { _, event -> + if (!enabled) return@onPointerEvent + hovered = true + event.consume() + } + .onPointerEvent(PointerEventType.EXIT) { _, event -> + hovered = false + dragging = false + if (enabled) event.consume() + } + .onPointerEvent(PointerEventType.PRESS) { node, event -> + if (!enabled) return@onPointerEvent + dragging = true + updateFromPointer(node, event.mouseX) + event.consume(true) + } + .onDrag { node, event -> + if (!enabled || !dragging) return@onDrag + updateFromPointer(node, event.mouseX) + event.consume() + } + .onPointerEvent(PointerEventType.GLOBAL_RELEASE) { _, _ -> + if (!enabled || !dragging) return@onPointerEvent + dragging = false + onValueChangeFinished() + } + .then(modifier), + ) { + content(hovered, dragging, normalizedValue) + } +} + +/** + * A standard themed horizontal slider, drawing a "slider" track and "slider_handle" thumb + * from the current theme, plus a solid-color fill up to the thumb. + * + * @param value The current value, normalized/snapped via [snapSliderValue]. + * @param onValueChange Called with the new normalized value on every drag/click update. + * @param modifier Additional modifiers applied to the outer container. + * @param enabled When `false`, the disabled state is drawn and input is ignored. + * @param variant The theme variant used for both the track and thumb textures. + * @param steps Number of discrete increments to snap to; `0` means continuous. + * @param onValueChangeFinished Called once when a drag interaction ends (on release). + */ +@Composable +fun Slider( + value: Float, + onValueChange: (Float) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + variant: String = ThemeVariants.DEFAULT, + steps: Int = 0, + onValueChangeFinished: () -> Unit = {}, +) { + val measurePolicy = remember { BoxMeasurePolicy(Alignment.CenterStart) } + val theme = LocalTheme.current + val trackTheme = theme.getComposableTheme("slider") + val thumbTheme = theme.getComposableTheme("slider_handle") + val sizeModifier = Modifier.sizeIn(minWidth = SLIDER_MIN_WIDTH, minHeight = SLIDER_MIN_HEIGHT) + SliderCore( + value = value, + onValueChange = onValueChange, + enabled = enabled, + steps = steps, + onValueChangeFinished = onValueChangeFinished, + modifier = sizeModifier.then(modifier), + ) { hovered, dragging, normalizedValue -> + Layout( + name = "Slider", + measurePolicy = measurePolicy, + modifier = sizeModifier, + renderer = object : Renderer { + override fun render( + node: UINode, + x: Int, + y: Int, + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float, + ) = guiGraphics { + val trackY = y + (node.height - SLIDER_TRACK_HEIGHT) / 2 + val trackStart = x + (SLIDER_THUMB_WIDTH / 2) + val trackEnd = x + node.width - (SLIDER_THUMB_WIDTH / 2) + val availableTrack = (trackEnd - trackStart).coerceAtLeast(1) + val fillEnd = trackStart + (availableTrack * normalizedValue).roundToInt() + val thumbX = resolveSliderThumbX( + rawThumbX = fillEnd - (SLIDER_THUMB_WIDTH / 2), + sliderX = x, + sliderWidth = node.width, + thumbWidth = SLIDER_THUMB_WIDTH, + ) + val thumbY = y + (node.height - SLIDER_THUMB_HEIGHT) / 2 + + val stateName = resolveSliderStateName(trackTheme, variant, enabled, hovered, dragging) + node.renderState = stateName + val trackState = trackTheme.getState(stateName, variant) + val thumbState = thumbTheme.getState(stateName, variant) + + val fillColor = if (enabled) 0xFF6BA8FF.toInt() else 0xFF5A5A5A.toInt() + + drawThemeState(trackState, x, y, node.width, node.height) + fill(trackStart, trackY, fillEnd, trackY + SLIDER_TRACK_HEIGHT, fillColor) + drawThemeState(thumbState, thumbX, thumbY, SLIDER_THUMB_WIDTH, SLIDER_THUMB_HEIGHT) + } + }, + ) + } +} + + + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Switch.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Switch.kt new file mode 100644 index 000000000..1ee370a11 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Switch.kt @@ -0,0 +1,133 @@ +package net.kernelpanicsoft.archie.gui.composables.input + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import net.kernelpanicsoft.archie.gui.animation.AnimationSpec +import net.kernelpanicsoft.archie.gui.animation.Easings +import net.kernelpanicsoft.archie.gui.animation.animateInt +import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates +import net.kernelpanicsoft.archie.gui.composables.theme.WidgetState +import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.Renderer +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.sizeIn +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.theme.LocalTheme +import net.kernelpanicsoft.archie.gui.theme.ThemeVariants +import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.minecraft.client.gui.GuiGraphics +import kotlin.time.Duration.Companion.milliseconds + +private const val SWITCH_MIN_WIDTH = 34 +private const val SWITCH_MIN_HEIGHT = 18 +private const val SWITCH_PADDING = 2 +private const val SWITCH_THUMB_SIZE = 14 + +/** Clamps a raw thumb x-offset so the thumb stays within the track, respecting [SWITCH_PADDING]. */ +internal fun resolveSwitchThumbOffset(thumbOffset: Int, trackWidth: Int): Int { + val minOffset = SWITCH_PADDING + val maxOffset = (trackWidth - SWITCH_THUMB_SIZE - SWITCH_PADDING).coerceAtLeast(minOffset) + return thumbOffset.coerceIn(minOffset, maxOffset) +} + +/** + * Low-level switch primitive exposing hover/press state and checked state to custom visuals. + */ +@Composable +fun SwitchCore( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + content: @Composable (isHovered: Boolean, isPressed: Boolean, checked: Boolean) -> Unit, +) { + Clickable( + onClick = { onCheckedChange(!checked) }, + enabled = enabled, + modifier = modifier, + ) { hovered, pressed -> + content(hovered, pressed, checked) + } +} + +/** + * Simple styled switch control suitable for toggling boolean settings. + * + * Renders the themed [trackTexture] state - a combined checked+hovered state is used when + * both apply and the theme defines it - with the themed [thumbTexture] drawn on top, + * animating between its off/on positions. + * + * @param checked The current checked state. + * @param onCheckedChange Called with the new checked value when the user clicks. + * @param modifier Additional modifiers applied to the outer container. + * @param enabled When `false`, pointer events are ignored and the [TextureStates.DISABLED] state is shown. + * @param trackTexture The themed texture key for the track, looked up via [LocalTheme]. + * @param thumbTexture The themed texture key for the thumb, looked up via [LocalTheme]. + * @param variant The theme variant of both textures to use. See [ThemeVariants]. + */ +@Composable +fun Switch( + checked: Boolean, + onCheckedChange: (Boolean) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + trackTexture: String = "switch_track", + thumbTexture: String = "switch_thumb", + variant: String = ThemeVariants.DEFAULT, +) { + val theme = LocalTheme.current + val trackTheme = theme.getComposableTheme(trackTexture) + val thumbTheme = theme.getComposableTheme(thumbTexture) + val measurePolicy = remember { BoxMeasurePolicy(Alignment.CenterStart) } + val sizeModifier = Modifier.sizeIn(minWidth = SWITCH_MIN_WIDTH, minHeight = SWITCH_MIN_HEIGHT) + val thumbOffset = animateInt( + targetValue = if (checked) SWITCH_MIN_WIDTH - SWITCH_THUMB_SIZE - SWITCH_PADDING else SWITCH_PADDING, + spec = AnimationSpec(durationMillis = 140.milliseconds, easing = Easings.OutCubic), + ) + + SwitchCore( + checked = checked, + onCheckedChange = onCheckedChange, + enabled = enabled, + modifier = sizeModifier.then(modifier), + ) { hovered, _, currentChecked -> + Layout( + name = "Switch", + measurePolicy = measurePolicy, + modifier = sizeModifier, + renderer = object : Renderer { + override fun render( + node: UINode, + x: Int, + y: Int, + guiGraphics: GuiGraphics, + mouseX: Int, + mouseY: Int, + partialTick: Float, + ) = guiGraphics { + val trackStateKey = WidgetState.resolve( + trackTheme, variant, + WidgetState.clicked(currentChecked), WidgetState.hovered(hovered), + enabled = enabled, + ) + node.renderState = trackStateKey + val trackState = trackTheme.getState(trackStateKey, variant) + val thumbState = thumbTheme.getState( + WidgetState.resolve(thumbTheme, variant, enabled = enabled), + variant + ) + + drawThemeState(trackState, x, y, node.width, node.height) + + val thumbX = x + resolveSwitchThumbOffset(thumbOffset, node.width) + val thumbY = y + ((node.height - SWITCH_THUMB_SIZE) / 2) + drawThemeState(thumbState, thumbX, thumbY, SWITCH_THUMB_SIZE, SWITCH_THUMB_SIZE) + } + }, + ) + } +} + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextField.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextField.kt new file mode 100644 index 000000000..0ff04b41d --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextField.kt @@ -0,0 +1,221 @@ +package net.kernelpanicsoft.archie.gui.composables.input.textfield + +import androidx.compose.runtime.* +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.MeasureResult +import net.kernelpanicsoft.archie.gui.layout.Renderer +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.util.KColor +import net.kernelpanicsoft.archie.gui.util.extension.invoke +import net.kernelpanicsoft.archie.gui.util.extension.pose +import net.kernelpanicsoft.archie.gui.util.extension.scissor +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.Font +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.renderer.RenderType +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.Mth +import kotlin.math.max +import kotlin.math.min + +private val TEXT_FIELD_SPRITE = ResourceLocation.withDefaultNamespace("widget/text_field") +private val TEXT_FIELD_HIGHLIGHTED = ResourceLocation.withDefaultNamespace("widget/text_field_highlighted") +private val SCROLLER_SPRITE = ResourceLocation.withDefaultNamespace("widget/scroller") +private const val BORDER_PADDING = 4 +private const val SCROLL_BAR_W = 8 + +/** + * A simple, controlled text field that uses a plain `String` as its state. + * + * This is a convenience wrapper around [TextField] that manages a [TextFieldValue] + * internally, converting to and from `String` for the [onValueChange] callback. + * + * @param value The current text string. + * @param onValueChange Called with the updated string on every edit. + * @param modifier Additional modifiers applied to the text field. + * @param enabled When `false`, input is ignored and the field appears disabled. + * @param readOnly When `true`, text can be selected and copied but not edited. + * @param textColor ARGB colour of the rendered text. + * @param cursorColor ARGB colour of the blinking cursor line. + * @param selectionColor ARGB colour of the text-selection highlight. + * @param font The [Font] used for rendering and measurement. + * @param singleLine When `true` the field occupies a single horizontal line. + * @param maxLength Maximum allowed character count. + * @param maxLines Maximum allowed line count (multi-line only). + */ +@Composable +fun BasicTextField( + value: String, + onValueChange: (String) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textColor: KColor = KColor.ofRgb(0xE0E0E0), + cursorColor: KColor = KColor.ofRgb(0xFFD0D0D0.toInt()), + selectionColor: KColor = KColor.ofRgb(-16776961), + font: Font = Minecraft.getInstance().font, + singleLine: Boolean = true, + maxLength: Int = Int.MAX_VALUE, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, +) { + var tfv by remember(value) { mutableStateOf(TextFieldValue(value)) } + TextField( + value = tfv, + onValueChange = { tfv = it; onValueChange(it.text) }, + modifier = modifier, enabled = enabled, readOnly = readOnly, + textColor = textColor, cursorColor = cursorColor, selectionColor = selectionColor, + font = font, singleLine = singleLine, maxLength = maxLength, maxLines = maxLines, + ) +} + +/** + * A fully controlled text field composable with the default Minecraft widget appearance. + * + * Supports single-line and multi-line modes, cursor navigation, text selection, + * clipboard operations, and an optional scrollbar for multi-line overflow. + * + * Use [BasicTextField] if you only need a simple `String`-based API. Use this composable + * when you need full control over [TextFieldValue] (e.g. selection or IME state). + * + * @param value The current [TextFieldValue]. + * @param onValueChange Called on every edit with the new [TextFieldValue]. + * @param modifier Additional modifiers. + * @param enabled Whether the field accepts input. + * @param readOnly Whether the field permits editing. + * @param textColor Text colour. + * @param cursorColor Cursor colour. + * @param selectionColor Selection highlight colour. + * @param font The [Font] used for rendering. + * @param singleLine Single vs multi-line mode. + * @param maxLength Character cap. + * @param maxLines Line cap (multi-line only). + */ +@Composable +fun TextField( + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + textColor: KColor = KColor.ofRgb(0xE0E0E0), + cursorColor: KColor = KColor.ofRgb(0xFFD0D0D0.toInt()), + selectionColor: KColor = KColor.ofRgb(-16776961), + font: Font = Minecraft.getInstance().font, + singleLine: Boolean = true, + maxLength: Int = Int.MAX_VALUE, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, +) { + TextFieldCore( + value = value, onValueChange = onValueChange, font = font, + modifier = modifier, enabled = enabled, readOnly = readOnly, + singleLine = singleLine, maxLength = maxLength, maxLines = maxLines, + ) { state -> + Layout( + name = "TextField", + measurePolicy = { _, _, constraints -> + val w = constraints.maxWidth + val h = if (singleLine) font.lineHeight + BORDER_PADDING * 2 else constraints.maxHeight + MeasureResult(w, h) {} + }, + renderer = object : Renderer { + override fun render(node: UINode, x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) = guiGraphics { + val (w, h) = state.layoutInfo + if (w <= 0 || h <= 0) return@guiGraphics + + val sprite = if (enabled && state.isFocused) TEXT_FIELD_HIGHLIGHTED else TEXT_FIELD_SPRITE + blitSprite(sprite, x, y, w, h) + + val cw = w - BORDER_PADDING * 2 + val ch = h - BORDER_PADDING * 2 + val cx = x + BORDER_PADDING + val cy = y + BORDER_PADDING + + scissor(cx, cy, cx + cw, cy + ch) { + pose { + translate(cx.toDouble(), cy.toDouble(), 0.0) + + if (singleLine) renderSingleLine( + value, font, state, cw, textColor.argb, + if (state.showCursor && state.isFocused) cursorColor.argb else 0, selectionColor.argb + ) + else + { + translate(0.0, -state.scrollY, 0.0) + renderMultiLine( + value, + font, + value.text.lines(), + if (state.showCursor && state.isFocused) cursorColor.argb else 0, + selectionColor.argb, + textColor.argb + ) + } + } + } + + if (!singleLine) { + val contentH = value.text.lines().size * font.lineHeight + if (contentH > ch) renderScrollBar(x + w - SCROLL_BAR_W, y, h, contentH, state.scrollY) + } + } + }, + ) + } +} + +// ── Rendering helpers ────────────────────────────────────────────────────── + +private fun GuiGraphics.renderSingleLine(value: TextFieldValue, font: Font, state: TextFieldState, width: Int, tc: Int, cc: Int, sc: Int) { + val text = value.text; val sel = value.selection + val visible = font.plainSubstrByWidth(text.substring(state.displayPos), width) + drawString(font, visible, 0, 0, tc) + if (sel.length > 0) { + val s = (sel.min - state.displayPos).coerceAtLeast(0) + val e = (sel.max - state.displayPos).coerceAtLeast(0) + val vp = text.substring(state.displayPos) + val sx = font.width(vp.take(s.coerceAtMost(vp.length))) + val ex = font.width(vp.take(e.coerceAtMost(vp.length))) + fill(RenderType.guiTextHighlight(), sx, -1, ex, font.lineHeight, sc) + } + if (cc != 0 && sel.isCollapsed && sel.start >= state.displayPos) { + val cx = font.width(text.substring(state.displayPos, sel.start)) + fill(cx, -1, cx + 1, font.lineHeight, cc) + } +} + +private fun GuiGraphics.renderMultiLine(value: TextFieldValue, font: Font, lines: List, cc: Int, sc: Int, tc: Int) { + val text = value.text; val sel = value.selection + var y = 0; var charIdx = 0 + for (line in lines) { + drawString(font, line, 0, y, tc) + if (sel.length > 0) { + val ls = charIdx; val le = ls + line.length + if (sel.min <= le && sel.max >= ls) { + val sil = max(sel.min, ls) - ls; val eil = min(sel.max, le) - ls + val sx = font.width(line.take(sil)); val ex = font.width(line.take(eil)) + fill(RenderType.guiTextHighlight(), sx, y, ex, y + font.lineHeight, sc) + } + } + y += font.lineHeight; charIdx += line.length + 1 + } + if (cc != 0 && sel.isCollapsed) { + val before = text.take(sel.start) + val li = before.count { it == '\n' } + val nl = before.lastIndexOf('\n') + val col = sel.start - (if (nl == -1) 0 else nl + 1) + if (li < lines.size) { + val curX = font.width(lines[li].substring(0, col.coerceAtMost(lines[li].length))) + val curY = li * font.lineHeight + fill(curX, curY, curX + 1, curY + font.lineHeight, cc) + } + } +} + +private fun GuiGraphics.renderScrollBar(x: Int, y: Int, nodeH: Int, contentH: Int, scrollY: Double) { + val innerH = nodeH - BORDER_PADDING * 2 + val thumbH = Mth.clamp((innerH * innerH) / contentH, 32, innerH) + val maxScroll = (contentH - innerH).coerceAtLeast(1) + val sby = y + BORDER_PADDING + Mth.clamp((scrollY * (innerH - thumbH)) / maxScroll, 0.0, (innerH - thumbH).toDouble()).toInt() + blitSprite(SCROLLER_SPRITE, x, sby, SCROLL_BAR_W, thumbH) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldCore.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldCore.kt new file mode 100644 index 000000000..93e3138fc --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldCore.kt @@ -0,0 +1,313 @@ +package net.kernelpanicsoft.archie.gui.composables.input.textfield + +import androidx.compose.runtime.* +import kotlinx.coroutines.delay +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.LayoutNode +import net.kernelpanicsoft.archie.gui.layout.MeasureResult +import net.kernelpanicsoft.archie.gui.layout.Size +import net.kernelpanicsoft.archie.gui.modifiers.Constraints +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.input.* +import net.kernelpanicsoft.archie.util.minecraftClient +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.Font +import net.minecraft.client.gui.screens.Screen +import net.minecraft.util.Mth +import net.minecraft.util.StringUtil +import kotlin.math.max + +private const val BORDER_PADDING = 4 +private const val CURSOR_BLINK_INTERVAL_MS = 300L + +/** + * Internal mutable state for a text field, tracking focus, scroll position, cursor blink, + * and layout dimensions. + * + * Obtain an instance via [rememberTextFieldState] and pass it to [TextFieldCore]. + */ +@Stable +class TextFieldState { + /** Horizontal scroll offset for single-line fields (index of the first visible character). */ + var displayPos by mutableStateOf(0) + /** Vertical scroll offset for multi-line fields, in pixels. */ + var scrollY by mutableStateOf(0.0) + /** Whether the field currently holds input focus. */ + var isFocused by mutableStateOf(false) + /** Whether the blinking cursor is currently visible. */ + var showCursor by mutableStateOf(false) + internal var lastBlink by mutableStateOf(0L) + /** Whether the user is dragging the multi-line scroll bar. */ + var isDraggingScrollbar by mutableStateOf(false) + internal var layoutInfo by mutableStateOf(Size(0, 0)) + + /** Updates focus state and resets the cursor blink timer on focus gain. */ + fun onFocusChange(focused: Boolean) { + if (isFocused != focused) { + isFocused = focused + if (focused) { lastBlink = System.currentTimeMillis(); showCursor = true } + else showCursor = false + } + } +} + +/** Creates and remembers a [TextFieldState] instance. */ +@Composable +fun rememberTextFieldState(): TextFieldState = remember { TextFieldState() } + +/** + * Core composable that handles all state, focus, and input logic for a text field while + * delegating visual rendering entirely to [content]. + * + * This is the lowest-level text field building block. Build higher-level components on top + * of it (as [TextField] and [BasicTextField] do) to add visual decorations. + * + * @param value The current [TextFieldValue]. + * @param onValueChange Called whenever the user modifies the text or cursor position. + * @param font The [Font] used for text measurement. + * @param modifier Additional modifiers applied to the invisible layout node. + * @param enabled When `false`, keyboard events are ignored. + * @param readOnly When `true`, the text can be selected and copied but not edited. + * @param singleLine When `true`, Enter inserts a newline; otherwise the field is single-line. + * @param maxLength Maximum permitted character count. + * @param maxLines Maximum permitted line count (only relevant when [singleLine] is `false`). + * @param content The visual content composable; receives the managed [TextFieldState]. + */ +@Composable +fun TextFieldCore( + value: TextFieldValue, + onValueChange: (TextFieldValue) -> Unit, + font: Font, + modifier: Modifier = Modifier, + enabled: Boolean = true, + readOnly: Boolean = false, + singleLine: Boolean = true, + maxLength: Int = Int.MAX_VALUE, + maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, + content: @Composable (state: TextFieldState) -> Unit, +) { + val state = rememberTextFieldState() + + // Cursor blink coroutine + LaunchedEffect(state.isFocused) { + if (state.isFocused) { + while (true) { + val t = System.currentTimeMillis() + if (t - state.lastBlink > CURSOR_BLINK_INTERVAL_MS) { state.showCursor = !state.showCursor; state.lastBlink = t } + delay(50) + } + } else state.showCursor = false + } + + val scrollToCursor = { + val (nodeWidth, nodeHeight) = state.layoutInfo + if (nodeWidth > 0 && nodeHeight > 0) { + if (singleLine) { + val innerWidth = nodeWidth - BORDER_PADDING * 2 + val visible = font.plainSubstrByWidth(value.text.substring(state.displayPos), innerWidth) + val endPos = visible.length + state.displayPos + if (value.selection.start > endPos) state.displayPos = value.selection.start - visible.length + else if (value.selection.start <= state.displayPos) state.displayPos = value.selection.start + state.displayPos = Mth.clamp(state.displayPos, 0, value.text.length) + } else { + val innerHeight = nodeHeight - BORDER_PADDING * 2 + val contentHeight = value.text.lines().size * font.lineHeight + val maxScroll = max(0, contentHeight - innerHeight) + val cursorLine = value.text.take(value.selection.start).count { it == '\n' } + val cursorY = cursorLine * font.lineHeight + if (cursorY < state.scrollY) state.scrollY = cursorY.toDouble() + if (cursorY + font.lineHeight > state.scrollY + innerHeight) state.scrollY = (cursorY + font.lineHeight - innerHeight).toDouble() + state.scrollY = Mth.clamp(state.scrollY, 0.0, maxScroll.toDouble()) + } + } + } + + val onValueChangeAndScroll: (TextFieldValue) -> Unit = { v -> + onValueChange(v); scrollToCursor(); state.showCursor = true; state.lastBlink = System.currentTimeMillis() + } + + Layout( + name = "TextFieldCore", + measurePolicy = { _, measurables, constraints -> + val w = constraints.maxWidth + val h = if (singleLine) font.lineHeight + BORDER_PADDING * 2 else constraints.maxHeight + state.layoutInfo = Size(w, h) + + val fixedConstraints = Constraints(minWidth = w, maxWidth = w, minHeight = h, maxHeight = h) + val placeables = measurables.map { it.measure(fixedConstraints) } + MeasureResult(w, h) { + placeables.forEach { it.placeAt(0, 0) } + } + }, + modifier = modifier + .onKeyEvent { _, event -> + if (!enabled || !state.isFocused) return@onKeyEvent + if (event.keyCode == 256) { state.onFocusChange(false); event.consume(true); return@onKeyEvent } + var handled = true + val result = when { + Screen.isSelectAll(event.keyCode) -> value.copy(selection = TextRange(0, value.text.length)) + Screen.isCopy(event.keyCode) -> { Minecraft.getInstance().keyboardHandler.clipboard = value.selectedText; value } + Screen.isPaste(event.keyCode) && !readOnly -> handlePaste(value, maxLength, maxLines, singleLine) + Screen.isCut(event.keyCode) && !readOnly -> { Minecraft.getInstance().keyboardHandler.clipboard = value.selectedText; deleteSelected(value) } + !singleLine && !readOnly && event.keyCode in listOf(257, 335) -> + if (value.text.lines().size < maxLines) insert(value, "\n") else value + else -> { val after = handleMovementKey(event, value, singleLine, readOnly); if (after == value) handled = false; after } + } + if (result != value) onValueChangeAndScroll(result) + if (handled || minecraftClient.options.keyInventory.matches(event.keyCode, 0)) event.consume(true) + } + .onCharTyped { _, event -> + if (enabled && !readOnly && state.isFocused && StringUtil.isAllowedChatCharacter(event.codePoint)) { + if (value.text.length - value.selection.length < maxLength) { + onValueChangeAndScroll(insert(value, event.codePoint.toString())); event.consume(true) + } + } + } + .onPointerEvent(PointerEventType.PRESS) { node, event -> + if (state.isFocused && !node.isBounded(event.mouseX.toInt(), event.mouseY.toInt())) + state.onFocusChange(false) + } + .onPointerEvent(PointerEventType.PRESS) { node, event -> + val (nX, _) = node.absoluteCoords + val scrollBarX = nX + state.layoutInfo.width - 8 + if (!singleLine && event.mouseX >= scrollBarX && event.mouseX < nX + state.layoutInfo.width) { + state.isDraggingScrollbar = true + } else { + state.onFocusChange(true) + val lX = event.mouseX - node.absoluteCoords.x - BORDER_PADDING + val lY = event.mouseY - node.absoluteCoords.y - BORDER_PADDING + val cur = findCursorPos(font, value.text, lX, lY, state, singleLine) + val sel = if (Screen.hasShiftDown()) TextRange(value.selection.end, cur) else TextRange(cur) + onValueChangeAndScroll(value.copy(selection = sel)) + } + event.consume() + } + .onPointerEvent(PointerEventType.RELEASE) { _, _ -> state.isDraggingScrollbar = false } + .onDrag { node, event -> + if (state.isDraggingScrollbar) { + val contentH = value.text.lines().size * font.lineHeight + val innerH = state.layoutInfo.height - BORDER_PADDING * 2 + val thumbH = Mth.clamp((innerH * innerH) / contentH, 32, innerH) + val maxScroll = (contentH - innerH).coerceAtLeast(1) + state.scrollY = Mth.clamp(state.scrollY + event.dragY * maxScroll.toDouble() / (innerH - thumbH), 0.0, maxScroll.toDouble()) + } else if (state.isFocused) { + val lX = event.mouseX - node.absoluteCoords.x - BORDER_PADDING + val lY = event.mouseY - node.absoluteCoords.y - BORDER_PADDING + val cur = findCursorPos(font, value.text, lX, lY, state, singleLine) + onValueChangeAndScroll(value.copy(selection = TextRange(value.selection.end, cur))) + } + event.consume() + } + .onScroll { _, event -> + if (singleLine && !state.isFocused) return@onScroll + val contentH = value.text.lines().size * font.lineHeight + val innerH = state.layoutInfo.height - BORDER_PADDING * 2 + val maxScroll = (contentH - innerH).coerceAtLeast(0) + state.scrollY = Mth.clamp(state.scrollY - event.scrollY * font.lineHeight / 2.0, 0.0, maxScroll.toDouble()) + event.consume() + }, + ) { content(state) } +} + +// ── Private helpers ──────────────────────────────────────────────────────── + +private fun findCursorPos(font: Font, text: String, x: Double, y: Double, state: TextFieldState, singleLine: Boolean): Int { + return if (singleLine) { + state.displayPos + font.plainSubstrByWidth(text.substring(state.displayPos), x.toInt().coerceAtLeast(0)).length + } else { + val scrolledY = y + state.scrollY + val lineIdx = Mth.floor(scrolledY / font.lineHeight).coerceIn(0, text.lines().size - 1) + val lineText = text.lines()[lineIdx] + val charIdx = font.plainSubstrByWidth(lineText, x.toInt().coerceAtLeast(0)).length + text.split('\n').take(lineIdx).sumOf { it.length + 1 } + charIdx + } +} + +private fun insert(value: TextFieldValue, text: String): TextFieldValue { + val new = value.text.take(value.selection.min) + text + value.text.substring(value.selection.max) + return TextFieldValue(new, TextRange(value.selection.min + text.length)) +} + +private fun deleteSelected(value: TextFieldValue): TextFieldValue { + if (value.selection.length == 0) return value + return TextFieldValue(value.text.take(value.selection.min) + value.text.substring(value.selection.max), TextRange(value.selection.min)) +} + +private fun handlePaste(value: TextFieldValue, maxLength: Int, maxLines: Int, singleLine: Boolean): TextFieldValue { + var clip = Minecraft.getInstance().keyboardHandler.clipboard + val avail = maxLength - (value.text.length - value.selection.length) + if (clip.length > avail) clip = clip.take(avail) + if (!singleLine) { + val currentLines = value.text.lines().size + val linesInSel = value.selectedText.count { it == '\n' } + val availLines = maxLines - (currentLines - linesInSel) + var nl = 0 + clip = buildString { for (c in clip) { if (c == '\n') { nl++; if (nl >= availLines) break }; append(c) } } + } + return insert(value, clip) +} + +private fun handleMovementKey(event: KeyEvent, value: TextFieldValue, singleLine: Boolean, readOnly: Boolean): TextFieldValue { + if (readOnly) return when (event.keyCode) { + 262, 263, 264, 265, 268, 269 -> moveKey(event, value, singleLine) + else -> value + } + return moveKey(event, value, singleLine) +} + +private fun moveKey(event: KeyEvent, value: TextFieldValue, singleLine: Boolean): TextFieldValue { + val shift = Screen.hasShiftDown(); val ctrl = Screen.hasControlDown() + val text = value.text; val sel = value.selection + return when (event.keyCode) { + 259 -> { // BACKSPACE + if (sel.length > 0) deleteSelected(value) + else if (sel.start == 0) value + else { val p = if (ctrl) findLastWord(text, sel.start) else sel.start - 1; TextFieldValue(text.take(p) + text.substring(sel.start), TextRange(p)) } + } + 261 -> { // DELETE + if (sel.length > 0) deleteSelected(value) + else if (sel.start == text.length) value + else { val p = if (ctrl) findNextWord(text, sel.start) else sel.start + 1; TextFieldValue(text.take(sel.start) + text.substring(p), TextRange(sel.start)) } + } + 263 -> { // LEFT + val p = if (ctrl) findLastWord(text, sel.start) else (sel.start - 1).coerceAtLeast(0) + value.copy(selection = if (shift) TextRange(sel.end, p) else TextRange(p)) + } + 262 -> { // RIGHT + val p = if (ctrl) findNextWord(text, sel.start) else (sel.start + 1).coerceAtMost(text.length) + value.copy(selection = if (shift) TextRange(sel.end, p) else TextRange(p)) + } + 265 -> if (!singleLine) moveVertical(value, -1, shift) else value // UP + 264 -> if (!singleLine) moveVertical(value, 1, shift) else value // DOWN + 268 -> { // HOME + val nl = text.take(sel.start).lastIndexOf('\n') + val ls = if (nl == -1) 0 else nl + 1 + value.copy(selection = if (shift) TextRange(sel.end, ls) else TextRange(ls)) + } + 269 -> { // END + val nl = text.indexOf('\n', sel.start) + val le = if (nl == -1) text.length else nl + value.copy(selection = if (shift) TextRange(sel.end, le) else TextRange(le)) + } + else -> value + } +} + +private fun moveVertical(value: TextFieldValue, delta: Int, select: Boolean): TextFieldValue { + val lines = value.text.lines(); val cur = value.selection.start + val curLine = value.text.take(cur).count { it == '\n' } + val target = (curLine + delta).coerceIn(0, lines.lastIndex) + if (curLine == target) return value + val lastNl = value.text.take(cur).lastIndexOf('\n') + val col = cur - (if (lastNl == -1) 0 else lastNl + 1) + val tStart = value.text.split('\n').take(target).sumOf { it.length + 1 } + val newPos = (tStart + col).coerceAtMost(tStart + lines[target].length) + return value.copy(selection = if (select) TextRange(value.selection.end, newPos) else TextRange(newPos)) +} + +private fun findNextWord(text: String, from: Int): Int { + var i = from; while (i < text.length && text[i] == ' ') i++; while (i < text.length && text[i] != ' ') i++; return i +} +private fun findLastWord(text: String, from: Int): Int { + var i = from - 1; while (i >= 0 && text[i] == ' ') i--; while (i >= 0 && text[i] != ' ') i--; return i + 1 +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldValue.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldValue.kt new file mode 100644 index 000000000..c6ea64c64 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldValue.kt @@ -0,0 +1,61 @@ +package net.kernelpanicsoft.archie.gui.composables.input.textfield + +import androidx.compose.runtime.Immutable +import kotlin.math.max +import kotlin.math.min + +/** + * Represents a half-open character range `[start, end)` within a text field's content string. + * + * When [start] == [end] the range is *collapsed* and represents a cursor position rather than + * a selection. [start] and [end] may be in either order; [min] and [max] always give the + * canonical inclusive/exclusive bounds regardless of direction. + * + * @property start The anchor end of the range (inclusive, 0-based character index). + * @property end The active end of the range. Defaults to [start] (collapsed cursor). + */ +@Immutable +data class TextRange(val start: Int, val end: Int = start) { + /** `true` when [start] and [end] are equal (cursor, no selection). */ + val isCollapsed: Boolean get() = start == end + + /** The number of characters in the selected range. */ + val length: Int get() = max(start, end) - min(start, end) + + /** The smaller of [start] and [end] — inclusive start of the selected region. */ + val min: Int get() = minOf(start, end) + + /** The larger of [start] and [end] — exclusive end of the selected region. */ + val max: Int get() = maxOf(start, end) + + companion object { + /** A collapsed [TextRange] positioned at the beginning of the string. */ + val Zero = TextRange(0) + } + + override fun toString(): String = "TextRange(start=$start, end=$end)" +} + +/** + * Immutable value holder for a [net.kernelpanicsoft.archie.gui.composables.input.textfield.TextField] + * or [BasicTextField]. + * + * Contains the full text, the current selection (or cursor position), and an optional IME + * composition range. Pass new instances to `onValueChange` to update the field. + * + * @property text The current text content. + * @property selection The current selection or cursor position within [text]. + * @property composition The active IME composition range, or `null` when no composition is in progress. + */ +@Immutable +data class TextFieldValue( + val text: String = "", + val selection: TextRange = TextRange(text.length), + val composition: TextRange? = null, +) { + /** The characters currently selected by the user (empty string when the selection is collapsed). */ + val selectedText: String get() = text.substring(selection.min, selection.max) + + override fun toString(): String = + "TextFieldValue(text='$text', selection=$selection, composition=$composition)" +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/ConfirmDialog.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/ConfirmDialog.kt new file mode 100644 index 000000000..d942575c1 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/ConfirmDialog.kt @@ -0,0 +1,108 @@ +package net.kernelpanicsoft.archie.gui.composables.modal + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import net.kernelpanicsoft.archie.gui.animation.AnimationSpec +import net.kernelpanicsoft.archie.gui.animation.Easings +import net.kernelpanicsoft.archie.gui.animation.animateInt +import net.kernelpanicsoft.archie.gui.composables.basic.Text +import net.kernelpanicsoft.archie.gui.composables.containers.Surface +import net.kernelpanicsoft.archie.gui.composables.input.Button +import net.kernelpanicsoft.archie.gui.layer.ModalScope +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.layout.Arrangement +import net.kernelpanicsoft.archie.gui.layout.Column +import net.kernelpanicsoft.archie.gui.layout.Row +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.position.margin +import net.kernelpanicsoft.archie.gui.modifiers.position.offset +import net.kernelpanicsoft.archie.gui.modifiers.position.padding +import net.kernelpanicsoft.archie.gui.modifiers.sizeIn +import net.kernelpanicsoft.archie.gui.theme.LocalTheme +import net.minecraft.network.chat.Component +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import kotlin.time.Duration.Companion.milliseconds + +private const val DIALOG_ANIMATION_MS = 180L + +/** + * A generic confirm/cancel modal with custom [content] and a slide/fade dismiss animation. + * + * Unlike [AlertDialog]/[PromptDialog]/[ChoiceDialog], [content] is fully custom rather than a + * fixed message layout. [net.kernelpanicsoft.archie.gui.layer.ModalScope.dismiss] is deferred until the close animation finishes so the modal + * doesn't disappear abruptly. + * + * @param title The dialog's header text. + * @param confirmText Label for the confirm button. + * @param cancelText Label for the cancel button. + * @param onConfirm Called immediately when the confirm button is pressed, before the close + * animation plays. + * @param onCancel Called immediately when the cancel button is pressed, before the close + * animation plays. + * @param content The dialog body, shown above the action row. + */ +@Composable +fun ModalScope.ConfirmDialog( + title: Component = Component.literal("Confirm Dialog"), + confirmText: Component = Component.literal("Confirm"), + cancelText: Component = Component.literal("Cancel"), + onConfirm: () -> Unit = {}, + onCancel: () -> Unit = {}, + content: @Composable () -> Unit +) +{ + val scope = rememberCoroutineScope() + var entered by remember { mutableStateOf(false) } + var closing by remember { mutableStateOf(false) } + LaunchedEffect(Unit) { entered = true } + + fun closeWithAnimation(action: () -> Unit) { + if (closing) return + closing = true + entered = false + action() + scope.launch { + delay(DIALOG_ANIMATION_MS.milliseconds) + dismiss() + } + } + + val offsetY = animateInt( + targetValue = if (entered) 0 else 8, + spec = AnimationSpec(durationMillis = DIALOG_ANIMATION_MS.milliseconds, easing = Easings.OutCubic), + ) + + Surface(modifier = Modifier.padding(4).offset(x = 0, y = offsetY)) { + Column(modifier = Modifier.margin(4)) { + Text( + text = title, + modifier = Modifier.margin(bottom = 4), + color = LocalTheme.current.darkTextColor, + dropShadow = false + ) + content() + Row( + modifier = Modifier.margin(top = 4), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + Button( + onClick = { closeWithAnimation(onConfirm) }, + enabled = !closing, + modifier = Modifier.sizeIn(minWidth = 50, minHeight = 20) + ) { Text(confirmText) } + Button( + onClick = { closeWithAnimation(onCancel) }, + enabled = !closing, + modifier = Modifier.sizeIn(minWidth = 50, minHeight = 20) + ) { Text(cancelText) } + } + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/DialogPrimitives.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/DialogPrimitives.kt new file mode 100644 index 000000000..5c9f56c76 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/DialogPrimitives.kt @@ -0,0 +1,202 @@ +package net.kernelpanicsoft.archie.gui.composables.modal + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import net.kernelpanicsoft.archie.gui.composables.basic.Text +import net.kernelpanicsoft.archie.gui.composables.containers.Surface +import net.kernelpanicsoft.archie.gui.composables.input.Button +import net.kernelpanicsoft.archie.gui.composables.input.textfield.BasicTextField +import net.kernelpanicsoft.archie.gui.layer.ModalScope +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.layout.Arrangement +import net.kernelpanicsoft.archie.gui.layout.Column +import net.kernelpanicsoft.archie.gui.layout.Row +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.position.margin +import net.kernelpanicsoft.archie.gui.modifiers.sizeIn +import net.kernelpanicsoft.archie.gui.modifiers.width +import net.kernelpanicsoft.archie.gui.theme.LocalTheme +import net.minecraft.network.chat.Component + +/** Value-label pair used by [ChoiceDialog]. */ +data class ModalChoice( + val value: T, + val label: Component, + val enabled: Boolean = true, +) + +/** Shared [Surface] layout (title, body, bottom action row) used by all built-in dialog composables. */ +@Composable +private fun ModalDialogScaffold( + title: Component, + modifier: Modifier = Modifier, + body: @Composable () -> Unit, + actions: @Composable () -> Unit, +) { + Surface(modifier = modifier) { + Column(modifier = Modifier.margin(4), verticalArrangement = Arrangement.spacedBy(4)) { + Text( + text = title, + color = LocalTheme.current.darkTextColor, + dropShadow = false, + ) + body() + Row( + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically, + modifier = Modifier.margin(top = 4), + ) { actions() } + } + } +} + +/** + * Simple one-action modal for acknowledgements and warnings. + * + * @param title The dialog's header text. + * @param message The body text explaining the alert. + * @param confirmText Label for the single dismiss button. + * @param onConfirm Called just before the modal dismisses itself. + */ +@Composable +fun ModalScope.AlertDialog( + title: Component, + message: Component, + confirmText: Component = Component.literal("OK"), + onConfirm: () -> Unit = {}, +) { + ModalDialogScaffold( + title = title, + modifier = Modifier.sizeIn(minWidth = 150, minHeight = 60), + body = { + Text(text = message, dropShadow = false, color = LocalTheme.current.darkTextColor) + }, + actions = { + Button(onClick = { + onConfirm() + dismiss() + }) { + Text(confirmText, dropShadow = false) + } + }, + ) +} + +/** + * Input modal with an inline text field and explicit confirm/cancel actions. + * + * @param title The dialog's header text. + * @param initialValue The text field's starting value. + * @param prompt Label text shown above the text field. + * @param confirmText Label for the confirm button. + * @param cancelText Label for the cancel button. + * @param validator The confirm button is only enabled while this returns `true` for the + * current field value. + * @param onConfirm Called with the field's value just before the modal dismisses itself. + * @param onCancel Called just before the modal dismisses itself via the cancel button. + */ +@Composable +fun ModalScope.PromptDialog( + title: Component, + initialValue: String = "", + prompt: Component = Component.literal("Enter a value:"), + confirmText: Component = Component.literal("Confirm"), + cancelText: Component = Component.literal("Cancel"), + validator: (String) -> Boolean = { true }, + onConfirm: (String) -> Unit, + onCancel: () -> Unit = {}, +) { + var value by remember(initialValue) { mutableStateOf(initialValue) } + + ModalDialogScaffold( + title = title, + modifier = Modifier.sizeIn(minWidth = 180, minHeight = 80), + body = { + Column(verticalArrangement = Arrangement.spacedBy(3)) { + Text(text = prompt, dropShadow = false, color = LocalTheme.current.darkTextColor) + BasicTextField( + value = value, + onValueChange = { value = it }, + modifier = Modifier.width(150), + ) + } + }, + actions = { + Button(onClick = { + onCancel() + dismiss() + }) { + Text(cancelText, dropShadow = false) + } + Button( + enabled = validator(value), + onClick = { + onConfirm(value) + dismiss() + }, + ) { + Text(confirmText, dropShadow = false) + } + }, + ) +} + +/** + * Multi-choice modal that maps each option in [choices] to its own button, plus one cancel + * action. + * + * @param title The dialog's header text. + * @param message Optional body text shown above the choice buttons. + * @param choices The selectable options, one button each, in order. + * @param cancelText Label for the cancel button. + * @param onSelected Called with the chosen value just before the modal dismisses itself. + * @param onCancel Called just before the modal dismisses itself via the cancel button. + */ +@Composable +fun ModalScope.ChoiceDialog( + title: Component, + message: Component? = null, + choices: List>, + cancelText: Component = Component.literal("Cancel"), + onSelected: (T) -> Unit, + onCancel: () -> Unit = {}, +) { + ModalDialogScaffold( + title = title, + modifier = Modifier.sizeIn(minWidth = 170, minHeight = 70), + body = { + Column(verticalArrangement = Arrangement.spacedBy(3)) { + if (message != null) { + Text(text = message, dropShadow = false, color = LocalTheme.current.darkTextColor) + } + Column(verticalArrangement = Arrangement.spacedBy(2)) { + choices.forEach { choice -> + Button( + enabled = choice.enabled, + modifier = Modifier.width(150), + onClick = { + onSelected(choice.value) + dismiss() + }, + ) { + Text(choice.label, dropShadow = false) + } + } + } + } + }, + actions = { + Button(onClick = { + onCancel() + dismiss() + }) { + Text(cancelText, dropShadow = false) + } + }, + ) +} + + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/TextureStates.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/TextureStates.kt new file mode 100644 index 000000000..26404864d --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/TextureStates.kt @@ -0,0 +1,26 @@ +package net.kernelpanicsoft.archie.gui.composables.theme +import net.kernelpanicsoft.archie.gui.theme.ComposableTheme +import net.kernelpanicsoft.archie.gui.theme.ThemeState + +/** + * Constant keys used to look up [ThemeState] entries within a [ComposableTheme]'s state map. + * + * Composables use these keys to select the correct texture variant based on their current + * interactive state (e.g. hovered, pressed, disabled). + */ +object TextureStates { + /** The default idle state used when no other state applies. */ + const val DEFAULT = "default" + + /** Used when the composable is disabled and cannot be interacted with. */ + const val DISABLED = "disabled" + + /** Used when the mouse cursor is hovering over the composable. */ + const val HOVERED = "hovered" + + /** Used when the composable has been activated/checked/clicked (toggle state). */ + const val CLICKED = "clicked" + + /** Used when the composable is both activated and hovered simultaneously. */ + const val CLICKED_AND_HOVERED = "clicked_and_hovered" +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/WidgetState.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/WidgetState.kt new file mode 100644 index 000000000..962570da7 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/WidgetState.kt @@ -0,0 +1,84 @@ +package net.kernelpanicsoft.archie.gui.composables.theme + +import net.kernelpanicsoft.archie.gui.theme.ComposableTheme + +/** + * Resolves a stateful composable's [TextureStates] key from an ordered set of independent + * boolean state axes (hovered, checked, pressed, ...), replacing the hand-written `when` chain + * every stateful composable (`Checkbox`, `Radio`, `Switch`, `Button`, `Slider`, `Tab`) used to + * maintain separately - which had drifted out of sync with each other (e.g. `Tab`'s combined + * hover state only fired via its `selected` axis, not its `pressed` axis, unlike every other + * component - see [resolve]'s "Tab" note). + * + * ### Example + * ```kotlin + * val stateKey = WidgetState.resolve( + * composableTheme, variant, + * WidgetState.clicked(checked), WidgetState.hovered(hovered), + * enabled = enabled, + * ) + * node.renderState = stateKey + * guiGraphics.drawThemeState(composableTheme.getState(stateKey, variant), x, y, node.width, node.height) + * ``` + */ +object WidgetState { + /** + * One named boolean axis of a widget's interaction state (e.g. "hovered" paired with + * whether the pointer currently is), in the priority order [resolve] should consider it - + * pass axes to [resolve] most-significant first (typically the "activated" axis - checked/ + * selected/pressed - before "hovered"). + */ + data class Axis(val name: String, val active: Boolean) + + /** An [Axis] for [TextureStates.HOVERED]. */ + fun hovered(active: Boolean) = Axis(TextureStates.HOVERED, active) + + /** An [Axis] for [TextureStates.CLICKED] - a checkbox/switch/radio's checked-or-selected state, a button/tab's pressed-or-selected state, or a slider's dragging state. */ + fun clicked(active: Boolean) = Axis(TextureStates.CLICKED, active) + + /** + * Resolves the [TextureStates] key for [theme]/[variant] given [enabled] and [axes] (in + * descending priority order - see [Axis]). + * + * - If [enabled] is `false`, returns [TextureStates.DISABLED] if [theme] defines it for + * [variant] (via [ComposableTheme.hasState]), otherwise falls through as if disabled + * weren't a factor - matching every existing chain's behavior of only branching on + * `!enabled` where a `disabled` theme state actually exists to show. + * - Otherwise, tries the most specific composite key first: every currently-active axis's + * [Axis.name], joined by `"_and_"` in priority order (e.g. `"clicked_and_hovered"` for + * [clicked]+[hovered] both active). If [theme] doesn't define that combination, falls + * back one axis at a time - by priority, i.e. trying each individual active axis's own + * key alone, highest priority first - stopping at the first one [theme] defines. + * - Returns [TextureStates.DEFAULT] if no active axis (alone or combined) has a defined + * state, or if no axis is active at all. + * + * This graceful per-axis fallback (rather than jumping straight from the full composite to + * [TextureStates.DEFAULT]) generalizes what `Button`'s chain alone used to do by hand + * (falling through a missing "clicked" state to "hovered" - `button.json` defines no + * "clicked" state at all) - every caller gets it for free, without needing its own + * `hasState` check. + * + * **Tab note:** `TabContainer.kt`'s old chain computed its "clicked" axis from + * `selected || isPressed`, but only paired it with `hovered` into the combined state when + * specifically `selected` was true - a pressed-but-unselected-and-hovered tab silently lost + * its hover visual. Callers migrating to this resolver should pass a single `clicked` axis + * (`selected || isPressed`) and a separate `hovered` axis as normal; [resolve] then treats + * both uniformly like every other component, which is a deliberate behavior fix, not an + * incidental one. + */ + fun resolve(theme: ComposableTheme, variant: String, vararg axes: Axis, enabled: Boolean = true): String { + if (!enabled && theme.hasState(TextureStates.DISABLED, variant)) return TextureStates.DISABLED + + val active = axes.filter { it.active } + if (active.isEmpty()) return TextureStates.DEFAULT + + val compositeKey = active.joinToString("_and_") { it.name } + if (theme.hasState(compositeKey, variant)) return compositeKey + + active.forEach { axis -> + if (theme.hasState(axis.name, variant)) return axis.name + } + + return TextureStates.DEFAULT + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemContainerMenu.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemContainerMenu.kt new file mode 100644 index 000000000..087e5aee9 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemContainerMenu.kt @@ -0,0 +1,147 @@ +package net.kernelpanicsoft.archie.gui.item + +import kotlinx.serialization.KSerializer +import net.kernelpanicsoft.archie.gui.ComposeContainerMenuBase +import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacket +import net.kernelpanicsoft.archie.gui.blockentity.toSerializedValue +import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel +import net.kernelpanicsoft.archie.serialization.NBTHolder +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.entity.player.Inventory +import net.minecraft.world.entity.player.Player +import net.minecraft.world.inventory.MenuType + +/** + * Base class for [net.minecraft.world.item.ItemStack]-backed Compose container menus - e.g. a + * backpack/bag with its own GUI. The [ComposeBlockContainerMenu][net.kernelpanicsoft.archie.gui.ComposeBlockContainerMenu] + * equivalent for items. + * + * See [ComposeContainerMenuBase] for slot pre-registration/positioning behavior, shared with + * [net.kernelpanicsoft.archie.gui.ComposeBlockContainerMenu] - this class only adds the + * item-specific pieces: locating the backing stack via [itemAccess], the [itemState] sync path, + * and periodic validity checking via [ItemStateManager]. + * + * ### Subclassing + * ```kotlin + * class MyBackpackMenu(id: Int, inventory: Inventory, access: ItemContainerAccess) : + * ComposeItemContainerMenu(MY_MENU_TYPE, id, inventory, access) { + * + * @Sync + * var progress by holder.intField() + * + * override fun registerSlotHandlers() { + * handler("inventory", holder.itemField(27)) + * } + * } + * ``` + * + * @param SELF The concrete menu subclass (self-referential for the [MenuType]). + * @param type The registered [MenuType] for this menu. + * @param id The container id assigned by the server. + * @param playerInventory The opening player's inventory. + * @param itemAccess Locates the backing [net.minecraft.world.item.ItemStack] and reports whether + * this menu should stay open. + */ +abstract class ComposeItemContainerMenu>( + type: MenuType, + id: Int, + playerInventory: Inventory, + protected val itemAccess: ItemContainerAccess, +) : ComposeContainerMenuBase(type, id, playerInventory), SyncedItemHolder { + + /** Client- and server-side sync state for this menu's own `@Sync`-annotated [holder] fields. */ + val itemState: ComposeItemState = ComposeItemState(containerId) + + /** + * An [NBTHolder] view of the backing stack, captured **once** at construction - mirroring + * [net.kernelpanicsoft.archie.gui.ComposeBlockContainerMenu]'s `tile` (stable for this menu's + * lifetime, not re-resolved per access). Declare this menu's own `@Sync`-annotated scalar + * fields against it, e.g. `@Sync var progress by holder.intField()`. + * + * Unlike [itemAccess]'s own `getStack()` (re-resolved fresh every call, since slot *contents* + * must always reflect the live inventory slot), this menu's own bookkeeping fields behave the + * same way [net.kernelpanicsoft.archie.gui.ComposeBlockContainerMenu]'s fields do against + * `tile` - captured once, not defended against the backing stack reference being swapped out + * from under an already-open menu. That's an unusual scenario that isn't defended against for + * block-entity-backed menus either (`tile` is captured the same way there). + */ + protected val holder: NBTHolder = NBTHolder.item(itemAccess.getStack()) + + /** Property names changed since the last [tickSync], with their serialized values ready to send. */ + private val dirtyUpdates = mutableMapOf() + + init + { + // Must run here, in this class's own init - not from ComposeContainerMenuBase's, which + // would dispatch into onMenuOpened() before `itemAccess` (this class's own constructor + // property) is actually assigned. See ComposeContainerMenuBase.onMenuOpened's KDoc. + onMenuOpened() + } + + override fun onMenuOpened() + { + if (!level.isClientSide) + ItemStateManager.register(this) + } + + override fun onMenuClosed(player: Player) + { + if (!level.isClientSide) + ItemStateManager.unregister(this) + } + + @Suppress("UNCHECKED_CAST") + override fun registerSyncedProperty(name: String, serializer: KSerializer) + { + // Runs on both sides, at field-declaration time - independent of observeItemProperty(), + // which only ever runs client-side inside a composable. Without this, the server never + // learns a serializer for `name` at all unless a fresh delegate's own initial-value write + // happens to fire onSyncedPropertyChanged first (which it doesn't for a property whose + // value already exists on an already-populated stack). + itemState.propertySerializers[name] = serializer as KSerializer + } + + override fun onSyncedPropertyChanged(name: String, serializer: KSerializer, value: T) + { + dirtyUpdates[name] = value.toSerializedValue(serializer) + } + + /** + * Applies a client-sent [ItemUpdatePacket] edit: a raw, low-level write straight into + * [holder]'s stored data (bypassing whatever property setter owns [name], the same way + * [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacketRegistry]'s serverbound + * handler calls `blockEntity.updateProperty(...)` rather than going through the property + * setter) - re-entering through [onSyncedPropertyChanged] here would only mark [name] dirty + * without ever actually persisting the new value, since that method is a notification hook, + * not a write path. Marks [name] dirty directly afterward so [tickSync] re-broadcasts it. + */ + internal fun applyRemoteUpdate(name: String, serializer: KSerializer, value: T) + { + holder.updateProperty(name, serializer, value) + dirtyUpdates[name] = value.toSerializedValue(serializer) + } + + /** + * Called once per server tick by [ItemStateManager]: force-closes this menu if [itemAccess] + * reports it's no longer valid, otherwise sends any accumulated [dirtyUpdates] as a single + * [ItemStatePacket]. + */ + internal fun tickSync(currentTick: Long) + { + if (!itemAccess.stillValid(player)) + { + player.closeContainer() + return + } + if (dirtyUpdates.isEmpty()) return + val packet = ItemStatePacket(containerId, dirtyUpdates.toMap(), currentTick) + dirtyUpdates.clear() + (player as? ServerPlayer)?.let { ArchieNetworkChannel.toPlayers(listOf(it), packet) } + } + + override fun stillValid(player: Player): Boolean = itemAccess.stillValid(player) + + /** Freezes the backpack's own slot in the player's inventory while its GUI is open - see [ComposeContainerMenuBase.isPlayerSlotExcluded]. */ + override fun isPlayerSlotExcluded(index: Int): Boolean = + (itemAccess as? PlayerInventoryItemAccess)?.slot == index +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemState.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemState.kt new file mode 100644 index 000000000..1153b9514 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemState.kt @@ -0,0 +1,145 @@ +package net.kernelpanicsoft.archie.gui.item + +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.mutableStateOf +import kotlinx.serialization.KSerializer +import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacket +import net.kernelpanicsoft.archie.gui.blockentity.deserialize +import net.kernelpanicsoft.archie.gui.blockentity.toSerializedValue +import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel + +/** + * Client- and server-side state holder for a [ComposeItemContainerMenu]'s synchronized + * properties. The [ComposeBlockEntityState][net.kernelpanicsoft.archie.gui.blockentity.ComposeBlockEntityState] + * equivalent for item-backed menus, keyed by [containerId] instead of a [net.minecraft.core.BlockPos]. + * + * Unlike block entities - which can be watched by multiple players simultaneously, needing a + * position-keyed global registry on both sides - an item-backed menu is inherently 1:1 with a + * single player's currently-open session. Client and server each already have exactly one live + * instance of "my currently open menu" (this one, owned directly by the [ComposeItemContainerMenu] + * itself), so no equivalent client-side registry is needed here. + * + * @param containerId The owning menu's vanilla [net.minecraft.world.inventory.AbstractContainerMenu.containerId] - + * used purely as a staleness guard against a stray packet arriving after this player closed one + * item menu and opened another, not as a lookup key. + */ +class ComposeItemState( + val containerId: Int, +) { + /** Map of property names to their Compose state values */ + val propertyStates = mutableMapOf>() + + /** Serializers used to encode/decode each observed property, keyed by property name. */ + val propertySerializers = mutableMapOf>() + + @Suppress("UNCHECKED_CAST") + private fun anySerializer(serializer: KSerializer): KSerializer = serializer as KSerializer + + @Suppress("UNCHECKED_CAST") + private fun typedSerializer(propertyName: String): KSerializer? = propertySerializers[propertyName] as? KSerializer + + @Suppress("UNCHECKED_CAST") + private fun getOrCreateState(propertyName: String, initialValue: T?): MutableState { + return propertyStates.computeIfAbsent(propertyName) { + PropertyState(this, propertyName, mutableStateOf(initialValue)) as MutableState + } as MutableState + } + + /** + * Gets or creates a Compose state for a property with a specific type. + * + * @param propertyName The name of the property. + * @param initialValue The initial value (optional, defaults to null). + * @param T The expected type of the property. + * @return A [MutableState] of type T that can be observed in composables. + */ + fun observeProperty( + propertyName: String, + serializer: KSerializer, + initialValue: T? = null, + ): MutableState { + propertySerializers[propertyName] = anySerializer(serializer) + return getOrCreateState(propertyName, initialValue) + } + + /** + * A [MutableState] delegate that forwards writes to [ComposeItemState.sendUpdatedProperty], + * so setting [value] from a composable both updates local state and pushes the change to the server. + */ + class PropertyState(private val state: ComposeItemState, private val propertyName: String, internal val mutableState: MutableState) : MutableState by mutableState + { + override var value: T + get() = mutableState.value + set(value) + { + mutableState.value = value + state.sendUpdatedProperty(propertyName, value) + } + } + + /** + * Updates a property value from a network packet. + * + * If the property doesn't exist yet, it will be created. + * + * @param propertyName The name of the property. + * @param value The new serialized value from the network packet. + */ + fun updateProperty(propertyName: String, value: BlockEntityStatePacket.SerializedValue) { + val deserializedValue = value.deserialize(propertySerializers[propertyName]) + // Must go through getOrCreateState(), not a separate computeIfAbsent - otherwise a + // property whose first appearance is a packet (not observeProperty()) gets stuck with a + // bare state that never forwards writes back to the server. + getOrCreateState(propertyName, deserializedValue).value = deserializedValue + } + + /** + * Updates a property value and sends the change to the server. + * + * This method should be called when a client-side interaction changes a property. + * + * @param propertyName The name of the property. + * @param value The new value. + */ + fun sendUpdatedProperty(propertyName: String, value: T) { + val serializer = typedSerializer(propertyName) ?: run { + println("No serializer found for property $propertyName. Cannot send update to server.") + return + } + + val serializedValue = value.toSerializedValue(serializer) + val packet = ItemUpdatePacket.singleUpdate(containerId, propertyName, serializedValue) + ArchieNetworkChannel.toServer(packet) + } + + /** + * Gets the current value of a property. + * + * @param propertyName The name of the property. + * @return The property value, or null if not tracked. + */ + fun getProperty(propertyName: String): Any? { + return propertyStates[propertyName]?.value + } + + /** + * Gets the current value of a property with type casting. + * + * @param propertyName The name of the property. + * @param T The expected type. + * @return The property value cast to T, or null if not found/wrong type. + */ + @Suppress("UNCHECKED_CAST") + fun getPropertyTyped(propertyName: String): T? { + return propertyStates[propertyName]?.value as? T + } + + /** + * Gets all currently tracked properties. + * + * @return A map of property names to their current values. + */ + fun getAllProperties(): Map { + return propertyStates.mapValues { (_, state) -> state.value } + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemContainerAccess.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemContainerAccess.kt new file mode 100644 index 000000000..ce38c1bb8 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemContainerAccess.kt @@ -0,0 +1,43 @@ +package net.kernelpanicsoft.archie.gui.item + +import net.minecraft.world.entity.player.Player +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack + +/** + * Locates the [ItemStack] backing a [ComposeItemContainerMenu] and reports whether it's still + * valid to keep the menu open. + */ +interface ItemContainerAccess +{ + /** + * Resolves the current backing [ItemStack]. Must be re-resolved fresh on every call, not + * cached - the underlying stack reference can be swapped out from under the menu (e.g. by + * another mod replacing the inventory slot's stack wholesale), and a cached reference would + * silently go stale rather than reflect that. + */ + fun getStack(): ItemStack + + /** Whether [player] should still be allowed to keep this menu open. */ + fun stillValid(player: Player): Boolean +} + +/** + * An [ItemContainerAccess] for an item sitting in [player]'s own inventory at [slot] (vanilla + * [net.minecraft.world.entity.player.Inventory] numbering: hotbar 0-8, main 9-35). + * + * @param expectedItem Guards [stillValid] against the slot's contents having been swapped out + * for a different item entirely (e.g. dropped and something else picked up into the same + * slot index) while the menu was open. + */ +class PlayerInventoryItemAccess( + private val player: Player, + val slot: Int, + private val expectedItem: Item, +) : ItemContainerAccess +{ + override fun getStack(): ItemStack = player.inventory.getItem(slot) + + override fun stillValid(player: Player): Boolean = + player === this.player && getStack().`is`(expectedItem) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateComposables.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateComposables.kt new file mode 100644 index 000000000..f35ea05ef --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateComposables.kt @@ -0,0 +1,49 @@ +package net.kernelpanicsoft.archie.gui.item + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.MutableState +import androidx.compose.runtime.compositionLocalOf +import kotlinx.serialization.serializer + +/** + * Provides the current [ComposeItemContainerMenu]'s state to composables in the composition + * tree - the [ComposeItemState] equivalent of + * [net.kernelpanicsoft.archie.gui.blockentity.LocalBlockEntityState]. `null` when the current + * screen's menu isn't item-backed. + * + * Use with `LocalItemState.current` to access the state, or use the [observeItemProperty] helper + * for convenience. + */ +val LocalItemState = compositionLocalOf { null } + +/** + * Observes a property on the current [ComposeItemContainerMenu] in the current composition + * context. The [net.kernelpanicsoft.archie.gui.blockentity.observeProperty] equivalent for + * item-backed menus. + * + * Returns a [MutableState] that automatically triggers recomposition when the property changes. + * Must be called where [LocalItemState] has been provided with a non-null value (i.e. inside an + * item-backed menu's screen composition) - otherwise it throws. + * + * ### Example + * ```kotlin + * @Composable + * fun MyComponent() { + * val progressState = observeItemProperty("progress") + * Text("Progress: ${progressState.value}") + * } + * ``` + * + * @param propertyName The name of the property to observe. + * @param T The expected type of the property. + * @return A [MutableState] of type T reflecting the property's current value. + * @throws RuntimeException if no [ComposeItemState] is available in the current composition. + */ +@Composable +inline fun observeItemProperty( + propertyName: String, + initialValue: T? = null, +): MutableState { + val state = LocalItemState.current ?: throw RuntimeException("No item container state available in composition") + return state.observeProperty(propertyName, serializer(), initialValue) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateManager.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateManager.kt new file mode 100644 index 000000000..072da51c7 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateManager.kt @@ -0,0 +1,53 @@ +package net.kernelpanicsoft.archie.gui.item + +import dev.architectury.event.events.common.TickEvent +import org.slf4j.LoggerFactory +import java.util.concurrent.CopyOnWriteArraySet + +private val LOGGER = LoggerFactory.getLogger(ItemStateManager::class.java) + +/** + * Server-side manager for currently-open [ComposeItemContainerMenu]s: drives dirty-property sync + * packets each tick (mirroring [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager]), + * and force-closes a menu whose [ItemContainerAccess] reports it's no longer valid - e.g. the + * backing item was consumed/dropped while the GUI was passively open. Vanilla's own `stillValid` + * polling only fires reactively on player-initiated clicks otherwise, so without this a stale + * menu could sit open indefinitely against a stack that no longer exists. + * + * Much lighter than [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager]: an + * item-backed menu is inherently 1:1 with one player's session (unlike a block entity, which can + * be watched by multiple players simultaneously), so there's no position-keyed registry or + * per-menu tracked-player set needed - just the set of currently-open menus themselves. + */ +object ItemStateManager { + private val openMenus: MutableSet> = CopyOnWriteArraySet() + + /** + * Registers the server tick listener that drives per-tick syncing/validity checks. + * + * Must be called once during mod init. + */ + fun init() { + TickEvent.SERVER_POST.register { + val currentTick = it.tickCount.toLong() + // One menu's tickSync() throwing shouldn't abort the loop for every other open menu. + openMenus.forEach { menu -> + try { + menu.tickSync(currentTick) + } catch (e: Exception) { + LOGGER.error("Error syncing item container menu $menu", e) + } + } + } + } + + /** Registers [menu] for tick-driven syncing. Called from [ComposeItemContainerMenu.onMenuOpened]. */ + fun register(menu: ComposeItemContainerMenu<*>) { + openMenus += menu + } + + /** Unregisters [menu]. Called from [ComposeItemContainerMenu.onMenuClosed]. */ + fun unregister(menu: ComposeItemContainerMenu<*>) { + openMenus -= menu + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacket.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacket.kt new file mode 100644 index 000000000..dd8a2eaea --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacket.kt @@ -0,0 +1,38 @@ +package net.kernelpanicsoft.archie.gui.item + +import kotlinx.serialization.Serializable +import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacket + +/** + * A network packet that carries [ComposeItemContainerMenu] state changes from server to client - + * the item-backed-menu equivalent of [BlockEntityStatePacket]. [containerId] addresses the + * player's currently open menu directly rather than acting as a lookup key: an item-backed menu + * is inherently 1:1 with one player's session, so there's no position-keyed registry to look + * anything up in, unlike a block entity that can be watched by multiple players at once. It's + * checked purely as a staleness guard against a stray packet arriving after this player closed + * one item menu and opened another. + * + * @property containerId The owning menu's vanilla [net.minecraft.world.inventory.AbstractContainerMenu.containerId]. + * @property updates A map of property names to their serialized values. + * @property timestamp Server tick when this packet was created (for ordering/deduplication). + */ +@Serializable +data class ItemStatePacket( + val containerId: Int, + val updates: Map = emptyMap(), + val timestamp: Long = 0, +) { + companion object { + /** Creates a new packet with a single property update. */ + fun singleUpdate( + containerId: Int, + propertyName: String, + value: BlockEntityStatePacket.SerializedValue, + timestamp: Long = 0, + ): ItemStatePacket = ItemStatePacket( + containerId = containerId, + updates = mapOf(propertyName to value), + timestamp = timestamp, + ) + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacketRegistry.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacketRegistry.kt new file mode 100644 index 000000000..0145956d8 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacketRegistry.kt @@ -0,0 +1,42 @@ +package net.kernelpanicsoft.archie.gui.item + +import kotlinx.serialization.KSerializer +import net.kernelpanicsoft.archie.gui.blockentity.deserialize +import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel + +/** + * Registers [ItemStatePacket]/[ItemUpdatePacket] handlers with [ArchieNetworkChannel] - the + * [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacketRegistry] equivalent for + * item-backed menus. + * + * Unlike the block-entity path, routing needs no position-keyed lookup on either side - an + * item-backed menu is inherently 1:1 with one player's session, so "the current menu" is always + * just `context.player.containerMenu` (true on both sides: [net.minecraft.world.entity.player.Player] + * has exactly one open [net.minecraft.world.inventory.AbstractContainerMenu] at a time). The + * `containerId` on each packet is checked purely as a staleness guard against a stray packet + * arriving after the player closed one item menu and opened another - a mismatch is silently + * dropped, not an error. + */ +object ItemStatePacketRegistry { + /** Registers the clientbound and serverbound packet handlers described above. */ + fun register() { + ArchieNetworkChannel.clientbound { packet, context -> + val menu = context.player.containerMenu as? ComposeItemContainerMenu<*> ?: return@clientbound + if (menu.containerId != packet.containerId) return@clientbound + packet.updates.forEach { (propertyName, value) -> + menu.itemState.updateProperty(propertyName, value) + } + } + + ArchieNetworkChannel.serverbound { packet, context -> + val menu = context.player.containerMenu as? ComposeItemContainerMenu<*> ?: return@serverbound + if (menu.containerId != packet.containerId) return@serverbound + packet.updates.forEach { (propertyName, serializedValue) -> + val serializer = menu.itemState.propertySerializers[propertyName] ?: return@forEach + val deserializedValue = serializedValue.deserialize(serializer) + @Suppress("UNCHECKED_CAST") + menu.applyRemoteUpdate(propertyName, serializer as KSerializer, deserializedValue) + } + } + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemUpdatePacket.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemUpdatePacket.kt new file mode 100644 index 000000000..ca00f99b3 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemUpdatePacket.kt @@ -0,0 +1,30 @@ +package net.kernelpanicsoft.archie.gui.item + +import kotlinx.serialization.Serializable +import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacket + +/** + * A network packet that carries [ComposeItemContainerMenu] state updates from client to server - + * the item-backed-menu equivalent of [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityUpdatePacket]. + * See [ItemStatePacket] for what [containerId] is used for. + * + * @property containerId The owning menu's vanilla [net.minecraft.world.inventory.AbstractContainerMenu.containerId]. + * @property updates A map of property names to their serialized values. + */ +@Serializable +data class ItemUpdatePacket( + val containerId: Int, + val updates: Map, +) { + companion object { + /** Creates a new packet with a single property update. */ + fun singleUpdate( + containerId: Int, + propertyName: String, + value: BlockEntityStatePacket.SerializedValue, + ): ItemUpdatePacket = ItemUpdatePacket( + containerId = containerId, + updates = mapOf(propertyName to value), + ) + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/SyncedItemHolder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/SyncedItemHolder.kt new file mode 100644 index 000000000..e89d4a89b --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/SyncedItemHolder.kt @@ -0,0 +1,33 @@ +package net.kernelpanicsoft.archie.gui.item + +import kotlinx.serialization.KSerializer + +/** + * Implemented by whatever owns an [net.kernelpanicsoft.archie.serialization.NBTHolder.item]-backed + * holder that wants its `@Sync`-annotated fields to actually push updates somewhere, mirroring + * what [net.minecraft.world.level.block.entity.BlockEntity] gets automatically via + * [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager]. + * + * [net.kernelpanicsoft.archie.serialization.ItemStackNBTHolderImpl] checks for this on its + * `thisRef` the same way it checks `thisRef is BlockEntity` for the block-entity-backed + * implementation - so any `NBTHolder.item(stack)`-delegated property declared directly on a type + * implementing this interface gets [registerSyncedProperty]/[onSyncedPropertyChanged] calls + * automatically. + */ +interface SyncedItemHolder +{ + /** + * Called once, at property-declaration time, for every `@Sync`-annotated `NBTHolder.item`- + * delegated property named [name] - independent of whether its value has ever actually been + * written. Needed so a serializer is available to decode an incoming edit even for a property + * whose value came from an *existing* stack's already-populated data (where the delegate's own + * initial-value write, which [onSyncedPropertyChanged] would otherwise piggyback on, never + * runs) - mirrors why [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateContainer] + * has its own separate `setPropertySerializer` call distinct from `updateProperty`. No-op by + * default for implementations that don't need it. + */ + fun registerSyncedProperty(name: String, serializer: KSerializer) {} + + /** Called on every write to a `@Sync`-annotated `NBTHolder.item`-delegated property named [name]. */ + fun onSyncedPropertyChanged(name: String, serializer: KSerializer, value: T) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/Layer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/Layer.kt new file mode 100644 index 000000000..e2e031c30 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/Layer.kt @@ -0,0 +1,53 @@ +package net.kernelpanicsoft.archie.gui.layer + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Composition +import androidx.compose.runtime.CompositionContext +import net.kernelpanicsoft.archie.gui.layout.LayoutNode +import net.kernelpanicsoft.archie.gui.nodes.LayoutNodeApplier +import java.util.* + +/** + * A self-contained UI layer with its own independent [LayoutNode] tree and [Composition]. + * + * Layers are used to implement overlapping UI surfaces such as modals, dropdowns, and + * tooltips. Each layer has its own root node that is measured and rendered separately + * from the base screen content. + * + * Layers are managed by [LayerStackManager]. Do not create or dispose [Layer] instances + * directly; use [LayerStackManager.push] or [LayerStackManager.modal] instead. + * + * @property id Unique identifier for this layer, used for removal. + * @property rootNode The root [LayoutNode] of this layer's composition tree. + * @property composition The Compose [Composition] backing this layer. + */ +class Layer( + val id: UUID = UUID.randomUUID(), + depth: Int, + parentComposition: CompositionContext, + content: @Composable () -> Unit, +) { + val rootNode = LayoutNode("Root").apply { layer = depth } + + /** The `"RootContainer"` node under [rootNode], if one has been composed. */ + val rootContainerNode by lazy { rootNode.findNode("RootContainer") } + + /** Finds a descendant of [rootNode] by name. See [LayoutNode.findNode]. */ + fun findNode(name: String): LayoutNode? = rootNode.findNode(name) + + + + val composition = Composition(LayoutNodeApplier(rootNode), parentComposition) + + init { + composition.setContent(content) + } + + /** + * Disposes the Compose [Composition] associated with this layer, releasing all + * remembered state and coroutines. + * + * Called automatically by [LayerStackManager.pop] and [LayerStackManager.popById]. + */ + fun dispose() = composition.dispose() +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/LayerStackManager.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/LayerStackManager.kt new file mode 100644 index 000000000..ffd26cd2f --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/LayerStackManager.kt @@ -0,0 +1,382 @@ +package net.kernelpanicsoft.archie.gui.layer + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Composition +import androidx.compose.runtime.CompositionContext +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.LaunchedEffect +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateListOf +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope +import androidx.compose.runtime.setValue +import net.kernelpanicsoft.archie.gui.animation.AnimationSpec +import net.kernelpanicsoft.archie.gui.animation.Easing +import net.kernelpanicsoft.archie.gui.animation.Easings +import net.kernelpanicsoft.archie.gui.animation.animateFloat +import net.kernelpanicsoft.archie.gui.animation.animateInt +import net.kernelpanicsoft.archie.gui.composables.containers.RootContainer +import net.kernelpanicsoft.archie.gui.composables.modal.AlertDialog +import net.kernelpanicsoft.archie.gui.composables.modal.ChoiceDialog +import net.kernelpanicsoft.archie.gui.composables.modal.ConfirmDialog +import net.kernelpanicsoft.archie.gui.composables.modal.ModalChoice +import net.kernelpanicsoft.archie.gui.composables.modal.PromptDialog +import net.kernelpanicsoft.archie.gui.layout.Box +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.layout.IntCoordinates +import net.kernelpanicsoft.archie.gui.layout.Size +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.appearance.background +import net.kernelpanicsoft.archie.gui.modifiers.fillMaxSize +import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType +import net.kernelpanicsoft.archie.gui.modifiers.input.onPointerEvent +import net.kernelpanicsoft.archie.gui.modifiers.position.offset +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.minecraft.network.chat.Component +import java.util.* +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch +import net.kernelpanicsoft.archie.gui.modifiers.position.zIndex +import kotlin.math.max +import kotlin.math.min +import kotlin.math.roundToInt +import kotlin.time.Duration +import kotlin.time.Duration.Companion.milliseconds + +/** + * Provides the nearest [LayerStackManager] to composables inside a [net.kernelpanicsoft.archie.gui.ComposeScreen] + * or [net.kernelpanicsoft.archie.gui.ComposeContainerScreen]. + * + * Access via `LocalLayerManager.current` to push new overlay layers. + */ +val LocalLayerManager = compositionLocalOf { + error("No LayerManager provided. Are you inside a ComposeScreen?") +} + +/** The depth index of the currently composed layer (base layer is `0`). */ +val LocalLayerDepth = compositionLocalOf { 0 } + +/** + * Receiver scope for modal layer content, exposing a way to close the modal. + */ +interface ModalScope { + /** + * Dismisses (removes) the modal layer that owns this scope. + */ + fun dismiss() +} + +/** Transition defaults applied to every modal pushed through [LayerStackManager.modal]. */ +data class ModalTransitionSpec( + val durationMillis: Duration = 180.milliseconds, + val easing: Easing = Easings.OutCubic, + val enterOffsetY: Int = 8, + val maxBackdropAlpha: Int = 132, +) + +/** + * Manages an ordered stack of [Layer]s for a single screen. + * + * The stack determines the rendering order (bottom to top) and input-dispatch priority + * (top layer receives events first). Overlays such as dialogs, dropdowns, and tooltips + * are each their own layer on top of the base screen content. + * + * Obtain an instance via the [LocalLayerManager] composition local. + * + * @param parentComposition The [CompositionContext] from the host screen, required + * when creating child [Composition]s for each layer. + */ +class LayerStackManager(private val parentComposition: CompositionContext) { + + /** The ordered list of active layers. Layers are rendered bottom-to-top. */ + val layers = mutableStateListOf() + + /** + * Represents the total size of the screen, calculated based on the dimensions of all active layers. + * + * This property computes the maximum width and height among all the root container nodes + * from the layers managed by the containing class. It aggregates these dimensions by traversing + * the active layers and comparing their widths and heights. + * + * If a layer does not have a root container node, it is skipped in the calculation. + * + * @return A [Size] object representing the combined width and height required to encapsulate + * all visible layers. + */ + val screenSize: Size + get() = layers.fold(Size(0, 0)) { acc, layer -> + val node = layer.rootContainerNode ?: return@fold acc + Size(max(node.width, acc.width), max(node.height, acc.height)) + } + + /** + * Represents the top-left position of the screen, calculated based on the root container + * nodes of all active layers within the layer stack. + * + * The result aggregates the minimum x and y coordinates across all layers. If no root + * container nodes are found, the default position is (0, 0). + * + * The position is determined by folding over all layers and comparing the x and y positions + * of their root container nodes, if present. The computation ensures that the resulting + * coordinates account for the smallest bounds of the visible layers in the stack. + */ + val screenPos: IntCoordinates + get() = layers.fold(null) { acc, layer -> + val node = layer.rootContainerNode ?: return@fold acc + IntCoordinates(min(acc?.x ?: node.x, node.x), min(acc?.y ?: node.y, node.y)) + } ?: IntCoordinates(0, 0) + + /** + * Pushes a new generic layer onto the stack. + * + * The content lambda receives a `dismiss` function it can call to remove itself + * from the stack. This overload is suitable for persistent overlays and custom + * layer types. + * + * @param layerContent The composable content for the new layer. + * @return A dismiss handle; call it to imperatively remove the layer. + */ + fun push(layerContent: @Composable (dismiss: () -> Unit) -> Unit): () -> Unit { + val layerId = UUID.randomUUID() + val layerDepth = layers.size + val layer = Layer(id = layerId, parentComposition = parentComposition, depth = layerDepth) { + CompositionLocalProvider(LocalLayerDepth provides layerDepth) { + layerContent { popById(layerId) } + } + } + layers.add(layer) + return { popById(layerId) } + } + + /** + * Pushes a new modal layer onto the stack. + * + * Modals are opinionated, input-blocking overlays ideal for dialogs and confirmation + * prompts. A click outside the modal content area triggers [onDismissRequest] and, + * when [dismissOnClickOutside] is `true`, automatically removes the layer. + * + * @param alignment Alignment of the modal within the full screen. Default [Alignment.Center]. + * @param dismissOnClickOutside Whether clicking outside the modal content closes it. + * @param onDismissRequest Optional callback invoked when the modal is dismissed. + * @param content The modal UI, a composable lambda with [ModalScope] receiver. + */ + fun modal( + alignment: Alignment = Alignment.Center, + dismissOnClickOutside: Boolean = true, + transitionSpec: ModalTransitionSpec = ModalTransitionSpec(), + onDismissRequest: () -> Unit = {}, + content: @Composable ModalScope.() -> Unit, + ) { + push { popLayer -> + var entered by remember { mutableStateOf(false) } + var closing by remember { mutableStateOf(false) } + val closeScope = rememberCoroutineScope() + val progress = animateFloat( + targetValue = if (entered) 1f else 0f, + spec = AnimationSpec(durationMillis = transitionSpec.durationMillis, easing = transitionSpec.easing), + ) + + fun requestDismiss() { + if (closing) return + closing = true + entered = false + onDismissRequest() + closeScope.launch { + delay(transitionSpec.durationMillis) + popLayer() + } + } + + val scope = object : ModalScope { + override fun dismiss() = requestDismiss() + } + + LaunchedEffect(Unit) { + entered = true + } + + ModalLayout( + alignment = alignment, + dismissOnClickOutside = dismissOnClickOutside, + onDismissRequest = ::requestDismiss, + transitionSpec = transitionSpec, + transitionProgress = progress, + content = { scope.content() }, + ) + } + } + + /** + * Pushes a modal presenting a [ConfirmDialog] with confirm/cancel actions. The modal + * animates out and dismisses itself after either action runs. + * + * @param onConfirm Invoked when the user confirms. + * @param onCancel Invoked when the user cancels. + * @param content Additional body content shown above the actions. + */ + fun confirmDialog( + title: Component = Component.literal("Confirm Dialog"), + confirmText: Component = Component.literal("Confirm"), + cancelText: Component = Component.literal("Cancel"), + onConfirm: () -> Unit = {}, + onCancel: () -> Unit = {}, + content: @Composable () -> Unit + ) { + modal( + dismissOnClickOutside = false + ) { + + ConfirmDialog( + title = title, + confirmText = confirmText, + cancelText = cancelText, + onConfirm = onConfirm, + onCancel = onCancel, + content = content + ) + } + } + + /** + * Pushes a modal presenting an [AlertDialog] with a single acknowledgement action. + * + * @param onConfirm Invoked when the user acknowledges the alert. + */ + fun alertDialog( + title: Component = Component.literal("Alert"), + message: Component, + confirmText: Component = Component.literal("OK"), + onConfirm: () -> Unit = {}, + ) { + modal(dismissOnClickOutside = false) { + AlertDialog( + title = title, + message = message, + confirmText = confirmText, + onConfirm = onConfirm, + ) + } + } + + /** + * Pushes a modal presenting a [PromptDialog] for single-line text input. + * + * @param initialValue Text prefilled in the input field. + * @param validator Predicate controlling whether the confirm action is enabled. + * @param onConfirm Invoked with the entered text when the user confirms. + * @param onCancel Invoked when the user cancels. + */ + fun promptDialog( + title: Component = Component.literal("Enter Value"), + initialValue: String = "", + prompt: Component = Component.literal("Enter a value:"), + confirmText: Component = Component.literal("Confirm"), + cancelText: Component = Component.literal("Cancel"), + validator: (String) -> Boolean = { true }, + onConfirm: (String) -> Unit, + onCancel: () -> Unit = {}, + ) { + modal(dismissOnClickOutside = false) { + PromptDialog( + title = title, + initialValue = initialValue, + prompt = prompt, + confirmText = confirmText, + cancelText = cancelText, + validator = validator, + onConfirm = onConfirm, + onCancel = onCancel, + ) + } + } + + /** + * Pushes a modal presenting a [ChoiceDialog] listing [choices] for the user to pick from. + * + * @param choices The selectable options. + * @param onSelected Invoked with the chosen value's [ModalChoice.value] when a choice is picked. + * @param onCancel Invoked when the user cancels without choosing. + */ + fun choiceDialog( + title: Component = Component.literal("Choose an Option"), + message: Component? = null, + choices: List>, + cancelText: Component = Component.literal("Cancel"), + onSelected: (T) -> Unit, + onCancel: () -> Unit = {}, + ) { + modal(dismissOnClickOutside = false) { + ChoiceDialog( + title = title, + message = message, + choices = choices, + cancelText = cancelText, + onSelected = onSelected, + onCancel = onCancel, + ) + } + } + + /** + * Removes and disposes the topmost layer. + */ + fun pop() = layers.removeLastOrNull()?.dispose() + + /** + * Removes and disposes the layer identified by [id]. + * + * Does nothing if no layer with that id exists. + * + * @param id The [UUID] of the layer to remove. + */ + fun popById(id: UUID) { + val layer = layers.find { it.id == id } ?: return + layer.dispose() + layers.remove(layer) + } + + /** + * The topmost (most recently pushed) layer, which receives input events first. + * `null` if the stack is empty. + */ + val top: Layer? get() = layers.lastOrNull() + + @Composable + private fun ModalLayout( + alignment: Alignment, + onDismissRequest: () -> Unit, + dismissOnClickOutside: Boolean, + transitionSpec: ModalTransitionSpec, + transitionProgress: Float, + content: @Composable () -> Unit, + ) { + val alpha = animateInt( + targetValue = (transitionSpec.maxBackdropAlpha * transitionProgress.coerceIn(0f, 1f)).roundToInt(), + spec = AnimationSpec(durationMillis = transitionSpec.durationMillis, easing = transitionSpec.easing), + ) + val offsetY = animateInt( + targetValue = ((1f - transitionProgress.coerceIn(0f, 1f)) * transitionSpec.enterOffsetY).roundToInt(), + spec = AnimationSpec(durationMillis = transitionSpec.durationMillis, easing = transitionSpec.easing), + ) + var rootModifier = Modifier.fillMaxSize() + .background((alpha.coerceIn(0, 255) shl 24)) + if (dismissOnClickOutside) { + rootModifier = rootModifier.onPointerEvent(PointerEventType.PRESS) { _, event -> + onDismissRequest() + event.consume() + } + } + Box(modifier = rootModifier, contentAlignment = alignment) { + RootContainer( + modifier = Modifier + .offset(x = 0, y = offsetY) + .onPointerEvent(PointerEventType.PRESS) { _, event -> event.consume() } + .zIndex(1f) + ) { + content() + } + } + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Alignment.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Alignment.kt new file mode 100644 index 000000000..1708e411c --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Alignment.kt @@ -0,0 +1,275 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.kernelpanicsoft.archie.gui.layout + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import kotlin.math.roundToInt + +/** + * An interface to calculate the position of a sized box inside an available space. [Alignment] is + * often used to define the alignment of a layout inside a parent layout. + * + * @see AbsoluteAlignment + * @see BiasAlignment + * @see BiasAbsoluteAlignment + */ +@Stable +fun interface Alignment { + /** + * Calculates the position of a box of size [size] relative to the top left corner of an area + * of size [space]. The returned offset can be negative or larger than `space - size`, + * meaning that the box will be positioned partially or completely outside the area. + */ + fun align(size: IntSize, space: IntSize, layoutDirection: LayoutDirection): IntOffset + + /** + * An interface to calculate the position of box of a certain width inside an available width. + * [Alignment.Horizontal] is often used to define the horizontal alignment of a layout inside a + * parent layout. + */ + @Stable + fun interface Horizontal { + /** + * Calculates the horizontal position of a box of width [size] relative to the left + * side of an area of width [space]. The returned offset can be negative or larger than + * `space - size` meaning that the box will be positioned partially or completely outside + * the area. + */ + fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int + } + + /** + * An interface to calculate the position of a box of a certain height inside an available + * height. [Alignment.Vertical] is often used to define the vertical alignment of a + * layout inside a parent layout. + */ + @Stable + fun interface Vertical { + /** + * Calculates the vertical position of a box of height [size] relative to the top edge of + * an area of height [space]. The returned offset can be negative or larger than + * `space - size` meaning that the box will be positioned partially or completely outside + * the area. + */ + fun align(size: Int, space: Int): Int + } + + /** + * A collection of common [Alignment]s aware of layout direction. + */ + companion object { + // 2D Alignments. + @Stable + val TopStart: Alignment = BiasAlignment(-1f, -1f) + @Stable + val TopCenter: Alignment = BiasAlignment(0f, -1f) + @Stable + val TopEnd: Alignment = BiasAlignment(1f, -1f) + @Stable + val CenterStart: Alignment = BiasAlignment(-1f, 0f) + @Stable + val Center: Alignment = BiasAlignment(0f, 0f) + @Stable + val CenterEnd: Alignment = BiasAlignment(1f, 0f) + @Stable + val BottomStart: Alignment = BiasAlignment(-1f, 1f) + @Stable + val BottomCenter: Alignment = BiasAlignment(0f, 1f) + @Stable + val BottomEnd: Alignment = BiasAlignment(1f, 1f) + + // 1D Alignment.Verticals. + @Stable + val Top: Vertical = BiasAlignment.Vertical(-1f) + @Stable + val CenterVertically: Vertical = BiasAlignment.Vertical(0f) + @Stable + val Bottom: Vertical = BiasAlignment.Vertical(1f) + + // 1D Alignment.Horizontals. + @Stable + val Start: Horizontal = BiasAlignment.Horizontal(-1f) + @Stable + val CenterHorizontally: Horizontal = BiasAlignment.Horizontal(0f) + @Stable + val End: Horizontal = BiasAlignment.Horizontal(1f) + } +} + +/** + * A collection of common [Alignment]s unaware of the layout direction. + */ +object AbsoluteAlignment { + // 2D AbsoluteAlignments. + @Stable + val TopLeft: Alignment = BiasAbsoluteAlignment(-1f, -1f) + + @Stable + val TopRight: Alignment = BiasAbsoluteAlignment(1f, -1f) + + @Stable + val CenterLeft: Alignment = BiasAbsoluteAlignment(-1f, 0f) + + @Stable + val CenterRight: Alignment = BiasAbsoluteAlignment(1f, 0f) + + @Stable + val BottomLeft: Alignment = BiasAbsoluteAlignment(-1f, 1f) + + @Stable + val BottomRight: Alignment = BiasAbsoluteAlignment(1f, 1f) + + // 1D BiasAbsoluteAlignment.Horizontals. + @Stable + val Left: Alignment.Horizontal = BiasAbsoluteAlignment.Horizontal(-1f) + + @Stable + val Right: Alignment.Horizontal = BiasAbsoluteAlignment.Horizontal(1f) +} + +/** + * An [Alignment] specified by bias: for example, a bias of -1 represents alignment to the + * start/top, a bias of 0 will represent centering, and a bias of 1 will represent end/bottom. + * Any value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained + * alignment will position the aligned size fully inside the available space, while outside the + * range it will the aligned size will be positioned partially or completely outside. + * + * @see BiasAbsoluteAlignment + * @see Alignment + */ +@Immutable +data class BiasAlignment( + val horizontalBias: Float, + val verticalBias: Float +) : Alignment { + override fun align( + size: IntSize, + space: IntSize, + layoutDirection: LayoutDirection + ): IntOffset { + // Convert to Px first and only round at the end, to avoid rounding twice while calculating + // the new positions + val centerX = (space.width - size.width).toFloat() / 2f + val centerY = (space.height - size.height).toFloat() / 2f + val resolvedHorizontalBias = if (layoutDirection == LayoutDirection.Ltr) { + horizontalBias + } else { + -1 * horizontalBias + } + + val x = centerX * (1 + resolvedHorizontalBias) + val y = centerY * (1 + verticalBias) + return IntOffset(x.roundToInt(), y.roundToInt()) + } + + /** + * An [Alignment.Horizontal] specified by bias: for example, a bias of -1 represents alignment + * to the start, a bias of 0 will represent centering, and a bias of 1 will represent end. + * Any value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained + * alignment will position the aligned size fully inside the available space, while outside the + * range it will the aligned size will be positioned partially or completely outside. + * + * @see BiasAbsoluteAlignment.Horizontal + * @see Vertical + */ + @Immutable + data class Horizontal(private val bias: Float) : Alignment.Horizontal { + override fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int { + // Convert to Px first and only round at the end, to avoid rounding twice while + // calculating the new positions + val center = (space - size).toFloat() / 2f + val resolvedBias = if (layoutDirection == LayoutDirection.Ltr) bias else -1 * bias + return (center * (1 + resolvedBias)).roundToInt() + } + } + + /** + * An [Alignment.Vertical] specified by bias: for example, a bias of -1 represents alignment + * to the top, a bias of 0 will represent centering, and a bias of 1 will represent bottom. + * Any value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained + * alignment will position the aligned size fully inside the available space, while outside the + * range it will the aligned size will be positioned partially or completely outside. + * + * @see Horizontal + */ + @Immutable + data class Vertical(private val bias: Float) : Alignment.Vertical { + override fun align(size: Int, space: Int): Int { + // Convert to Px first and only round at the end, to avoid rounding twice while + // calculating the new positions + val center = (space - size).toFloat() / 2f + return (center * (1 + bias)).roundToInt() + } + } +} + +/** + * An [Alignment] specified by bias: for example, a bias of -1 represents alignment to the + * left/top, a bias of 0 will represent centering, and a bias of 1 will represent right/bottom. + * Any value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained + * alignment will position the aligned size fully inside the available space, while outside the + * range it will the aligned size will be positioned partially or completely outside. + * + * @see AbsoluteAlignment + * @see Alignment + */ +@Immutable +data class BiasAbsoluteAlignment( + private val horizontalBias: Float, + private val verticalBias: Float +) : Alignment { + /** + * Returns the position of a 2D point in a container of a given size, according to this + * [BiasAbsoluteAlignment]. The position will not be mirrored in Rtl context. + */ + override fun align(size: IntSize, space: IntSize, layoutDirection: LayoutDirection): IntOffset { + // Convert to Px first and only round at the end, to avoid rounding twice while calculating + // the new positions + val remaining = IntSize(space.width - size.width, space.height - size.height) + val centerX = remaining.width.toFloat() / 2f + val centerY = remaining.height.toFloat() / 2f + + val x = centerX * (1 + horizontalBias) + val y = centerY * (1 + verticalBias) + return IntOffset(x.roundToInt(), y.roundToInt()) + } + + /** + * An [Alignment.Horizontal] specified by bias: for example, a bias of -1 represents alignment + * to the left, a bias of 0 will represent centering, and a bias of 1 will represent right. + * Any value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained + * alignment will position the aligned size fully inside the available space, while outside the + * range it will the aligned size will be positioned partially or completely outside. + * + * @see BiasAlignment.Horizontal + */ + @Immutable + data class Horizontal(private val bias: Float) : Alignment.Horizontal { + /** + * Returns the position of a 2D point in a container of a given size, + * according to this [BiasAbsoluteAlignment.Horizontal]. This position will not be + * mirrored in Rtl context. + */ + override fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int { + // Convert to Px first and only round at the end, to avoid rounding twice while + // calculating the new positions + val center = (space - size).toFloat() / 2f + return (center * (1 + bias)).roundToInt() + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Arrangement.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Arrangement.kt new file mode 100644 index 000000000..5fc9a9c3e --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Arrangement.kt @@ -0,0 +1,689 @@ +package net.kernelpanicsoft.archie.gui.layout + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import kotlin.math.min +import kotlin.math.roundToInt + +/** + * Used to specify the arrangement of the layout's children in layouts like [Row] or [Column] in + * the main axis direction (horizontal and vertical, respectively). + * + * Below is an illustration of different horizontal arrangements in [Row]s: + * ![Row arrangements](https://developer.android.com/images/reference/androidx/compose/foundation/layout/row_arrangement_visualization.gif) + * + * Different vertical arrangements in [Column]s: + * ![Column arrangements](https://developer.android.com/images/reference/androidx/compose/foundation/layout/column_arrangement_visualization.gif) + */ +@Immutable +object Arrangement { + /** + * Used to specify the horizontal arrangement of the layout's children in layouts like [Row]. + */ + @Stable + interface Horizontal { + /** + * Spacing that should be added between any two adjacent layout children. + */ + val spacing get() = 0 + + /** + * Horizontally places the layout children. + * + * @param totalSize Available space that can be occupied by the children, in pixels. + * @param sizes An array of sizes of all children, in pixels. + * @param layoutDirection A layout direction, left-to-right or right-to-left, of the parent + * layout that should be taken into account when determining positions of the children. + * @param outPositions An array of the size of [sizes] that returns the calculated + * positions relative to the left, in pixels. + */ + fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) + } + + /** + * Used to specify the vertical arrangement of the layout's children in layouts like [Column]. + */ + @Stable + interface Vertical { + /** + * Spacing that should be added between any two adjacent layout children. + */ + val spacing get() = 0.dp + + /** + * Vertically places the layout children. + * + * @param totalSize Available space that can be occupied by the children, in pixels. + * @param sizes An array of sizes of all children, in pixels. + * @param outPositions An array of the size of [sizes] that returns the calculated + * positions relative to the top, in pixels. + */ + fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) + } + + /** + * Used to specify the horizontal arrangement of the layout's children in horizontal layouts + * like [Row], or the vertical arrangement of the layout's children in vertical layouts like + * [Column]. + */ + @Stable + interface HorizontalOrVertical : Horizontal, Vertical { + /** + * Spacing that should be added between any two adjacent layout children. + */ + override val spacing: Dp get() = 0.dp + } + + /** + * Place children horizontally such that they are as close as possible to the beginning of the + * horizontal axis (left if the layout direction is LTR, right otherwise). + * Visually: 123#### for LTR and ####321. + */ + @Stable + val Start = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = if (layoutDirection == LayoutDirection.Ltr) { + placeLeftOrTop(sizes, outPositions, reverseInput = false) + } else { + placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = true) + } + + override fun toString() = "Arrangement#Start" + } + + /** + * Place children horizontally such that they are as close as possible to the end of the main + * axis. + * Visually: ####123 for LTR and 321#### for RTL. + */ + @Stable + val End = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = if (layoutDirection == LayoutDirection.Ltr) { + placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = false) + } else { + placeLeftOrTop(sizes, outPositions, reverseInput = true) + } + + override fun toString() = "Arrangement#End" + } + + /** + * Place children vertically such that they are as close as possible to the top of the main + * axis. + * Visually: (top) 123#### (bottom) + */ + @Stable + val Top = object : Vertical { + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = placeLeftOrTop(sizes, outPositions, reverseInput = false) + + override fun toString() = "Arrangement#Top" + } + + /** + * Place children vertically such that they are as close as possible to the bottom of the main + * axis. + * Visually: (top) ####123 (bottom) + */ + @Stable + val Bottom = object : Vertical { + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "Arrangement#Bottom" + } + + /** + * Place children such that they are as close as possible to the middle of the main axis. + * Visually: ##123## for LTR and ##321## for RTL. + */ + @Stable + val Center = object : HorizontalOrVertical { + override val spacing = 0.dp + + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = if (layoutDirection == LayoutDirection.Ltr) { + placeCenter(totalSize, sizes, outPositions, reverseInput = false) + } else { + placeCenter(totalSize, sizes, outPositions, reverseInput = true) + } + + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = placeCenter(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "Arrangement#Center" + } + + /** + * Place children such that they are spaced evenly across the main axis, including free + * space before the first child and after the last child. + * Visually: #1#2#3# for LTR and #3#2#1# for RTL. + */ + @Stable + val SpaceEvenly = object : HorizontalOrVertical { + override val spacing = 0.dp + + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = if (layoutDirection == LayoutDirection.Ltr) { + placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = false) + } else { + placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = true) + } + + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "Arrangement#SpaceEvenly" + } + + /** + * Place children such that they are spaced evenly across the main axis, without free + * space before the first child or after the last child. + * Visually: 1##2##3 for LTR or 3##2##1 for RTL. + */ + @Stable + val SpaceBetween = object : HorizontalOrVertical { + override val spacing = 0.dp + + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = if (layoutDirection == LayoutDirection.Ltr) { + placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = false) + } else { + placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = true) + } + + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "Arrangement#SpaceBetween" + } + + /** + * Place children such that they are spaced evenly across the main axis, including free + * space before the first child and after the last child, but half the amount of space + * existing otherwise between two consecutive children. + * Visually: #1##2##3# for LTR and #3##2##1# for RTL + */ + @Stable + val SpaceAround = object : HorizontalOrVertical { + override val spacing = 0.dp + + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = if (layoutDirection == LayoutDirection.Ltr) { + placeSpaceAround(totalSize, sizes, outPositions, reverseInput = false) + } else { + placeSpaceAround(totalSize, sizes, outPositions, reverseInput = true) + } + + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = placeSpaceAround(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "Arrangement#SpaceAround" + } + + /** + * Place children such that each two adjacent ones are spaced by a fixed [space] distance across + * the main axis. The spacing will be subtracted from the available space that the children + * can occupy. The [space] can be negative, in which case children will overlap. + * + * To change alignment of the spaced children horizontally or vertically, use [spacedBy] + * overloads with `alignment` parameter. + * + * @param space The space between adjacent children. + */ + @Stable + fun spacedBy(space: Dp): HorizontalOrVertical = + SpacedAligned(space, true) { size, layoutDirection -> + Alignment.Start.align(0, size, layoutDirection) + } + + /** + * Place children horizontally such that each two adjacent ones are spaced by a fixed [space] + * distance. The spacing will be subtracted from the available width that the children + * can occupy. An [alignment] can be specified to align the spaced children horizontally + * inside the parent, in case there is empty width remaining. The [space] can be negative, + * in which case children will overlap. + * + * @param space The space between adjacent children. + * @param alignment The alignment of the spaced children inside the parent. + */ + @Stable + fun spacedBy(space: Dp, alignment: Alignment.Horizontal): Horizontal = + SpacedAligned(space, true) { size, layoutDirection -> + alignment.align(0, size, layoutDirection) + } + + /** + * Place children vertically such that each two adjacent ones are spaced by a fixed [space] + * distance. The spacing will be subtracted from the available height that the children + * can occupy. An [alignment] can be specified to align the spaced children vertically + * inside the parent, in case there is empty height remaining. The [space] can be negative, + * in which case children will overlap. + * + * @param space The space between adjacent children. + * @param alignment The alignment of the spaced children inside the parent. + */ + @Stable + fun spacedBy(space: Dp, alignment: Alignment.Vertical): Vertical = + SpacedAligned(space, false) { size, _ -> alignment.align(0, size) } + + /** + * Place children horizontally one next to the other and align the obtained group + * according to an [alignment]. + * + * @param alignment The alignment of the children inside the parent. + */ + @Stable + fun aligned(alignment: Alignment.Horizontal): Horizontal = + SpacedAligned(0.dp, true) { size, layoutDirection -> + alignment.align(0, size, layoutDirection) + } + + /** + * Place children vertically one next to the other and align the obtained group + * according to an [alignment]. + * + * @param alignment The alignment of the children inside the parent. + */ + @Stable + fun aligned(alignment: Alignment.Vertical): Vertical = + SpacedAligned(0.dp, false) { size, _ -> alignment.align(0, size) } + + @Immutable + object Absolute { + /** + * Place children horizontally such that they are as close as possible to the left edge of + * the [Row]. + * + * Unlike [Arrangement.Start], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * Visually: 123#### + */ + @Stable + val Left = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = placeLeftOrTop(sizes, outPositions, reverseInput = false) + + override fun toString() = "AbsoluteArrangement#Left" + } + + /** + * Place children such that they are as close as possible to the middle of the [Row]. + * + * Unlike [Arrangement.Center], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * Visually: ##123## + */ + @Stable + val Center = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = placeCenter(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "AbsoluteArrangement#Center" + } + + /** + * Place children horizontally such that they are as close as possible to the right edge of + * the [Row]. + * + * Unlike [Arrangement.End], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * Visually: ####123 + */ + @Stable + val Right = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "AbsoluteArrangement#Right" + } + + /** + * Place children such that they are spaced evenly across the main axis, without free + * space before the first child or after the last child. + * + * Unlike [Arrangement.SpaceBetween], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * Visually: 1##2##3 + */ + @Stable + val SpaceBetween = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "AbsoluteArrangement#SpaceBetween" + } + + /** + * Place children such that they are spaced evenly across the main axis, including free + * space before the first child and after the last child. + * + * Unlike [Arrangement.SpaceEvenly], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * Visually: #1#2#3# + */ + @Stable + val SpaceEvenly = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "AbsoluteArrangement#SpaceEvenly" + } + + /** + * Place children such that they are spaced evenly horizontally, including free + * space before the first child and after the last child, but half the amount of space + * existing otherwise between two consecutive children. + * + * Unlike [Arrangement.SpaceAround], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * Visually: #1##2##3##4# + */ + @Stable + val SpaceAround = object : Horizontal { + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) = placeSpaceAround(totalSize, sizes, outPositions, reverseInput = false) + + override fun toString() = "AbsoluteArrangement#SpaceAround" + } + + /** + * Place children such that each two adjacent ones are spaced by a fixed [space] distance across + * the main axis. The spacing will be subtracted from the available space that the children + * can occupy. + * + * Unlike [Arrangement.spacedBy], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * @param space The space between adjacent children. + */ + @Stable + fun spacedBy(space: Dp): HorizontalOrVertical = + SpacedAligned(space, false, null) + + /** + * Place children horizontally such that each two adjacent ones are spaced by a fixed [space] + * distance. The spacing will be subtracted from the available width that the children + * can occupy. An [alignment] can be specified to align the spaced children horizontally + * inside the parent, in case there is empty width remaining. + * + * Unlike [Arrangement.spacedBy], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * @param space The space between adjacent children. + * @param alignment The alignment of the spaced children inside the parent. + */ + @Stable + fun spacedBy(space: Dp, alignment: Alignment.Horizontal): Horizontal = + SpacedAligned(space, false) { size, layoutDirection -> + alignment.align(0, size, layoutDirection) + } + + /** + * Place children vertically such that each two adjacent ones are spaced by a fixed [space] + * distance. The spacing will be subtracted from the available height that the children + * can occupy. An [alignment] can be specified to align the spaced children vertically + * inside the parent, in case there is empty height remaining. + * + * Unlike [Arrangement.spacedBy], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * @param space The space between adjacent children. + * @param alignment The alignment of the spaced children inside the parent. + */ + @Stable + fun spacedBy(space: Dp, alignment: Alignment.Vertical): Vertical = + SpacedAligned(space, false) { size, _ -> alignment.align(0, size) } + + /** + * Place children horizontally one next to the other and align the obtained group + * according to an [alignment]. + * + * Unlike [Arrangement.aligned], when the layout direction is RTL, the children will not be + * mirrored and as such children will appear in the order they are composed inside the [Row]. + * + * @param alignment The alignment of the children inside the parent. + */ + @Stable + fun aligned(alignment: Alignment.Horizontal): Horizontal = + SpacedAligned(0.dp, false) { size, layoutDirection -> + alignment.align(0, size, layoutDirection) + } + } + + /** + * Arrangement with spacing between adjacent children and alignment for the spaced group. + * Should not be instantiated directly, use [spacedBy] instead. + */ + @Immutable + internal data class SpacedAligned( + val space: Dp, + val rtlMirror: Boolean, + val alignment: ((Int, LayoutDirection) -> Int)? + ) : HorizontalOrVertical { + + override val spacing = space + + override fun arrange( + totalSize: Int, + sizes: IntArray, + layoutDirection: LayoutDirection, + outPositions: IntArray + ) { + if (sizes.isEmpty()) return + val spacePx = space + + var occupied = 0 + var lastSpace = 0 + val reversed = rtlMirror && layoutDirection == LayoutDirection.Rtl + sizes.forEachIndexed(reversed) { index, it -> + outPositions[index] = min(occupied, totalSize - it) + lastSpace = min(spacePx, totalSize - outPositions[index] - it) + occupied = outPositions[index] + it + lastSpace + } + occupied -= lastSpace + + if (alignment != null && occupied < totalSize) { + val groupPosition = alignment.invoke(totalSize - occupied, layoutDirection) + for (index in outPositions.indices) { + outPositions[index] += groupPosition + } + } + } + + override fun arrange( + totalSize: Int, + sizes: IntArray, + outPositions: IntArray + ) = arrange(totalSize, sizes, LayoutDirection.Ltr, outPositions) + + override fun toString() = + "${if (rtlMirror) "" else "Absolute"}Arrangement#spacedAligned($space, $alignment)" + } + + internal fun placeRightOrBottom( + totalSize: Int, + size: IntArray, + outPosition: IntArray, + reverseInput: Boolean + ) { + val consumedSize = size.fold(0) { a, b -> a + b } + var current = totalSize - consumedSize + size.forEachIndexed(reverseInput) { index, it -> + outPosition[index] = current + current += it + } + } + + internal fun placeLeftOrTop(size: IntArray, outPosition: IntArray, reverseInput: Boolean) { + var current = 0 + size.forEachIndexed(reverseInput) { index, it -> + outPosition[index] = current + current += it + } + } + + internal fun placeCenter( + totalSize: Int, + size: IntArray, + outPosition: IntArray, + reverseInput: Boolean + ) { + val consumedSize = size.fold(0) { a, b -> a + b } + var current = (totalSize - consumedSize).toFloat() / 2 + size.forEachIndexed(reverseInput) { index, it -> + outPosition[index] = current.roundToInt() + current += it.toFloat() + } + } + + internal fun placeSpaceEvenly( + totalSize: Int, + size: IntArray, + outPosition: IntArray, + reverseInput: Boolean + ) { + val consumedSize = size.fold(0) { a, b -> a + b } + val gapSize = (totalSize - consumedSize).toFloat() / (size.size + 1) + var current = gapSize + size.forEachIndexed(reverseInput) { index, it -> + outPosition[index] = current.roundToInt() + current += it.toFloat() + gapSize + } + } + + internal fun placeSpaceBetween( + totalSize: Int, + size: IntArray, + outPosition: IntArray, + reverseInput: Boolean + ) { + if (size.isEmpty()) return + + val consumedSize = size.fold(0) { a, b -> a + b } + val noOfGaps = maxOf(size.lastIndex, 1) + val gapSize = (totalSize - consumedSize).toFloat() / noOfGaps + + var current = 0f + if (reverseInput && size.size == 1) { + // If the layout direction is right-to-left and there is only one gap, + // we start current with the gap size. That forces the single item to be right-aligned. + current = gapSize + } + size.forEachIndexed(reverseInput) { index, it -> + outPosition[index] = current.roundToInt() + current += it.toFloat() + gapSize + } + } + + internal fun placeSpaceAround( + totalSize: Int, + size: IntArray, + outPosition: IntArray, + reverseInput: Boolean + ) { + val consumedSize = size.fold(0) { a, b -> a + b } + val gapSize = if (size.isNotEmpty()) { + (totalSize - consumedSize).toFloat() / size.size + } else { + 0f + } + var current = gapSize / 2 + size.forEachIndexed(reverseInput) { index, it -> + outPosition[index] = current.roundToInt() + current += it.toFloat() + gapSize + } + } + + private inline fun IntArray.forEachIndexed(reversed: Boolean, action: (Int, Int) -> Unit) { + if (!reversed) { + forEachIndexed(action) + } else { + for (i in (size - 1) downTo 0) { + action(i, get(i)) + } + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Box.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Box.kt new file mode 100644 index 000000000..e69c5ccb6 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Box.kt @@ -0,0 +1,60 @@ +package net.kernelpanicsoft.archie.gui.layout + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import net.kernelpanicsoft.archie.gui.modifiers.Constraints +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.position.MarginModifier +import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier +import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingValues + +/** + * A layout composable that stacks its children on top of each other, aligned within its bounds. + * + * Each child is independently aligned using [contentAlignment]. Children are drawn in + * declaration order (first child at the bottom, last child on top). + * + * ### Example + * ```kotlin + * Box(contentAlignment = Alignment.Center, modifier = Modifier.size(100, 60)) { + * // background fills the box + * Spacer(modifier = Modifier.fillMaxSize().background(KColor.DARK_GRAY)) + * Text(Component.literal("Centered")) + * } + * ``` + * + * @param modifier Modifiers applied to the outer Box node. + * @param contentAlignment How children are positioned within the box. Default [Alignment.TopStart]. + * @param content The child composables to stack. + */ +@Composable +fun Box( + modifier: Modifier = Modifier, + contentAlignment: Alignment = Alignment.TopStart, + content: @Composable () -> Unit +) { + val measurePolicy = remember(contentAlignment) { BoxMeasurePolicy(contentAlignment) } + Layout( + name = "Box", + measurePolicy, + modifier = modifier, + content = content + ) +} + +internal data class BoxMeasurePolicy( + private val alignment: Alignment, +) : RowColumnMeasurePolicy() { + + override fun placeChildren(scope: MeasureScope, measurables: List, placeables: List, width: Int, height: Int): MeasureResult { + return MeasureResult(width, height) { + val inset = (scope as? LayoutNode)?.get()?.padding + ?: PaddingValues() + var accumulatedOutset = 0 + for ((index, child) in placeables.withIndex()) { + child.placeAt(alignment.align(child.size, IntSize(width, height), LayoutDirection.Ltr) + inset.getOffset()) + (measurables[index] as? LayoutNode)?.get()?.let { accumulatedOutset += it.horizontal + it.vertical } + } + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Column.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Column.kt new file mode 100644 index 000000000..835506c31 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Column.kt @@ -0,0 +1,87 @@ +package net.kernelpanicsoft.archie.gui.layout + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.position.MarginModifier +import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier +import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingValues + +/** + * A layout composable that arranges its children in a vertical sequence from top to bottom. + * + * Children are measured sequentially and their heights subtracted from the available space. + * Use [verticalArrangement] to control spacing and placement along the main axis, and + * [horizontalAlignment] to align children along the cross axis. + * + * ### Example + * ```kotlin + * Column( + * verticalArrangement = Arrangement.spacedBy(8), + * horizontalAlignment = Alignment.CenterHorizontally, + * ) { + * Text(Component.literal("Title")) + * Text(Component.literal("Subtitle")) + * } + * ``` + * + * @param modifier Modifiers applied to the Column node. + * @param verticalArrangement Controls spacing and placement along the vertical axis. + * @param horizontalAlignment Controls alignment of children along the horizontal axis. + * @param content The child composables to lay out in a column. + */ +@Composable +fun Column( + modifier: Modifier = Modifier, + verticalArrangement: Arrangement.Vertical = Arrangement.Top, + horizontalAlignment: Alignment.Horizontal = Alignment.Start, + content: @Composable () -> Unit +) { + val measurePolicy = remember(verticalArrangement, horizontalAlignment) { + ColumnMeasurePolicy( + verticalArrangement, + horizontalAlignment + ) + } + Layout( + name = "Column", + measurePolicy, + modifier = modifier, + content = content + ) +} + +private data class ColumnMeasurePolicy( + private val verticalArrangement: Arrangement.Vertical, + private val horizontalAlignment: Alignment.Horizontal, +) : RowColumnMeasurePolicy( + sumHeight = true, + arrangementSpacing = verticalArrangement.spacing +) { + override fun placeChildren(scope: MeasureScope, measurables: List, placeables: List, width: Int, height: Int): MeasureResult { + val childCount = placeables.size + val positions = IntArray(childCount) + val sizes = IntArray(childCount) + for (index in 0 until childCount) { + sizes[index] = placeables[index].height + } + + verticalArrangement.arrange( + totalSize = height, + sizes = sizes, + outPositions = positions + ) + + return MeasureResult(width, height) { + val inset = (scope as? LayoutNode)?.get()?.padding + ?: PaddingValues() + var accumulatedOutset = 0 + + for (index in 0 until childCount) { + val child = placeables[index] + child.placeAt(horizontalAlignment.align(child.width, width, LayoutDirection.Ltr) + inset.left, positions[index] + accumulatedOutset + inset.top) + (measurables[index] as? LayoutNode)?.get()?.let { accumulatedOutset += it.vertical } + } + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Helpers.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Helpers.kt new file mode 100644 index 000000000..52487f5ee --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Helpers.kt @@ -0,0 +1,13 @@ +package net.kernelpanicsoft.archie.gui.layout + +/** + * A density-independent pixel unit used throughout the layout system. Currently an alias for + * [Int] since GUI measurements map 1:1 to Minecraft GUI pixels (no separate density scaling). + */ +typealias Dp = Int + +/** + * Converts this [Int] to a [Dp] value. Provided so measurements read naturally at call sites, + * e.g. `16.dp`, mirroring Compose's `Dp` API even though no unit conversion currently happens. + */ +inline val Int.dp: Int get() = this \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntCoordinates.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntCoordinates.kt new file mode 100644 index 000000000..04d912239 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntCoordinates.kt @@ -0,0 +1,55 @@ +package net.kernelpanicsoft.archie.gui.layout + +import kotlinx.serialization.Serializable + +/** + * A 2D integer coordinate pair, packed into a single [Long] (`x` in the high 32 bits, `y` in the + * low 32 bits) to avoid boxing allocations. Also aliased as [IntOffset] when used to represent a + * relative displacement rather than an absolute position. + */ +@JvmInline +@Serializable +value class IntCoordinates(val pair: Long) { + val x get() = (pair shr 32).toInt() + val y get() = pair.toInt() + + operator fun component1() = x + operator fun component2() = y + + constructor(x: Int, y: Int) : this((x.toLong() shl 32) or y.toLong()) + + override fun toString(): String = "($x, $y)" + + operator fun plus(other: IntCoordinates) = IntCoordinates(x + other.x, y + other.y) + operator fun minus(other: IntCoordinates) = IntCoordinates(x - other.x, y - other.y) +} + +/** An [IntCoordinates] used to represent a relative displacement rather than an absolute position. */ +typealias IntOffset = IntCoordinates + +/** + * An integer width/height pair, packed into a single [Long] (`width` in the high 32 bits, + * `height` in the low 32 bits) to avoid boxing allocations. + */ +@JvmInline +@Serializable +value class IntSize(val pair: Long) { + val width get() = (pair shr 32).toInt() + val height get() = pair.toInt() + + operator fun component1() = width + operator fun component2() = height + + constructor(width: Int, height: Int) : this((width.toLong() shl 32) or height.toLong()) + + override fun toString(): String = "($width, $height)" +} + +/** Creates an [IntCoordinates] at the given [x], [y] position. */ +fun pos(x: Int, y: Int) = IntCoordinates(x, y) + +/** Creates an [IntOffset] with the given [x], [y] displacement, defaulting to zero. */ +fun offset(x: Int = 0, y: Int = 0) = IntOffset(x, y) + +/** Creates an [IntSize] with the given [width] and [height]. */ +fun size(width: Int, height: Int) = IntSize(width, height) \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntRect.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntRect.kt new file mode 100644 index 000000000..86a083c37 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntRect.kt @@ -0,0 +1,44 @@ +package net.kernelpanicsoft.archie.gui.layout + +import kotlinx.serialization.Serializable + +/** + * An axis-aligned integer rectangle described by its min/max bounds along each axis, rather than + * an origin and a size. Used for hit-testing and clip/overlap calculations (e.g. scissor regions). + */ +@Serializable +data class IntRect( + val minX: Int, + val minY: Int, + val maxX: Int, + val maxY: Int, +) { + val width: Int get() = maxX - minX + val height: Int get() = maxY - minY + + /** Returns `true` if this rect has zero or negative width/height. */ + fun isEmpty(): Boolean = width <= 0 || height <= 0 + + /** + * Returns the overlapping region between this rect and [other], or `null` if they don't + * overlap. + */ + fun intersect(other: IntRect): IntRect? { + val ix = maxOf(minX, other.minX) + val iy = maxOf(minY, other.minY) + val ax = minOf(maxX, other.maxX) + val ay = minOf(maxY, other.maxY) + return if (ax <= ix || ay <= iy) null else IntRect(ix, iy, ax, ay) + } + + operator fun div(other: IntRect): IntRect? = intersect(other) + + companion object { + /** A rect with zero bounds on every side. */ + val EMPTY: IntRect = IntRect(0, 0, 0, 0) + + /** Builds an [IntRect] from a top-left [position] and a [size]. */ + fun fromPositionAndSize(position: IntCoordinates, size: Size): IntRect = + IntRect(position.x, position.y, position.x + size.width, position.y + size.height) + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Layout.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Layout.kt new file mode 100644 index 000000000..924503fe3 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Layout.kt @@ -0,0 +1,60 @@ +package net.kernelpanicsoft.archie.gui.layout + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.ComposeNode +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.nodes.LayoutNodeApplier + +/** + * The fundamental building block for creating custom Compose-based UI elements in Archie. + * + * [Layout] is the lowest-level composable: it emits a single [UINode] into the composition + * tree and wires up measurement, rendering, and modifier behaviour via the provided policies. + * Higher-level composables such as [Box], [Row], [Column], and all built-in widgets are + * implemented in terms of [Layout]. + * + * ### Creating a custom composable + * ```kotlin + * @Composable + * fun MyBox(modifier: Modifier = Modifier) { + * Layout( + * measurePolicy = { measurables, constraints -> + * val placeables = measurables.map { it.measure(constraints) } + * MeasureResult(constraints.maxWidth, constraints.maxHeight) { + * placeables.forEach { it.placeAt(0, 0) } + * } + * }, + * renderer = object : Renderer { + * override fun render(node, x, y, guiGraphics, mouseX, mouseY, partialTick) { + * guiGraphics.fill(x, y, x + node.width, y + node.height, 0xFFFF0000.toInt()) + * } + * }, + * modifier = modifier, + * ) + * } + * ``` + * + * @param measurePolicy Defines how this node and its children are measured and placed. + * @param renderer Defines how this node renders itself. Defaults to [EmptyRenderer]. + * @param modifier [Modifier] chain applied to this node. + * @param content Child composables emitted inside this node. + */ +@Composable +inline fun Layout( + name: String, + measurePolicy: MeasurePolicy, + renderer: Renderer = EmptyRenderer, + modifier: Modifier = Modifier, + content: @Composable () -> Unit = {} +) { + ComposeNode( + factory = { LayoutNode(name) }, + update = { + set(measurePolicy) { this.measurePolicy = it } + set(renderer) { this.renderer = it } + set(modifier) { this.modifier = it } + }, + content = content, + ) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutDirection.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutDirection.kt new file mode 100644 index 000000000..24385572e --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutDirection.kt @@ -0,0 +1,19 @@ +package net.kernelpanicsoft.archie.gui.layout + + +/** + * A class for defining layout directions. + * + * A layout direction can be left-to-right (LTR) or right-to-left (RTL). + */ +enum class LayoutDirection { + /** + * Horizontal layout direction is from Left to Right. + */ + Ltr, + + /** + * Horizontal layout direction is from Right to Left. + */ + Rtl +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutNode.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutNode.kt new file mode 100644 index 000000000..cba70c335 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutNode.kt @@ -0,0 +1,401 @@ +package net.kernelpanicsoft.archie.gui.layout + +import net.kernelpanicsoft.archie.gui.ComposeContainerScreen +import net.minecraft.client.Minecraft +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.network.chat.Component +import net.kernelpanicsoft.archie.gui.modifiers.* +import net.kernelpanicsoft.archie.gui.modifiers.position.MarginModifier +import net.kernelpanicsoft.archie.gui.modifiers.position.ZIndexModifier +import net.kernelpanicsoft.archie.gui.modifiers.DebugModifier +import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.util.extension.drawRectOutline +import kotlin.reflect.KClass + +// ARGB debug overlay colours +private const val COMPONENT_OUTLINE = 0xFF00FFFF.toInt() +private const val DEBUG_OUTLINE = 0xFF000000.toInt() +private const val DEBUG_FILL = 0xA7000000.toInt() +private const val DEBUG_TEXT = 0xFFFFFFFF.toInt() +private const val OUTSET_FILL = 0x80800080.toInt() +private const val INSET_FILL = 0x80FF0000.toInt() +private const val LINE_SPACING = 2 +private const val COLUMN_SPACING = 6 + +/** + * The concrete node type that forms Archie's UI scene graph. + * + * Every composable in the Archie GUI framework ultimately creates one [LayoutNode]. + * It handles measurement, draw-chain rendering (including [DrawModifier] wrapping), + * z-index sorting, input hit-testing, and the Ctrl+Shift debug overlay. + * + * Do not instantiate directly — use [Layout] and higher-level composables instead. + * + * @param nodeName A human-readable label shown in the debug overlay for this node. + */ +class LayoutNode( + private val nodeName: String = "LayoutNode", +) : Measurable, Placeable, UINode, MeasureScope { + + override var measurePolicy: MeasurePolicy = ChildMeasurePolicy + override var renderer: Renderer = EmptyRenderer + + /** Mutable list of child LayoutNodes managed by the Compose applier. */ + val children = mutableListOf() + + var layer: Int = 0 + get() = parent?.layer ?: field + set(value) = parent?.let { it.layer = value } ?: run { field = value } + + private var childrenAscendingZCache: List? = null + private var childrenDescendingZCache: List? = null + + /** This node's human-readable label, as shown in the debug overlay and used by [findNode]/[findAllNodes]. */ + val name: String get() = nodeName + + /** Recursively searches this subtree for a descendant node whose [nodeName] equals [name]. */ + fun findNode(name: String): LayoutNode? { + val snapshot = children.toList() + return snapshot.find { it.nodeName == name } ?: snapshot.firstNotNullOfOrNull { it.findNode(name) } + } + + /** Recursively searches this subtree for every descendant node whose [nodeName] equals [name], in depth-first order. */ + fun findAllNodes(name: String): List = children.toList().flatMap { child -> + if (child.nodeName == name) listOf(child) + child.findAllNodes(name) else child.findAllNodes(name) + } + + /** This subtree (this node plus every descendant), in depth-first pre-order. */ + fun flatten(): List = listOf(this) + children.toList().flatMap { it.flatten() } + + override var modifier: Modifier = Modifier + set(value) { + val previousZ = zIndex + field = value + // Rebuild processed-modifier map (merged by type) + processedModifier = modifier.foldIn(mutableMapOf()) { acc, element -> + val existing = acc[element::class] + acc[element::class] = if (existing != null) existing.unsafeMergeWith(element) else element + acc + } + drawModifiers = modifier.foldIn(mutableListOf()) { acc, element -> + if (element is DrawModifier) acc.add(element) + acc + } + layoutChangingModifiers = modifier.foldIn(mutableListOf()) { acc, element -> + if (element is LayoutChangingModifier) acc.add(element) + acc + } + + if (previousZ != zIndex) { + parent?.invalidateChildrenZCache() + } + } + + /** Processed modifier map keyed by element type for O(1) lookup. */ + var processedModifier = mapOf>, Modifier.Element<*>>() + private set + + /** Ordered list of [DrawModifier]s extracted from [modifier]. */ + var drawModifiers: List = emptyList() + private set + + /** Ordered list of [LayoutChangingModifier]s extracted from [modifier]. */ + var layoutChangingModifiers: List = emptyList() + private set + + /** Retrieves the merged [Modifier.Element] of type [T] from [processedModifier], or `null`. */ + inline fun > get(): T? = processedModifier[T::class] as? T + + /** The parent [LayoutNode] in the scene graph, or `null` for root nodes. */ + var parent: LayoutNode? = null + + override var width: Int = 0 + override var height: Int = 0 + override var x: Int = 0 + override var y: Int = 0 + override var renderState: String? = null + + /** The effective z-index for this node, used for draw and input ordering. */ + val zIndex: Float get() = get()?.zIndex ?: 0f + + /** This node's absolute z-depth, combining its [layer]'s base z with all ancestor [zIndex]es. */ + val effectiveZ: Float get() = effectiveZ(ComposeContainerScreen.layerBaseZ(layer)) + + /** Computes the maximum effective z-depth in this subtree, adding [layerOffset]. */ + fun getMaxZ(layerOffset: Float): Float { + val myZ = effectiveZ(layerOffset) + return maxOf(myZ, children.toList().maxOfOrNull { it.getMaxZ(layerOffset) } ?: myZ) + } + + internal fun invalidateChildrenZCache() { + childrenAscendingZCache = null + childrenDescendingZCache = null + } + + internal fun childrenAscendingZ(): List { + val cached = childrenAscendingZCache + if (cached != null) return cached + return children.toList().sortedBy { it.zIndex }.also { sorted -> + childrenAscendingZCache = sorted + childrenDescendingZCache = sorted.asReversed() + } + } + + internal fun childrenDescendingZ(): List { + val cached = childrenDescendingZCache + if (cached != null) return cached + return children.toList().sortedByDescending { it.zIndex }.also { sorted -> + childrenDescendingZCache = sorted + childrenAscendingZCache = sorted.asReversed() + } + } + + private fun effectiveZ(layerOffset: Float): Float = + (parent?.effectiveZ(layerOffset) ?: layerOffset) + zIndex + + /** + * Absolute on-screen coordinates, accumulating parent offsets up the scene graph. + */ + val absoluteCoords: IntCoordinates + get() { + var coords = IntCoordinates(x, y) + var p = parent + while (p != null) { coords += IntCoordinates(p.x, p.y); p = p.parent } + return coords + } + + /** The topmost ancestor [LayoutNode] (the root of this subtree). */ + val rootNode: LayoutNode get() = parent?.rootNode ?: this + + /** + * Whether the debug overlay is active. Setting this on a child propagates to the root. + */ + var debug: Boolean = false + get() = parent?.debug ?: field + set(value) = parent?.let { it.debug = value } ?: run { field = value } + + /** + * Whether the extended modifier info is shown in the debug overlay. Setting this propagates to the root. + */ + var extraDebug: Boolean = false + get() = parent?.extraDebug ?: field + set(value) = parent?.let { it.extraDebug = value } ?: run { field = value } + + // ── Measurement ─────────────────────────────────────────────────────── + + override fun measure(constraints: Constraints): Placeable { + // Snapshot once - Compose's Recomposer applies structural changes (LayoutNodeApplier + // insert/remove/move) from its own recompose+apply coroutine, which isn't necessarily + // synchronized with whatever thread is measuring, so iterating the live `children` list + // directly here (as this used to) could throw ConcurrentModificationException if a + // recomposition mutates it mid-measure. + val childrenSnapshot = children.toList() + + // Collect outset (margin) from children + val outset = childrenSnapshot.fold(listOf()) { acc, child -> + acc + child.modifier.getAll() + } + val horizontal = outset.sumOf { it.horizontal } + val vertical = outset.sumOf { it.vertical } + + val innerConstraints = layoutChangingModifiers.fold(constraints) { c, m -> m.modifyInnerConstraints(c) } + val result = measurePolicy.measure(this, childrenSnapshot, innerConstraints) + + // Account for padding inset + val inset = get() + val insetH = inset?.horizontal ?: 0 + val insetV = inset?.vertical ?: 0 + + val newWidth = result.width + horizontal + insetH + val newHeight = result.height + vertical + insetV + + if (width != newWidth || height != newHeight) { + get()?.onSizeChanged?.invoke(Size(newWidth, newHeight)) + } + width = newWidth + height = newHeight + + val layoutConstraints = layoutChangingModifiers.fold(constraints) { c, m -> + m.modifyLayoutConstraints(IntSize(newWidth, newHeight), c) + } + width = width.coerceIn(layoutConstraints.minWidth..layoutConstraints.maxWidth) + height = height.coerceIn(layoutConstraints.minHeight..layoutConstraints.maxHeight) + + result.placer.placeChildren() + + return object : Placeable by this { + override var width: Int = this@LayoutNode.width + override var height: Int = this@LayoutNode.height + } + } + + override fun placeAt(x: Int, y: Int) { + val offset = layoutChangingModifiers.fold(IntOffset(x, y)) { acc, m -> m.modifyPosition(acc) } + this.x = offset.x + this.y = offset.y + get()?.onGloballyPositioned?.invoke(absoluteCoords) + } + + // ── Rendering ───────────────────────────────────────────────────────── + + override fun render(x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + render(x, y, guiGraphics, mouseX, mouseY, partialTick, 0f) + } + + /** + * Renders this node and its entire subtree, with z-index translation, draw-modifier + * wrapping, and the optional debug overlay. + */ + fun render(x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, zOffset: Float) { + if (parent == null) { + guiGraphics.pose().pushPose() + guiGraphics.pose().translate(0.0, 0.0, zOffset.toDouble()) + renderRecursive(x, y, guiGraphics, mouseX, mouseY, partialTick, zOffset) + + if (rootNode.debug) { + guiGraphics.pose().pushPose() + guiGraphics.pose().translate(0.0, 0.0, 1000.0 + zOffset) + renderDebug(x, y, guiGraphics, mouseX, mouseY, partialTick) + guiGraphics.pose().popPose() + } + + guiGraphics.pose().popPose() + } + } + + private fun renderRecursive(x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, zOffset: Float) { + val dx = this.x + x + val dy = this.y + y + + guiGraphics.pose().pushPose() + guiGraphics.pose().translate(0.0, 0.0, zIndex.toDouble()) + + // Build the draw chain from innermost (content) outward through DrawModifiers + val contentDrawer: () -> Unit = { + renderer.render(this, dx, dy, guiGraphics, mouseX, mouseY, partialTick) + childrenAscendingZ().forEach { it.renderRecursive(dx, dy, guiGraphics, mouseX, mouseY, partialTick, zOffset) } + renderer.renderAfterChildren(this, dx, dy, guiGraphics, mouseX, mouseY, partialTick) + } + + val drawChain = drawModifiers.reversed().fold(contentDrawer) { acc, mod -> + { + val scope = object : ContentDrawScope { + override val guiGraphics = guiGraphics + override val width = this@LayoutNode.width + override val height = this@LayoutNode.height + override val x = dx + override val y = dy + override fun drawContent() = acc() + } + with(mod) { scope.draw() } + } + } + drawChain() + + guiGraphics.pose().popPose() + } + + // ── Debug overlay ───────────────────────────────────────────────────── + + private fun renderDebug(x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { + val dx = this.x + x + val dy = this.y + y + + val hoveredChildren = children.toList().filter { it.isBounded(mouseX, mouseY) } + if (hoveredChildren.isNotEmpty()) { + hoveredChildren.forEach { it.renderDebug(dx, dy, guiGraphics, mouseX, mouseY, partialTick) } + return + } + if (!isBounded(mouseX, mouseY)) return + + guiGraphics.drawRectOutline(dx, dy, width, height, COMPONENT_OUTLINE) + + // Margin (outset) visualisation + (processedModifier[MarginModifier::class] as? MarginModifier)?.let { mod -> + with(mod.margin) { + if (top != 0) guiGraphics.fill(dx, dy - top, dx + width, dy, OUTSET_FILL) + if (bottom != 0) guiGraphics.fill(dx, dy + height, dx + width, dy + height + bottom, OUTSET_FILL) + if (left != 0) guiGraphics.fill(dx - left, dy, dx, dy + height, OUTSET_FILL) + if (right != 0) guiGraphics.fill(dx + width, dy, dx + width + right, dy + height, OUTSET_FILL) + } + } + + // Padding (inset) visualisation + (processedModifier[PaddingModifier::class] as? PaddingModifier)?.let { mod -> + with(mod.padding) { + if (top != 0) guiGraphics.fill(dx + left, dy, dx + width - right, dy + top, INSET_FILL) + if (bottom != 0) guiGraphics.fill(dx + left, dy + height - bottom, dx + width - right, dy + height, INSET_FILL) + if (left != 0) guiGraphics.fill(dx, dy + top, dx + left, dy + height - bottom, INSET_FILL) + if (right != 0) guiGraphics.fill(dx + width - right, dy + top, dx + width, dy + height - bottom, INSET_FILL) + } + } + + // Tooltip panel + val font = Minecraft.getInstance().font + var tooltipY = dy + height + 1 + + val debugLines: List> = buildList { + add(listOf(Component.literal(nodeName))) + add(listOf( + Component.literal("X:").apply { append(Component.literal("$dx").withColor(0x00FFFF)); append(", Y:"); append(Component.literal("$dy").withColor(0x32CD32)); append(", Z:"); append(Component.literal("$effectiveZ").withColor(0xFF66FF)) }, + Component.literal("W:").apply { append(Component.literal("$width").withColor(0xFFA500)); append(", H:"); append(Component.literal("$height").withColor(0x87CEEB)); append(", L:"); append(Component.literal("$layer").withColor(0x87CEEB)) }, + )) + if (extraDebug) { + val mods = mutableListOf() + modifier.all { mod -> + if (mod is DebugModifier) mods.addAll(0, mod.toComponents()) + else mods.add(mod.toComponent()) + true + } + if (mods.isNotEmpty()) { + add(listOf(Component.literal("Modifiers:"))) + mods.forEach { add(listOf(it)) } + } + } + } + + val lineWidths = debugLines.map { line -> line.sumOf { font.width(it) } + (line.size - 1) * COLUMN_SPACING } + val maxLineWidth = (lineWidths.maxOrNull() ?: 0) + 4 + val panelHeight = debugLines.size * (font.lineHeight + LINE_SPACING) - LINE_SPACING + 2 + + if (tooltipY + panelHeight > guiGraphics.guiHeight()) tooltipY -= height + panelHeight + 2 + + guiGraphics.drawRectOutline(dx + 1, tooltipY, maxLineWidth, panelHeight, DEBUG_OUTLINE) + guiGraphics.fill(dx + 1, tooltipY, dx + 1 + maxLineWidth, tooltipY + panelHeight, DEBUG_FILL) + + debugLines.forEachIndexed { row, line -> + var colX = dx + 3 + val textY = tooltipY + row * (font.lineHeight + LINE_SPACING) + 1 + line.forEachIndexed { col, text -> + guiGraphics.drawString(font, text, colX, textY, DEBUG_TEXT) + if (col < line.size - 1) colX += font.width(text) + COLUMN_SPACING + } + } + } + + // ── Hit testing ─────────────────────────────────────────────────────── + + /** + * Returns `true` if ([mouseX], [mouseY]) falls within this node's absolute screen bounds. + */ + fun isBounded(mouseX: Int, mouseY: Int): Boolean { + val (ax, ay) = absoluteCoords + return mouseX in ax until (ax + width) && mouseY in ay until (ay + height) + } + + override fun toString() = children.toList().run { if (isNotEmpty()) joinToString(prefix = "$nodeName {\n", separator = "\n", postfix = "\n}") { "\t$it" } else "$nodeName()" } + + internal companion object { + val ChildMeasurePolicy = MeasurePolicy { _, measurables, constraints -> + val placeables = measurables.map { it.measure(constraints) } + MeasureResult( + placeables.maxOfOrNull { it.width } ?: 0, + placeables.maxOfOrNull { it.height } ?: 0, + ) { placeables.forEach { it.placeAt(0, 0) } } + } + } +} + +/** A [Renderer] that performs no drawing — the default for layout-only nodes. */ +val EmptyRenderer = object : Renderer {} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/MeasurePolicy.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/MeasurePolicy.kt new file mode 100644 index 000000000..dd8b7b9a8 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/MeasurePolicy.kt @@ -0,0 +1,123 @@ +package net.kernelpanicsoft.archie.gui.layout + +import androidx.compose.runtime.Stable +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.modifiers.Constraints +import net.minecraft.client.gui.GuiGraphics + +/** + * Marker interface implemented by [LayoutNode] and passed as the first argument to + * [MeasurePolicy.measure]. Measure policies may cast this to [LayoutNode] to access + * node-level properties such as padding or margin modifiers during layout. + */ +interface MeasureScope + +/** + * The result of a [MeasurePolicy.measure] call, containing the intrinsic dimensions of the + * node and a [Placer] that positions child nodes within those bounds. + * + * @property width The measured width in pixels. + * @property height The measured height in pixels. + * @property placer The [Placer] that executes child placement when called. + */ +data class MeasureResult( + val width: Int, + val height: Int, + val placer: Placer, +) + +/** + * Defines how a [LayoutNode] measures itself and its children. + * + * The `scope` parameter is the [LayoutNode] currently being measured, allowing + * measure policies to read node properties (e.g. padding) during layout. + */ +@Stable +fun interface MeasurePolicy { + /** + * Measures [measurables] within [constraints] and returns a [MeasureResult]. + * + * @param scope The [LayoutNode] currently being measured (implements [MeasureScope]). + * @param measurables The child nodes to measure. + * @param constraints The size constraints imposed by the parent. + */ + fun measure(scope: MeasureScope, measurables: List, constraints: Constraints): MeasureResult +} + +/** + * A deferred child-placement action returned inside a [MeasureResult]. + * + * The [placeChildren] function is invoked by [LayoutNode] after measurement is complete to + * call [Placeable.placeAt] on each child. + */ +@Stable +fun interface Placer { + /** Executes all [Placeable.placeAt] calls for this layout pass. */ + fun placeChildren() +} + +/** + * Defines the rendering behaviour of a [LayoutNode]. + * + * Both [render] and [renderAfterChildren] have no-op defaults so implementors only + * override what they need. + */ +@Stable +interface Renderer { + /** + * Called before the node's children are rendered. + * + * Use this for backgrounds, borders, or content that should appear *below* children. + */ + fun render( + node: UINode, x: Int, y: Int, + guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, + ) {} + + /** + * Called after all children have been rendered. + * + * Use this for overlays or post-process effects that should appear *above* children. + */ + fun renderAfterChildren( + node: UINode, x: Int, y: Int, + guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, + ) {} +} + +/** + * A node that can participate in a layout pass by returning a [Placeable]. + */ +interface Measurable { + /** + * Measures this node within [constraints] and returns a [Placeable] for placement. + * + * @param constraints The size constraints imposed by the parent. + */ + fun measure(constraints: Constraints): Placeable +} + +/** + * The result of measuring a node, which can subsequently be positioned via [placeAt]. + */ +interface Placeable { + /** The measured width in pixels. */ + var width: Int + + /** The measured height in pixels. */ + var height: Int + + /** + * Places this node at the given screen coordinates. + * + * @param x Absolute x position in screen pixels. + * @param y Absolute y position in screen pixels. + */ + fun placeAt(x: Int, y: Int) + + /** Places this node using an [IntOffset] convenience type. */ + fun placeAt(offset: IntOffset) = placeAt(offset.x, offset.y) + + /** The measured size as an [IntSize] value. */ + val size: IntSize get() = IntSize(width, height) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Row.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Row.kt new file mode 100644 index 000000000..264077141 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Row.kt @@ -0,0 +1,80 @@ +package net.kernelpanicsoft.archie.gui.layout + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.remember +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.position.MarginModifier +import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier +import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingValues + +/** + * A layout composable that arranges its children in a horizontal sequence from left to right. + * + * Children are measured sequentially and their widths subtracted from the available space. + * Use [horizontalArrangement] to control spacing and alignment along the main axis, and + * [verticalAlignment] to align children along the cross axis. + * + * ### Example + * ```kotlin + * Row( + * horizontalArrangement = Arrangement.spacedBy(8), + * verticalAlignment = Alignment.CenterVertically, + * ) { + * Icon(...) + * Text(Component.literal("Label")) + * } + * ``` + * + * @param modifier Modifiers applied to the Row node. + * @param horizontalArrangement Controls spacing and placement along the horizontal axis. + * @param verticalAlignment Controls alignment of children along the vertical axis. + * @param content The child composables to lay out in a row. + */ +@Composable +fun Row( + modifier: Modifier = Modifier, + horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, + verticalAlignment: Alignment.Vertical = Alignment.Top, + content: @Composable () -> Unit +) { + val measurePolicy = remember(horizontalArrangement, verticalAlignment) { + RowMeasurePolicy( + horizontalArrangement, + verticalAlignment + ) + } + Layout( + name = "Row", + measurePolicy, + modifier = modifier, + content = content + ) +} + +private data class RowMeasurePolicy( + private val horizontalArrangement: Arrangement.Horizontal, + private val verticalAlignment: Alignment.Vertical, +) : RowColumnMeasurePolicy(sumWidth = true, arrangementSpacing = horizontalArrangement.spacing) { + override fun placeChildren(scope: MeasureScope, measurables: List, placeables: List, width: Int, height: Int): MeasureResult { + val childCount = placeables.size + val positions = IntArray(childCount) + val sizes = IntArray(childCount) + for (index in 0 until childCount) { + sizes[index] = placeables[index].width + } + + horizontalArrangement.arrange(totalSize = width, sizes = sizes, layoutDirection = LayoutDirection.Ltr, outPositions = positions) + + return MeasureResult(width, height) { + val inset = (scope as? LayoutNode)?.get()?.padding + ?: PaddingValues() + var accumulatedOutset = 0 + + for (index in 0 until childCount) { + val child = placeables[index] + child.placeAt(positions[index] + accumulatedOutset + inset.left, verticalAlignment.align(child.height, height) + inset.top) + (measurables[index] as? LayoutNode)?.get()?.let { accumulatedOutset += it.horizontal } + } + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/RowColumnMeasurePolicy.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/RowColumnMeasurePolicy.kt new file mode 100644 index 000000000..d607375b0 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/RowColumnMeasurePolicy.kt @@ -0,0 +1,71 @@ +package net.kernelpanicsoft.archie.gui.layout + +import net.kernelpanicsoft.archie.gui.modifiers.Constraints +import kotlin.math.max + +/** + * Base [MeasurePolicy] for [Row] and [Column] layouts. + * + * Handles sequential measurement (subtracting consumed space when [sumWidth] or [sumHeight] + * is `true`) and delegates child placement to [placeChildren]. + * + * @param sumWidth When `true`, each child's width is subtracted from the remaining + * max-width before the next child is measured (Row behaviour). + * @param sumHeight When `true`, each child's height is subtracted from the remaining + * max-height before the next child is measured (Column behaviour). + * @param arrangementSpacing Additional pixels added between siblings by the arrangement. + */ +abstract class RowColumnMeasurePolicy( + val sumWidth: Boolean = false, + val sumHeight: Boolean = false, + val arrangementSpacing: Int = 0, +) : MeasurePolicy { + + override fun measure(scope: MeasureScope, measurables: List, constraints: Constraints): MeasureResult { + var remaining = constraints.copy(minWidth = 0, minHeight = 0) + val placeables = ArrayList(measurables.size) + var widthValue = 0 + var heightValue = 0 + + for (index in measurables.indices) { + val measured = measurables[index].measure(remaining) + placeables += measured + + if (sumWidth) widthValue += measured.width else widthValue = max(widthValue, measured.width) + if (sumHeight) heightValue += measured.height else heightValue = max(heightValue, measured.height) + + remaining = remaining.copy( + maxWidth = if (sumWidth) (remaining.maxWidth - measured.width).coerceAtLeast(0) else remaining.maxWidth, + maxHeight = if (sumHeight) (remaining.maxHeight - measured.height).coerceAtLeast(0) else remaining.maxHeight, + ) + } + + val extraSpacing = (arrangementSpacing * (placeables.size - 1)).coerceAtLeast(0) + val width = if (sumWidth) widthValue + extraSpacing else widthValue + val height = if (sumHeight) heightValue + extraSpacing else heightValue + + return placeChildren( + scope, measurables, placeables, + max(width, constraints.minWidth), + max(height, constraints.minHeight), + ) + } + + /** + * Positions all measured [placeables] within [width] × [height] and returns the + * [MeasureResult]. + * + * @param scope The measuring [LayoutNode]. + * @param measurables The original measurables (for modifier access). + * @param placeables The measured placeables to position. + * @param width The resolved container width. + * @param height The resolved container height. + */ + abstract fun placeChildren( + scope: MeasureScope, + measurables: List, + placeables: List, + width: Int, + height: Int, + ): MeasureResult +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Size.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Size.kt new file mode 100644 index 000000000..2ac00c7ad --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Size.kt @@ -0,0 +1,16 @@ +package net.kernelpanicsoft.archie.gui.layout + +import androidx.compose.runtime.Immutable +import kotlinx.serialization.Serializable + +/** + * An integer width/height pair. Unlike [IntSize], this is a regular [data class][Size] (not an + * inline value class), which makes it convenient where a boxed, nullable, or default-constructed + * size is needed, e.g. component configuration. + */ +@Immutable +@Serializable +data class Size( + val width: Int = 0, + val height: Int = 0 +) \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Constraints.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Constraints.kt new file mode 100644 index 000000000..95c2bbb47 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Constraints.kt @@ -0,0 +1,80 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.kernelpanicsoft.archie.gui.modifiers + +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable + +@Immutable +/** + * Immutable size constraints passed from a parent layout to its children during measurement. + * + * A child must produce a size whose width is in `[minWidth, maxWidth]` and whose height is + * in `[minHeight, maxHeight]`. Use [copy] to derive a modified copy with some dimensions changed, + * and [offset] to shrink the available space by a fixed amount (e.g. for padding). + * + * @property minWidth Minimum allowed width in pixels (inclusive). + * @property maxWidth Maximum allowed width in pixels (inclusive). + * @property minHeight Minimum allowed height in pixels (inclusive). + * @property maxHeight Maximum allowed height in pixels (inclusive). + */ +class Constraints( + val minWidth: Int = 0, + val maxWidth: Int = Int.MAX_VALUE, + val minHeight: Int = 0, + val maxHeight: Int = Int.MAX_VALUE +) { + fun copy( + minWidth: Int = this.minWidth, + maxWidth: Int = this.maxWidth, + minHeight: Int = this.minHeight, + maxHeight: Int = this.maxHeight + ) = Constraints( + minWidth.coerceAtMost(maxWidth), + maxWidth.coerceAtLeast(minWidth), + minHeight.coerceAtMost(maxHeight), + maxHeight.coerceAtLeast(minHeight) + ) + + override fun toString(): String + { + return "Constraints(minWidth=$minWidth, maxWidth=$maxWidth, minHeight=$minHeight, maxHeight=$maxHeight)" + } +} + +/** + * Returns a copy of these [Constraints] expanded or shrunk by [horizontal] pixels on each + * horizontal side and [vertical] pixels on each vertical side. + * + * Negative values shrink the available space (useful for padding). + * [Constraints.maxWidth] and [Constraints.maxHeight] are never reduced below zero. + */ +@Stable +fun Constraints.offset(horizontal: Int = 0, vertical: Int = 0) = Constraints( + (minWidth + horizontal).coerceAtLeast(0), + addMaxWithMinimum(maxWidth, horizontal), + (minHeight + vertical).coerceAtLeast(0), + addMaxWithMinimum(maxHeight, vertical) +) + +private fun addMaxWithMinimum(max: Int, value: Int): Int { + return if (max == Int.MAX_VALUE) { + max + } else { + (max + value).coerceAtLeast(0) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DebugModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DebugModifier.kt new file mode 100644 index 000000000..eaf33a45c --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DebugModifier.kt @@ -0,0 +1,55 @@ +package net.kernelpanicsoft.archie.gui.modifiers + +import androidx.compose.runtime.Stable +import net.minecraft.network.chat.Component + +/** + * A [Modifier.Element] that attaches arbitrary debug information to a composable node. + * + * Debug information is only visible when the debug overlay is active (toggle with + * **Ctrl + Shift** while the screen is open). When [net.kernelpanicsoft.archie.gui.layout.LayoutNode.extraDebug] + * is enabled (hold Shift in debug mode), the attached strings and components are rendered + * inside the debug tooltip alongside node dimensions and coordinates. + * + * Multiple [DebugModifier] elements on the same node are merged by concatenation. + * + * @property strs Plain-text debug strings. + * @property comps Formatted [Component] debug labels. + */ +data class DebugModifier( + val strs: List = emptyList(), + val comps: List = emptyList(), +) : Modifier.Element { + + override fun mergeWith(other: DebugModifier): DebugModifier = + DebugModifier(strs = strs + other.strs, comps = comps + other.comps) + + override fun toString(): String = strs.joinToString(", ").ifEmpty { super.toString() } + + override fun toComponent(): Component = Component.empty().apply { + strs.map { Component.literal(it) }.forEach { append(it) } + comps.forEach { append(it) } + }.takeIf { it != Component.empty() } ?: Component.literal(super.toString()) + + /** Returns the debug information as a list of individual [Component]s, one per item. */ + fun toComponents(): List = + (strs.map { Component.literal(it) } + comps).ifEmpty { listOf(Component.literal(super.toString())) } +} + +/** + * Attaches one or more plain-text debug strings to the composable. + * + * The strings are displayed in the debug overlay when debug mode is active. + * + * @param strs The strings to attach. + */ +@Stable +fun Modifier.debug(vararg strs: String): Modifier = this then DebugModifier(strs = strs.toList()) + +/** + * Attaches one or more formatted [Component] debug labels to the composable. + * + * @param comps The components to attach. + */ +@Stable +fun Modifier.debug(vararg comps: Component): Modifier = this then DebugModifier(comps = comps.toList()) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DrawModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DrawModifier.kt new file mode 100644 index 000000000..6e7e1d1fe --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DrawModifier.kt @@ -0,0 +1,51 @@ +package net.kernelpanicsoft.archie.gui.modifiers + +import net.minecraft.client.gui.GuiGraphics + +/** + * A [Modifier] element that can participate in the draw chain for a composable node. + * + * [DrawModifier]s wrap the node's normal render call, allowing effects to be drawn + * **before** (e.g. a background fill) or **after** (e.g. an overlay) the node's own + * content. The modifier calls [ContentDrawScope.drawContent] to trigger the wrapped render. + * + * Implement [draw] inside the modifier to define the drawing logic. + */ +interface DrawModifier { + /** + * Performs custom drawing for this modifier. + * + * Call [ContentDrawScope.drawContent] at the desired point to render the wrapped content. + * Omitting the call suppresses the node's normal rendering entirely. + */ + fun ContentDrawScope.draw() +} + +/** + * Receiver scope provided to [DrawModifier.draw] containing everything needed to render + * and position content. + */ +interface ContentDrawScope { + /** The current [GuiGraphics] context. */ + val guiGraphics: GuiGraphics + + /** The width of the node being drawn, in pixels. */ + val width: Int + + /** The height of the node being drawn, in pixels. */ + val height: Int + + /** The absolute x coordinate of the node's top-left corner on screen. */ + val x: Int + + /** The absolute y coordinate of the node's top-left corner on screen. */ + val y: Int + + /** + * Renders the wrapped content (the node's own renderer and all child nodes). + * + * Call this at any point inside [DrawModifier.draw] to position the content + * relative to any surrounding draw operations. + */ + fun drawContent() +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/LayoutChangingModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/LayoutChangingModifier.kt new file mode 100644 index 000000000..819c1e3b3 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/LayoutChangingModifier.kt @@ -0,0 +1,47 @@ +package net.kernelpanicsoft.archie.gui.modifiers + +import net.kernelpanicsoft.archie.gui.layout.IntOffset +import net.kernelpanicsoft.archie.gui.layout.IntSize + +/** + * A [Modifier.Element] that can alter a node's position and the [Constraints] seen by the + * node and its children. + * + * Implement this interface alongside [Modifier.Element] when a modifier needs to change + * where the node is placed (e.g. [net.kernelpanicsoft.archie.gui.modifiers.position.OffsetModifier], + * [net.kernelpanicsoft.archie.gui.modifiers.position.MarginModifier]) or the space available + * to the node's subtree (e.g. [net.kernelpanicsoft.archie.gui.modifiers.SizeModifier], + * [net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier]). + */ +interface LayoutChangingModifier { + /** + * Shifts the node's placement by transforming the parent-supplied [offset]. + * + * @param offset The position computed by the parent layout. + * @return The adjusted position for this node. + */ + fun modifyPosition(offset: IntOffset): IntOffset = offset + + /** + * Adjusts the [Constraints] as seen by the **parent** when it lays out this node. + * + * Use this to report a different effective size to the parent (e.g. after accounting for + * margin space that the parent should reserve). + * + * @param measuredSize The actual measured size of this node. + * @param constraints The constraints originally passed by the parent. + * @return The constraints the parent should use when accounting for this node's footprint. + */ + fun modifyLayoutConstraints(measuredSize: IntSize, constraints: Constraints): Constraints = + modifyInnerConstraints(constraints) + + /** + * Adjusts the [Constraints] passed **into** this node for measuring its children. + * + * Use this to reduce the available space before measuring children (e.g. padding). + * + * @param constraints The constraints supplied by this node's parent. + * @return The constraints to use when measuring this node's children. + */ + fun modifyInnerConstraints(constraints: Constraints): Constraints = constraints +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Modifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Modifier.kt new file mode 100644 index 000000000..8dbe4f75a --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Modifier.kt @@ -0,0 +1,153 @@ +/* + * Copyright 2019 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package net.kernelpanicsoft.archie.gui.modifiers + +import net.minecraft.network.chat.Component + +/** + * An ordered, immutable collection of [modifier elements][Modifier.Element] that decorate or add + * behavior to Compose UI elements. For example, backgrounds, padding and click event listeners + * decorate or add behavior to rows, text or buttons. + * + * This class is taken from the androidx Jetpack Compose UI library so as to avoid extra dependencies. + */ +interface Modifier { + + /** + * Accumulates a value starting with [initial] and applying [operation] to the current value + * and each element from outside in. + * + * Elements wrap one another in a chain from left to right; an [Element] that appears to the + * left of another in a `+` expression or in [operation]'s parameter order affects all + * of the elements that appear after it. [foldIn] may be used to accumulate a value starting + * from the parent or head of the modifier chain to the final wrapped child. + */ + fun foldIn(initial: R, operation: (R, Element<*>) -> R): R + + /** + * Accumulates a value starting with [initial] and applying [operation] to the current value + * and each element from inside out. + * + * Elements wrap one another in a chain from left to right; an [Element] that appears to the + * left of another in a `+` expression or in [operation]'s parameter order affects all + * of the elements that appear after it. [foldOut] may be used to accumulate a value starting + * from the child or tail of the modifier chain up to the parent or head of the chain. + */ + fun foldOut(initial: R, operation: (Element<*>, R) -> R): R + + /** + * Returns `true` if [predicate] returns true for any [Element] in this [Modifier]. + */ + fun any(predicate: (Element<*>) -> Boolean): Boolean + + /** + * Returns `true` if [predicate] returns true for all [Element]s in this [Modifier] or if + * this [Modifier] contains no [Element]s. + */ + fun all(predicate: (Element<*>) -> Boolean): Boolean + + /** + * Concatenates this modifier with another. + * + * Returns a [Modifier] representing this modifier followed by [other] in sequence. + */ + infix fun then(other: Modifier): Modifier = + if (other === Modifier) this else CombinedModifier(this, other) + + /** + * A single element contained within a [Modifier] chain. + */ + interface Element> : Modifier { + override fun foldIn(initial: R, operation: (R, Element<*>) -> R): R = + operation(initial, this) + + override fun foldOut(initial: R, operation: (Element<*>, R) -> R): R = + operation(this, initial) + + override fun any(predicate: (Element<*>) -> Boolean): Boolean = predicate(this) + + override fun all(predicate: (Element<*>) -> Boolean): Boolean = predicate(this) + + fun mergeWith(other: Self): Self + + @Suppress("UNCHECKED_CAST") + private fun castSelf(other: Element<*>): Self = other as Self + + fun unsafeMergeWith(other: Element<*>) = mergeWith(castSelf(other)) + + /** + * Converts this modifier element to a debug [Component] representation. + */ + fun toComponent(): Component = Component.literal(toString()) + } + + /** + * The companion object `Modifier` is the empty, default, or starter [Modifier] + * that contains no [elements][Element]. Use it to create a new [Modifier] using + * modifier extension factory functions. + */ + // The companion object implements `Modifier` so that it may be used as the start of a + // modifier extension factory expression. + companion object : Modifier { + override fun foldIn(initial: R, operation: (R, Element<*>) -> R): R = initial + override fun foldOut(initial: R, operation: (Element<*>, R) -> R): R = initial + override fun any(predicate: (Element<*>) -> Boolean): Boolean = false + override fun all(predicate: (Element<*>) -> Boolean): Boolean = true + override infix fun then(other: Modifier): Modifier = other + override fun toString() = "Modifier" + } +} + +/** + * A node in a [Modifier] chain. A CombinedModifier always contains at least two elements; + * a Modifier [outer] that wraps around the Modifier [inner]. + */ +class CombinedModifier( + private val outer: Modifier, + private val inner: Modifier +) : Modifier { + override fun foldIn(initial: R, operation: (R, Modifier.Element<*>) -> R): R = + inner.foldIn(outer.foldIn(initial, operation), operation) + + override fun foldOut(initial: R, operation: (Modifier.Element<*>, R) -> R): R = + outer.foldOut(inner.foldOut(initial, operation), operation) + + override fun any(predicate: (Modifier.Element<*>) -> Boolean): Boolean = + outer.any(predicate) || inner.any(predicate) + + override fun all(predicate: (Modifier.Element<*>) -> Boolean): Boolean = + outer.all(predicate) && inner.all(predicate) + + override fun equals(other: Any?): Boolean = + other is CombinedModifier && outer == other.outer && inner == other.inner + + override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode() + + override fun toString() = "[" + foldIn("") { acc, element -> + if (acc.isEmpty()) element.toString() else "$acc, $element" + } + "]" +} + +/** + * Collects all [Modifier.Element] instances of type [T] from this modifier chain. + * + * @return A list of all modifier elements matching type [T], in declaration order. + */ +inline fun > Modifier.getAll(): List = + foldIn(mutableListOf()) { acc, element -> + if (element is T) acc.apply { add(element) } else acc + } \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnGloballyPositionedModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnGloballyPositionedModifier.kt new file mode 100644 index 000000000..2a377a67b --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnGloballyPositionedModifier.kt @@ -0,0 +1,33 @@ +package net.kernelpanicsoft.archie.gui.modifiers + +import net.kernelpanicsoft.archie.gui.layout.IntCoordinates + +/** + * A [Modifier.Element] that invokes [onGloballyPositioned] with the node's absolute on-screen + * coordinates whenever it is placed by [net.kernelpanicsoft.archie.gui.layout.LayoutNode.placeAt]. + * + * Multiple [OnGloballyPositionedModifier] elements on the same node are merged so that every + * callback in the chain fires, in declaration order, for each placement. + * + * @property merged Internal flag marking whether this instance already wraps other merged + * callbacks; set automatically by [mergeWith], not intended to be passed by callers. + * @property onGloballyPositioned Invoked with the node's absolute [IntCoordinates] on placement. + */ +class OnGloballyPositionedModifier( + val merged: Boolean = false, + val onGloballyPositioned: (IntCoordinates) -> Unit +) : Modifier.Element +{ + override fun mergeWith(other: OnGloballyPositionedModifier): OnGloballyPositionedModifier = OnGloballyPositionedModifier(merged = true) { position -> + if (!other.merged) + onGloballyPositioned(position) + other.onGloballyPositioned(position) + } + +} + +/** + * Registers [onGloballyPositioned] to be called with the node's absolute screen coordinates + * every time it is placed (e.g. on layout changes). + */ +fun Modifier.onGloballyPositioned(onGloballyPositioned: (IntCoordinates) -> Unit) = this then OnGloballyPositionedModifier(onGloballyPositioned = onGloballyPositioned) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnSizeChangedModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnSizeChangedModifier.kt new file mode 100644 index 000000000..23f63c8cf --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnSizeChangedModifier.kt @@ -0,0 +1,28 @@ +package net.kernelpanicsoft.archie.gui.modifiers + +import net.kernelpanicsoft.archie.gui.layout.Size + +/** + * A [Modifier.Element] that invokes [onSizeChanged] whenever the node's measured [Size] changes + * between layout passes. + * + * Multiple [OnSizeChangedModifier] elements on the same node are merged so that every callback + * in the chain fires, in declaration order, for each size change. + * + * @property merged Internal flag marking whether this instance already wraps other merged + * callbacks; set automatically by [mergeWith], not intended to be passed by callers. + * @property onSizeChanged Invoked with the node's new measured [Size]. + */ +class OnSizeChangedModifier( + val merged: Boolean = false, + val onSizeChanged: (Size) -> Unit +) : Modifier.Element { + override fun mergeWith(other: OnSizeChangedModifier) = OnSizeChangedModifier(merged = true) { size -> + if (!other.merged) + onSizeChanged(size) + other.onSizeChanged(size) + } +} + +/** Notifies callback of any size changes to element. */ +fun Modifier.onSizeChanged(onSizeChanged: (Size) -> Unit) = this then OnSizeChangedModifier(onSizeChanged = onSizeChanged) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/SizeModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/SizeModifier.kt new file mode 100644 index 000000000..2227ba2a7 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/SizeModifier.kt @@ -0,0 +1,132 @@ +package net.kernelpanicsoft.archie.gui.modifiers + +import androidx.compose.runtime.Stable +import kotlin.math.roundToInt + +/** + * A [Modifier.Element] that constrains the intrinsic size of a composable node by clamping + * the [Constraints] passed to it during measurement. + * + * Multiple [SizeModifier] elements on the same node are merged by intersecting their ranges, + * so the resulting constraints satisfy all modifiers simultaneously. + * + * Prefer the extension functions ([size], [sizeIn], [width], [height]) over constructing + * this class directly. + * + * @property constraints The [Constraints] to enforce. + */ +data class SizeModifier( + val constraints: Constraints +) : Modifier.Element, LayoutChangingModifier { + override fun mergeWith(other: SizeModifier) = with(constraints) { + SizeModifier( + Constraints( + other.constraints.minWidth.coerceIn(minWidth, maxWidth), + other.constraints.maxWidth.coerceIn(minWidth, maxWidth), + other.constraints.minHeight.coerceIn(minHeight, maxHeight), + other.constraints.maxHeight.coerceIn(minHeight, maxHeight), + ) + ) + } + + override fun modifyInnerConstraints(constraints: Constraints): Constraints { + return SizeModifier(constraints).mergeWith(this).constraints + } +} + +/** + * A [LayoutChangingModifier] that forces the node to fill a [percent] fraction of the + * available horizontal space. + * + * @property percent Fraction of available width to fill (0.0–1.0, default 1.0 = full width). + */ +data class HorizontalFillModifier( + val percent: Double +) : Modifier.Element, LayoutChangingModifier { + override fun mergeWith(other: HorizontalFillModifier) = other + + override fun modifyInnerConstraints(constraints: Constraints): Constraints { + val fillWidth = (constraints.minWidth + percent * (constraints.maxWidth - constraints.minWidth)).roundToInt() + return constraints.copy( + minWidth = fillWidth, + maxWidth = fillWidth + ) + } +} + +/** + * A [LayoutChangingModifier] that forces the node to fill a [percent] fraction of the + * available vertical space. + * + * @property percent Fraction of available height to fill (0.0–1.0, default 1.0 = full height). + */ +data class VerticalFillModifier( + val percent: Double +) : Modifier.Element, LayoutChangingModifier { + override fun mergeWith(other: VerticalFillModifier) = other + + override fun modifyInnerConstraints(constraints: Constraints): Constraints { + val fillHeight = + (constraints.minHeight + percent * (constraints.maxHeight - constraints.minHeight)).roundToInt() + return constraints.copy( + minHeight = fillHeight, + maxHeight = fillHeight + ) + } +} + +/** + * Forces the node to fill [percent] of the maximum available width. + * + * @param percent Fraction of available width (0.0–1.0). Default `1.0` fills all available width. + */ +@Stable +fun Modifier.fillMaxWidth(percent: Double = 1.0) = then(HorizontalFillModifier(percent)) + +/** + * Forces the node to fill [percent] of the maximum available height. + * + * @param percent Fraction of available height (0.0–1.0). Default `1.0` fills all available height. + */ +@Stable +fun Modifier.fillMaxHeight(percent: Double = 1.0) = then(VerticalFillModifier(percent)) + +/** + * Forces the node to fill [percent] of both the available width and height. + * + * @param percent Fraction of available space (0.0–1.0). Default `1.0` fills all available space. + */ +@Stable +fun Modifier.fillMaxSize(percent: Double = 1.0) = then(HorizontalFillModifier(percent)).then(VerticalFillModifier(percent)) + +/** + * Constrains the node's width and height to be within the given min/max bounds. + * + * @param minWidth Minimum width in pixels. + * @param maxWidth Maximum width in pixels. + * @param minHeight Minimum height in pixels. + * @param maxHeight Maximum height in pixels. + */ +@Stable +fun Modifier.sizeIn( + minWidth: Int = 0, + maxWidth: Int = Integer.MAX_VALUE, + minHeight: Int = 0, + maxHeight: Int = Integer.MAX_VALUE, +) = then(SizeModifier(Constraints(minWidth, maxWidth, minHeight, maxHeight))) + +/** Sets an exact fixed size of [width] × [height] pixels. */ +@Stable +fun Modifier.size(width: Int, height: Int) = sizeIn(width, width, height, height) + +/** Sets an exact fixed square size of [size] × [size] pixels. */ +@Stable +fun Modifier.size(size: Int) = size(size, size) + +/** Sets an exact fixed width of [width] pixels (height unconstrained). */ +@Stable +fun Modifier.width(width: Int) = sizeIn(width, width, 0, Integer.MAX_VALUE) + +/** Sets an exact fixed height of [height] pixels (width unconstrained). */ +@Stable +fun Modifier.height(height: Int) = sizeIn(0, Integer.MAX_VALUE, height, height) \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BackgroundModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BackgroundModifier.kt new file mode 100644 index 000000000..01e41c0b6 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BackgroundModifier.kt @@ -0,0 +1,97 @@ +package net.kernelpanicsoft.archie.gui.modifiers.appearance + +import androidx.compose.runtime.Stable +import net.kernelpanicsoft.archie.gui.modifiers.ContentDrawScope +import net.kernelpanicsoft.archie.gui.modifiers.DrawModifier +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.util.KColor +import net.kernelpanicsoft.archie.gui.util.extension.fillGradient +import net.kernelpanicsoft.archie.gui.util.extension.invoke + +/** + * The direction along which a background gradient transitions. + */ +enum class GradientDirection { + TOP_TO_BOTTOM, + RIGHT_TO_LEFT, + LEFT_TO_RIGHT, + BOTTOM_TO_TOP, +} + +/** + * A [DrawModifier] that fills a composable's background with a solid colour or a two-stop + * linear gradient. + * + * When [startColor] and [endColor] are equal the fill is solid; otherwise the two colours + * are interpolated across the node bounds in [gradientDirection]. + * + * @property startColor ARGB packed start colour. + * @property endColor ARGB packed end colour. + * @property gradientDirection The direction of the gradient transition. + */ +data class BackgroundModifier( + val startColor: Int, + val endColor: Int, + val gradientDirection: GradientDirection = GradientDirection.TOP_TO_BOTTOM, +) : Modifier.Element, DrawModifier { + + override fun ContentDrawScope.draw() { + guiGraphics { + val (topLeft, topRight, bottomLeft, bottomRight) = when (gradientDirection) + { + GradientDirection.TOP_TO_BOTTOM -> listOf(startColor, startColor, endColor, endColor) + GradientDirection.BOTTOM_TO_TOP -> listOf(endColor, endColor, startColor, startColor) + GradientDirection.LEFT_TO_RIGHT -> listOf(startColor, endColor, startColor, endColor) + GradientDirection.RIGHT_TO_LEFT -> listOf(endColor, startColor, endColor, startColor) + } + fillGradient(x, y, width, height, topLeft, topRight, bottomLeft, bottomRight) + } + drawContent() + } + + override fun mergeWith(other: BackgroundModifier): BackgroundModifier = other + + override fun toString(): String = + if (startColor == endColor) + "BackgroundModifier(color=#${String.format("%08X", startColor)})" + else + "BackgroundModifier(startColor=#${String.format("%08X", startColor)}, endColor=#${String.format("%08X", endColor)}, direction=$gradientDirection)" +} + +/** + * Fills the composable's background with a solid [color]. + */ +@Stable fun Modifier.background(color: KColor): Modifier = + this then BackgroundModifier(color.argb, color.argb) + +/** + * Fills the composable's background with a gradient from [startColor] to [endColor] + * going top-to-bottom. + */ +@Stable fun Modifier.background(startColor: KColor, endColor: KColor): Modifier = + this then BackgroundModifier(startColor.argb, endColor.argb) + +/** + * Fills the composable's background with a gradient from [startColor] to [endColor] + * in the given [gradientDirection]. + */ +@Stable fun Modifier.background( + startColor: KColor, + endColor: KColor, + gradientDirection: GradientDirection = GradientDirection.TOP_TO_BOTTOM, +): Modifier = this then BackgroundModifier(startColor.argb, endColor.argb, gradientDirection) + +/** Fills the composable's background with a solid ARGB integer [color]. */ +@Stable fun Modifier.background(color: Int): Modifier = + this then BackgroundModifier(color, color) + +/** Fills the composable's background with a gradient between two ARGB integer colours. */ +@Stable fun Modifier.background(startColor: Int, endColor: Int): Modifier = + this then BackgroundModifier(startColor, endColor) + +/** Fills the composable's background with a directional gradient between two ARGB integer colours. */ +@Stable fun Modifier.background( + startColor: Int, + endColor: Int, + gradientDirection: GradientDirection = GradientDirection.TOP_TO_BOTTOM, +): Modifier = this then BackgroundModifier(startColor, endColor, gradientDirection) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BorderModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BorderModifier.kt new file mode 100644 index 000000000..d2d76b076 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BorderModifier.kt @@ -0,0 +1,53 @@ +package net.kernelpanicsoft.archie.gui.modifiers.appearance + +import androidx.compose.runtime.Stable +import net.kernelpanicsoft.archie.gui.modifiers.ContentDrawScope +import net.kernelpanicsoft.archie.gui.modifiers.DrawModifier +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.util.KColor +import net.kernelpanicsoft.archie.gui.util.extension.drawRectOutline + +/** + * A [DrawModifier] that draws a rectangular border around a composable. + * + * The border is rendered **before** the composable's own content so that it appears + * underneath any child nodes. + * + * @property color ARGB packed border colour. + * @property thickness Border stroke width in pixels. + */ +data class BorderModifier( + val color: Int, + val thickness: Int, +) : Modifier.Element, DrawModifier { + + override fun mergeWith(other: BorderModifier): BorderModifier = other + + override fun ContentDrawScope.draw() { + guiGraphics.drawRectOutline(x, y, width, height, color, thickness) + drawContent() + } + + override fun toString(): String = + "BorderModifier(width=$thickness, color=#${String.format("%08X", color)})" +} + +/** + * Adds a border of [thickness] pixels and [color] to the composable. + * + * @param color The border colour. + * @param thickness The border stroke width in pixels (default 1). + */ +@Stable +fun Modifier.border(color: KColor, thickness: Int = 1): Modifier = + this then BorderModifier(color.argb, thickness) + +/** + * Adds a border using a raw ARGB integer [color]. + * + * @param color ARGB packed colour. + * @param thickness The border stroke width in pixels (default 1). + */ +@Stable +fun Modifier.border(color: Int, thickness: Int = 1): Modifier = + this then BorderModifier(color, thickness) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TextureModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TextureModifier.kt new file mode 100644 index 000000000..043eae1b8 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TextureModifier.kt @@ -0,0 +1,26 @@ +package net.kernelpanicsoft.archie.gui.modifiers.appearance + +import androidx.compose.runtime.Stable +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.minecraft.resources.ResourceLocation + +/** + * A [Modifier.Element] that overrides the texture used by certain theme-aware composables + * (such as [net.kernelpanicsoft.archie.gui.Slot]). + * + * Only the last applied [TextureModifier] on a node takes effect. + * + * @property texture The [ResourceLocation] of the replacement texture. + */ +data class TextureModifier(val texture: ResourceLocation) : Modifier.Element { + override fun mergeWith(other: TextureModifier): TextureModifier = + throw UnsupportedOperationException("TextureModifier cannot be merged; only one texture can be active at a time.") +} + +/** + * Overrides the texture of theme-aware composables with the given [ResourceLocation]. + * + * @param texture The replacement texture resource location. + */ +@Stable +fun Modifier.texture(texture: ResourceLocation): Modifier = this then TextureModifier(texture) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TooltipModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TooltipModifier.kt new file mode 100644 index 000000000..3cfe840ba --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TooltipModifier.kt @@ -0,0 +1,29 @@ +package net.kernelpanicsoft.archie.gui.modifiers.appearance + +import androidx.compose.runtime.Stable +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.minecraft.world.inventory.tooltip.TooltipComponent + +/** + * A [Modifier.Element] that attaches one or more [TooltipComponent]s to a composable. + * + * Multiple [TooltipModifier] elements on the same node are merged by concatenating their + * tooltip lists. + * + * @property tooltips The list of tooltip components to display. + */ +data class TooltipModifier(val tooltips: List) : Modifier.Element { + override fun mergeWith(other: TooltipModifier): TooltipModifier = + TooltipModifier(tooltips + other.tooltips) +} + +/** + * Attaches one or more [TooltipComponent]s to this composable. + * + * The tooltips are merged with any existing [TooltipModifier] on the node. + * + * @param tooltips The tooltip components to attach. + */ +@Stable +fun Modifier.tooltip(vararg tooltips: TooltipComponent): Modifier = + this then TooltipModifier(tooltips.toList()) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/InputEvent.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/InputEvent.kt new file mode 100644 index 000000000..bc64e163f --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/InputEvent.kt @@ -0,0 +1,112 @@ +package net.kernelpanicsoft.archie.gui.modifiers.input + +/** + * Base class for all events dispatched through the composable input system. + * + * Events propagate through the node tree from the innermost child outward. Once + * [consume] is called the propagation stops for most event types. The [bypassSuper] + * flag additionally controls whether the originating [net.minecraft.client.gui.screens.Screen] + * method forwards the event to its Minecraft `super` implementation. + */ +sealed class InputEvent { + /** + * Whether this event has been consumed by a handler. + * + * Consumed events do not continue propagating to outer nodes. + */ + internal var isConsumed: Boolean = false + private set + + /** + * When `true`, the screen method that triggered this event will bypass the Minecraft + * `super` call (i.e. return `true` to absorb the input at the screen level). + */ + internal var bypassSuper: Boolean = false + private set + + /** + * Marks this event as consumed, stopping further propagation. + * + * @param bypassSuperCall When `true`, the corresponding screen method will return `true` + * instead of delegating to the `super` implementation. Use this when the UI has fully + * handled a keyboard or mouse event and the vanilla logic should be suppressed. + */ + fun consume(bypassSuperCall: Boolean = false) { + isConsumed = true + bypassSuper = bypassSuperCall + } +} + +/** + * Base class for all pointer (mouse) events. + * + * @property type The specific kind of pointer interaction. + * @property mouseX The current cursor x position in screen pixels. + * @property mouseY The current cursor y position in screen pixels. + */ +sealed class PointerEvent( + val type: PointerEventType, + val mouseX: Double, + val mouseY: Double, +) : InputEvent() + +/** A basic pointer event carrying position and type information. */ +class BasicPointerEvent( + type: PointerEventType, + mouseX: Double, + mouseY: Double, +) : PointerEvent(type, mouseX, mouseY) + +/** + * A pointer event generated by mouse wheel movement. + * + * @property scrollX Horizontal scroll delta. + * @property scrollY Vertical scroll delta. + */ +class ScrollEvent( + type: PointerEventType = PointerEventType.SCROLL, + mouseX: Double, + mouseY: Double, + val scrollX: Double, + val scrollY: Double, +) : PointerEvent(type, mouseX, mouseY) + +/** + * A pointer event generated by click-dragging the mouse. + * + * @property button The mouse button held during the drag (0 = left, 1 = right, 2 = middle). + * @property dragX Horizontal drag delta since the last frame. + * @property dragY Vertical drag delta since the last frame. + */ +class DragEvent( + type: PointerEventType = PointerEventType.DRAG, + mouseX: Double, + mouseY: Double, + val button: Int, + val dragX: Double, + val dragY: Double, +) : PointerEvent(type, mouseX, mouseY) + +/** + * An event generated by a keyboard key press. + * + * @property keyCode The GLFW key code. + * @property scanCode The platform-specific scan code. + * @property modifiers Bitmask of active modifier keys (Shift / Ctrl / Alt). + */ +data class KeyEvent( + val keyCode: Int, + val scanCode: Int, + val modifiers: Int, +) : InputEvent() + +/** + * An event generated when a printable character is typed. + * + * @property codePoint The typed character as a Unicode code point. + * @property modifiers Bitmask of active modifier keys. + */ +data class CharEvent( + val codePoint: Char, + val modifiers: Int, +) : InputEvent() diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnCharTypedModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnCharTypedModifier.kt new file mode 100644 index 000000000..afb4bb0db --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnCharTypedModifier.kt @@ -0,0 +1,34 @@ +package net.kernelpanicsoft.archie.gui.modifiers.input + +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.modifiers.Modifier + +/** + * A [Modifier.Element] that registers a character-typed handler on a composable node. + * + * Multiple [OnCharTypedModifier] elements on the same node are **chained**: earlier handlers + * fire first, and later handlers fire only if the event was not yet consumed. + * + * @property onEvent The callback invoked with (node, [CharEvent]) when a character is typed. + */ +data class OnCharTypedModifier( + val onEvent: (UINode, CharEvent) -> Unit, +) : Modifier.Element { + + override fun mergeWith(other: OnCharTypedModifier): OnCharTypedModifier = + OnCharTypedModifier { node, event -> + onEvent(node, event) + if (!event.isConsumed) other.onEvent(node, event) + } +} + +/** + * Registers a character-typed handler on this composable. + * + * Called when the user types a printable character while the node (or a descendant) + * participates in key event dispatch. + * + * @param onEvent Callback invoked with (node, [CharEvent]). + */ +fun Modifier.onCharTyped(onEvent: (UINode, CharEvent) -> Unit): Modifier = + this then OnCharTypedModifier(onEvent) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnKeyEventModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnKeyEventModifier.kt new file mode 100644 index 000000000..c6a19df7c --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnKeyEventModifier.kt @@ -0,0 +1,36 @@ +package net.kernelpanicsoft.archie.gui.modifiers.input + +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.modifiers.Modifier + +/** + * A [Modifier.Element] that registers a keyboard key-press handler on a composable node. + * + * Multiple [OnKeyEventModifier] elements on the same node are **chained**: the earlier + * handler fires first, and the later handler fires only if the event was not yet consumed. + * + * @property onEvent The callback invoked with (node, [KeyEvent]) on a key press. + */ +data class OnKeyEventModifier( + val onEvent: (UINode, KeyEvent) -> Unit, +) : Modifier.Element { + + override fun mergeWith(other: OnKeyEventModifier): OnKeyEventModifier = + OnKeyEventModifier { node, event -> + onEvent(node, event) + if (!event.isConsumed) other.onEvent(node, event) + } + + override fun toString(): String = "OnKeyEventModifier()" +} + +/** + * Registers a key-press handler on this composable. + * + * The handler is called when a keyboard key is pressed while the node (or a descendant) + * holds focus in the key event dispatch chain. + * + * @param onEvent Callback invoked with (node, [KeyEvent]). + */ +fun Modifier.onKeyEvent(onEvent: (UINode, KeyEvent) -> Unit): Modifier = + this then OnKeyEventModifier(onEvent) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnPointerEventModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnPointerEventModifier.kt new file mode 100644 index 000000000..63a27be46 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnPointerEventModifier.kt @@ -0,0 +1,150 @@ +package net.kernelpanicsoft.archie.gui.modifiers.input + +import androidx.compose.runtime.Stable +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.kernelpanicsoft.archie.gui.modifiers.Modifier + +/** Identifies the type of pointer (mouse) interaction that triggers an event handler. */ +enum class PointerEventType { + /** The primary or secondary mouse button was pressed inside the node's bounds. */ + PRESS, + /** + * A mouse button was pressed anywhere on the screen regardless of bounds. + * Useful for detecting clicks outside a focused element. + */ + GLOBAL_PRESS, + /** A mouse button was released inside the node's bounds. */ + RELEASE, + /** A mouse button was released anywhere on the screen. */ + GLOBAL_RELEASE, + /** The mouse cursor moved while inside the node's bounds. */ + MOVE, + /** The mouse cursor entered the node's bounds from outside. */ + ENTER, + /** The mouse cursor left the node's bounds. */ + EXIT, + /** The mouse wheel was scrolled over the node. */ + SCROLL, + /** The mouse wheel was scrolled anywhere on the screen. */ + GLOBAL_SCROLL, + /** The mouse was dragged (button held + moved) over the node. */ + DRAG, + /** The mouse was dragged anywhere on the screen. */ + GLOBAL_DRAG, +} + +/** Milliseconds a press must be held to qualify as a long-click. */ +const val LONG_CLICK_THRESHOLD = 500 + +/** Milliseconds within which two successive presses qualify as a double-click. */ +const val DOUBLE_CLICK_THRESHOLD = 300 + +/** + * A [Modifier.Element] that registers a callback for a specific [PointerEventType]. + * + * When multiple [OnPointerEventModifier] elements with the same [eventType] exist on a node, + * the **last** one replaces earlier ones (they do not chain). Use [combinedClickable] if + * you need multiple click behaviours on a single node. + * + * @param T The concrete [UINode] subtype the handler expects. + * @param eventType The pointer event that triggers [onEvent]. + * @param onEvent The handler invoked with the receiving node and the event. + */ +data class OnPointerEventModifier( + val eventType: PointerEventType, + val onEvent: (T, PointerEvent) -> Unit, +) : Modifier.Element> { + override fun mergeWith(other: OnPointerEventModifier<*>): OnPointerEventModifier<*> = other + override fun toString(): String = "OnPointerEventModifier(eventType=${eventType.name})" +} + +/** + * Registers a handler for the given pointer [type] on this composable. + * + * @param T The expected [UINode] subtype; use [UINode] for the generic case. + * @param type The [PointerEventType] to listen for. + * @param onEvent The callback invoked with (node, event) when the event occurs. + */ +@Stable +fun Modifier.onPointerEvent( + type: PointerEventType, + onEvent: (T, PointerEvent) -> Unit, +): Modifier = this then OnPointerEventModifier(type, onEvent) + +/** + * Registers a scroll event handler on this composable. + * + * @param global When `true`, the handler fires for scroll events anywhere on screen. + * @param onScrollEvent The callback invoked with (node, [ScrollEvent]). + */ +@Suppress("UNCHECKED_CAST") +@Stable +fun Modifier.onScroll( + global: Boolean = false, + onScrollEvent: (T, ScrollEvent) -> Unit, +): Modifier = this then OnPointerEventModifier( + if (global) PointerEventType.GLOBAL_SCROLL else PointerEventType.SCROLL, + onScrollEvent as (T, PointerEvent) -> Unit, +) + +/** + * Registers a drag event handler on this composable. + * + * @param global When `true`, the handler fires for drag events anywhere on screen. + * @param onDragEvent The callback invoked with (node, [DragEvent]). + */ +@Suppress("UNCHECKED_CAST") +@Stable +fun Modifier.onDrag( + global: Boolean = false, + onDragEvent: (T, DragEvent) -> Unit, +): Modifier = this then OnPointerEventModifier( + if (global) PointerEventType.GLOBAL_DRAG else PointerEventType.DRAG, + onDragEvent as (T, PointerEvent) -> Unit, +) + +/** + * Adds multiple click-type handlers to a composable in a single modifier. + * + * At least one of the three callbacks must be non-null. + * + * @param onLongClick Invoked when the node is held for more than [LONG_CLICK_THRESHOLD] ms. + * @param onDoubleClick Invoked when two presses occur within [DOUBLE_CLICK_THRESHOLD] ms. + * @param onClick Invoked on a normal single click (mouse release). + */ +@Stable +fun Modifier.combinedClickable( + onLongClick: ((T, PointerEvent) -> Unit)? = null, + onDoubleClick: ((T, PointerEvent) -> Unit)? = null, + onClick: ((T, PointerEvent) -> Unit)? = null, +): Modifier { + require(onClick != null || onLongClick != null || onDoubleClick != null) { + "You must specify at least one click handler" + } + var mod = this + + if (onLongClick != null) { + var clickStart = 0L + mod = mod + .onPointerEvent(PointerEventType.PRESS) { _, _ -> clickStart = System.currentTimeMillis() } + .onPointerEvent(PointerEventType.RELEASE) { node, event -> + if (clickStart != 0L && (System.currentTimeMillis() - clickStart) > LONG_CLICK_THRESHOLD) { + clickStart = 0L + onLongClick(node, event) + } + } + } + if (onDoubleClick != null) { + var clickStart = 0L + mod = mod.onPointerEvent(PointerEventType.PRESS) { node, event -> + if (clickStart != 0L && (System.currentTimeMillis() - clickStart) < DOUBLE_CLICK_THRESHOLD) { + clickStart = 0L + return@onPointerEvent onDoubleClick(node, event) + } + clickStart = System.currentTimeMillis() + } + } + if (onClick != null) mod = mod.onPointerEvent(PointerEventType.RELEASE, onClick) + + return mod +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/MarginModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/MarginModifier.kt new file mode 100644 index 000000000..436839603 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/MarginModifier.kt @@ -0,0 +1,90 @@ +package net.kernelpanicsoft.archie.gui.modifiers.position + +import androidx.compose.runtime.Stable +import net.kernelpanicsoft.archie.gui.layout.IntCoordinates +import net.kernelpanicsoft.archie.gui.modifiers.LayoutChangingModifier +import net.kernelpanicsoft.archie.gui.modifiers.Modifier + +/** + * Holds the four-sided margin values used by [MarginModifier]. + * + * @property left Left margin in pixels. + * @property right Right margin in pixels. + * @property top Top margin in pixels. + * @property bottom Bottom margin in pixels. + */ +data class MarginValues( + val left: Int = 0, + val right: Int = 0, + val top: Int = 0, + val bottom: Int = 0, +) { + /** Returns the top-left corner offset (left, top) as an [IntCoordinates]. */ + fun getOffset(): IntCoordinates = IntCoordinates(left, top) + + operator fun plus(other: MarginValues): MarginValues = MarginValues( + left + other.left, right + other.right, top + other.top, bottom + other.bottom, + ) +} + +/** + * A [Modifier.Element] that adds outer spacing (margin) around a composable. + * + * Margins are applied **outside** the node bounds and are accumulated additively when + * multiple [MarginModifier] elements are chained. + * + * @property margin The [MarginValues] describing each side's margin. + */ +data class MarginModifier(val margin: MarginValues) : Modifier.Element, LayoutChangingModifier { + override fun mergeWith(other: MarginModifier): MarginModifier = MarginModifier(margin + other.margin) + + /** Total horizontal margin (left + right). */ + val horizontal get() = margin.left + margin.right + + /** Total vertical margin (top + bottom). */ + val vertical get() = margin.top + margin.bottom + + override fun modifyPosition(offset: IntCoordinates): IntCoordinates = offset + margin.getOffset() + + override fun toString(): String = buildString { + append("MarginModifier(") + val sides = buildList { + if (margin.left != 0) add("left=${margin.left}") + if (margin.right != 0) add("right=${margin.right}") + if (margin.top != 0) add("top=${margin.top}") + if (margin.bottom != 0) add("bottom=${margin.bottom}") + } + append(sides.joinToString(", ")) + append(")") + } +} + +/** + * Adds independent per-side margins around this composable. + * + * @param left Left margin in pixels. + * @param right Right margin in pixels. + * @param top Top margin in pixels. + * @param bottom Bottom margin in pixels. + */ +@Stable +fun Modifier.margin(left: Int = 0, right: Int = 0, top: Int = 0, bottom: Int = 0): Modifier = + this then MarginModifier(MarginValues(left, right, top, bottom)) + +/** + * Adds symmetric horizontal and vertical margins. + * + * @param horizontal Margin applied to both the left and right sides. + * @param vertical Margin applied to both the top and bottom sides. + */ +@Stable +fun Modifier.margin(horizontal: Int = 0, vertical: Int = 0): Modifier = + margin(horizontal, horizontal, vertical, vertical) + +/** + * Adds a uniform margin on all four sides. + * + * @param all The margin in pixels applied to every side. + */ +@Stable +fun Modifier.margin(all: Int = 0): Modifier = margin(all, all, all, all) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/OffsetModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/OffsetModifier.kt new file mode 100644 index 000000000..a3536ebe7 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/OffsetModifier.kt @@ -0,0 +1,34 @@ +package net.kernelpanicsoft.archie.gui.modifiers.position + +import androidx.compose.runtime.Stable +import net.kernelpanicsoft.archie.gui.layout.IntOffset +import net.kernelpanicsoft.archie.gui.modifiers.LayoutChangingModifier +import net.kernelpanicsoft.archie.gui.modifiers.Modifier + +/** + * A [Modifier.Element] that shifts a composable's position by a fixed pixel offset after + * layout has been computed. + * + * The offset is applied on top of any position assigned by the parent layout; it does not + * affect the parent's size calculation. + * + * Only the **last** [OffsetModifier] in a chain takes effect. + * + * @property offset The pixel offset to apply as an [IntOffset] value. + */ +data class OffsetModifier(val offset: IntOffset) : Modifier.Element, LayoutChangingModifier { + override fun mergeWith(other: OffsetModifier): OffsetModifier = other + + override fun modifyPosition(offset: IntOffset): IntOffset = offset + this.offset +} + +/** + * Shifts the composable by ([x], [y]) pixels after layout. + * + * The shift does not affect the space reserved for the composable in its parent layout. + * + * @param x Horizontal pixel offset (positive moves right). + * @param y Vertical pixel offset (positive moves down). + */ +@Stable +fun Modifier.offset(x: Int = 0, y: Int = 0): Modifier = this then OffsetModifier(IntOffset(x, y)) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/PaddingModifier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/PaddingModifier.kt new file mode 100644 index 000000000..5db12d5f3 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/PaddingModifier.kt @@ -0,0 +1,96 @@ +package net.kernelpanicsoft.archie.gui.modifiers.position + +import androidx.compose.runtime.Stable +import net.kernelpanicsoft.archie.gui.layout.IntCoordinates +import net.kernelpanicsoft.archie.gui.modifiers.Constraints +import net.kernelpanicsoft.archie.gui.modifiers.LayoutChangingModifier +import net.kernelpanicsoft.archie.gui.modifiers.Modifier + +/** + * Holds the four-sided padding values used by [PaddingModifier]. + * + * @property left Left padding in pixels. + * @property right Right padding in pixels. + * @property top Top padding in pixels. + * @property bottom Bottom padding in pixels. + */ +data class PaddingValues( + val left: Int = 0, + val right: Int = 0, + val top: Int = 0, + val bottom: Int = 0, +) { + /** Returns the top-left corner offset (left, top) as an [IntCoordinates]. */ + fun getOffset(): IntCoordinates = IntCoordinates(left, top) + + operator fun plus(other: PaddingValues): PaddingValues = PaddingValues( + left + other.left, right + other.right, top + other.top, bottom + other.bottom, + ) +} + +/** + * A [Modifier.Element] that adds inner spacing (padding) inside a composable. + * + * Padding is applied **inside** the node bounds and reduces the available space for children. + * + * @property padding The [PaddingValues] describing each side's padding. + */ +data class PaddingModifier(val padding: PaddingValues) : Modifier.Element, LayoutChangingModifier { + override fun mergeWith(other: PaddingModifier): PaddingModifier = PaddingModifier(padding + other.padding) + + /** Total horizontal padding (left + right). */ + val horizontal get() = padding.left + padding.right + + /** Total vertical padding (top + bottom). */ + val vertical get() = padding.top + padding.bottom + + override fun modifyInnerConstraints(constraints: Constraints): Constraints = + constraints.copy( + maxWidth = (constraints.maxWidth - horizontal).coerceAtLeast(0), + maxHeight = (constraints.maxHeight - vertical).coerceAtLeast(0), + minWidth = (constraints.minWidth - horizontal).coerceAtLeast(0), + minHeight = (constraints.minHeight - vertical).coerceAtLeast(0), + ) + + override fun toString(): String = buildString { + append("PaddingModifier(") + val sides = buildList { + if (padding.left != 0) add("left=${padding.left}") + if (padding.right != 0) add("right=${padding.right}") + if (padding.top != 0) add("top=${padding.top}") + if (padding.bottom != 0) add("bottom=${padding.bottom}") + } + append(sides.joinToString(", ")) + append(")") + } +} + +/** + * Adds independent per-side padding inside this composable. + * + * @param left Left padding in pixels. + * @param right Right padding in pixels. + * @param top Top padding in pixels. + * @param bottom Bottom padding in pixels. + */ +@Stable +fun Modifier.padding(left: Int = 0, right: Int = 0, top: Int = 0, bottom: Int = 0): Modifier = + this then PaddingModifier(PaddingValues(left, right, top, bottom)) + +/** + * Adds symmetric horizontal and vertical padding. + * + * @param horizontal Padding applied to both the left and right sides. + * @param vertical Padding applied to both the top and bottom sides. + */ +@Stable +fun Modifier.padding(horizontal: Int = 0, vertical: Int = 0): Modifier = + padding(horizontal, horizontal, vertical, vertical) + +/** + * Adds uniform padding on all four sides. + * + * @param all The padding in pixels applied to every side. + */ +@Stable +fun Modifier.padding(all: Int = 0): Modifier = padding(all, all, all, all) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/ZIndex.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/ZIndex.kt new file mode 100644 index 000000000..c60dcbf8a --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/ZIndex.kt @@ -0,0 +1,33 @@ +package net.kernelpanicsoft.archie.gui.modifiers.position + +import androidx.compose.runtime.Stable +import net.kernelpanicsoft.archie.gui.modifiers.Modifier + +/** + * A [Modifier.Element] that controls the rendering and input-dispatch order of a composable + * relative to its siblings. + * + * A higher [zIndex] causes the node to be drawn on top of siblings with lower z-indices and + * to receive input events first. The effective depth is accumulated hierarchically: each + * node's z-index is added to its parent's computed depth. + * + * When multiple [ZIndexModifier] elements are chained on the same node, the **last** one wins. + * + * @property zIndex The z-index value. Positive values move the node towards the viewer. + */ +data class ZIndexModifier(val zIndex: Float) : Modifier.Element { + /** When multiple z-index modifiers exist on the same node, the last one always wins. */ + override fun mergeWith(other: ZIndexModifier): ZIndexModifier = other + + override fun toString(): String = "ZIndexModifier(zIndex=$zIndex)" +} + +/** + * Sets the rendering depth of this composable relative to its siblings. + * + * Higher values appear on top and receive input events before lower-valued siblings. + * + * @param zIndex The z-index to apply. + */ +@Stable +fun Modifier.zIndex(zIndex: Float): Modifier = this then ZIndexModifier(zIndex) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/LayoutNodeApplier.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/LayoutNodeApplier.kt new file mode 100644 index 000000000..70561f268 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/LayoutNodeApplier.kt @@ -0,0 +1,42 @@ +package net.kernelpanicsoft.archie.gui.nodes + +import androidx.compose.runtime.AbstractApplier +import net.kernelpanicsoft.archie.gui.layout.LayoutNode + +/** + * Compose [AbstractApplier] that materializes composed UI into the [LayoutNode] tree rooted + * at [root]. Used as the applier for each [net.kernelpanicsoft.archie.gui.layer.Layer]'s + * [androidx.compose.runtime.Composition]. + */ +class LayoutNodeApplier(root: LayoutNode) : AbstractApplier(root) { + override fun insertTopDown(index: Int, instance: LayoutNode) { + // Ignored, we insert bottom-up. + } + + override fun insertBottomUp(index: Int, instance: LayoutNode) { + current.children.add(index, instance) + current.invalidateChildrenZCache() + check(instance.parent == null) { + "$instance must not have a parent when being inserted." + } + instance.parent = current + } + + override fun remove(index: Int, count: Int) { + repeat(count) { + current.children.removeAt(index).parent = null + } + current.invalidateChildrenZCache() + } + + override fun move(from: Int, to: Int, count: Int) { + current.children.move(from, to, count) + current.invalidateChildrenZCache() + } + + override fun onClear() { + current.children.forEach { it.parent = null } + current.children.clear() + current.invalidateChildrenZCache() + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/UINode.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/UINode.kt new file mode 100644 index 000000000..e066a0da1 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/UINode.kt @@ -0,0 +1,57 @@ +package net.kernelpanicsoft.archie.gui.nodes + +import net.kernelpanicsoft.archie.gui.layout.LayoutNode +import net.kernelpanicsoft.archie.gui.layout.MeasurePolicy +import net.kernelpanicsoft.archie.gui.layout.Renderer +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.minecraft.client.gui.GuiGraphics + +/** + * A node in Archie's Compose-based UI tree, exposing the layout/render state a node needs + * regardless of its concrete representation. Implemented by [LayoutNode], the tree node type + * produced by [LayoutNodeApplier]. + */ +interface UINode { + /** Determines how this node measures and places its children. */ + var measurePolicy: MeasurePolicy + + /** Draws this node's own content (not its children) each frame. */ + var renderer: Renderer + + /** The chained [Modifier] applied to this node. */ + var modifier: Modifier + + /** This node's measured width, in pixels. */ + var width: Int + + /** This node's measured height, in pixels. */ + var height: Int + + /** This node's placed x position, in pixels, relative to its parent. */ + var x: Int + + /** This node's placed y position, in pixels, relative to its parent. */ + var y: Int + + /** + * The [net.kernelpanicsoft.archie.gui.composables.theme.TextureStates] key a stateful + * [Renderer] most recently selected to draw (e.g. `"hovered"`, `"clicked_and_hovered"`), + * or `null` for nodes that don't render theme-state-driven visuals. + * + * Set by the [Renderer] itself, purely as a test hook - lets a client GameTest assert which + * visual state a component resolved to without pixel comparison. Not read by the framework. + */ + var renderState: String? + + /** + * Renders this node and its subtree at the given absolute screen position. + * + * @param x Absolute screen x position to render at. + * @param y Absolute screen y position to render at. + * @param guiGraphics The graphics context to draw with. + * @param mouseX Current mouse x position, in screen space. + * @param mouseY Current mouse y position, in screen space. + * @param partialTick Fractional tick time for this frame, for smooth animation. + */ + fun render(x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt new file mode 100644 index 000000000..57d3be9d5 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt @@ -0,0 +1,21 @@ +package net.kernelpanicsoft.archie.gui.render + +import net.minecraft.client.renderer.texture.TextureAtlasSprite +import net.minecraft.world.level.material.Fluid + +/** + * Cross-loader lookup of a [Fluid]'s client-rendering appearance, backed by an `actual` per mod + * loader - Fabric's `FluidRenderHandlerRegistry` and NeoForge's `IClientFluidTypeExtensions` + * expose the same information through unrelated APIs, so [net.kernelpanicsoft.archie.gui.composables.basic.FluidTank] + * goes through this instead of touching either directly. + * + * Client-only; only ever called from GUI rendering code. + */ +expect object AFluidRenderPlatform +{ + /** The fluid's still-texture sprite from the blocks atlas, or `null` if it can't be resolved. */ + fun getStillSprite(fluid: Fluid): TextureAtlasSprite? + + /** The ARGB tint color applied over [getStillSprite]'s sprite (`0xFFFFFFFF` = no tint). */ + fun getTintColor(fluid: Fluid): Int +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/ComposableTheme.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/ComposableTheme.kt new file mode 100644 index 000000000..cf7feb180 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/ComposableTheme.kt @@ -0,0 +1,287 @@ +package net.kernelpanicsoft.archie.gui.theme + +import dev.architectury.registry.ReloadListenerRegistry +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates +import net.kernelpanicsoft.archie.gui.layout.Size +import net.kernelpanicsoft.archie.resourcepacks.SerializationReloadListener +import net.kernelpanicsoft.archie.serialization.SerializationManager +import net.kernelpanicsoft.archie.serialization.serializers.SResourceLocation +import net.minecraft.resources.ResourceLocation +import net.minecraft.server.packs.resources.PreparableReloadListener +import net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.util.profiling.ProfilerFiller + +/* ─────────────────────── Theme state data classes ─────────────────────── */ + +/** Sealed base for composable theme states rendered as sprites. */ +@Serializable +sealed interface ThemeState { + /** Resource location of the sprite atlas or source image. */ + val texture: SResourceLocation + /** Full pixel size of the source image when UV rendering is used. */ + @SerialName("texture_size") val textureSize: Size + /** Horizontal UV offset within the atlas. */ + val u: Int + /** Vertical UV offset within the atlas. */ + val v: Int +} + +/** + * A fixed-size sprite slice within a sprite atlas or source image. + * + * @property width Rendered width in pixels. + * @property height Rendered height in pixels. + * @property uWidth Width of the source region in the atlas. + * @property vHeight Height of the source region in the atlas. + */ +@Serializable +data class SimpleThemeState( + override val texture: SResourceLocation, + @SerialName("texture_size") override val textureSize: Size, + override val u: Int = 0, + override val v: Int = 0, + val width: Int, + val height: Int, + @SerialName("u_width") val uWidth: Int, + @SerialName("v_height") val vHeight: Int, +) : ThemeState + +/** A map of state-name → [ThemeState] for a single variant of a composable. */ +@Serializable +data class StatefulTheme(val states: Map) + +/** Raw JSON shape of a theme file, deserialized as-is and resolved by [ThemeResourceListener] into a [ComposableTheme]. */ +@Serializable +data class RawComposableTheme( + val states: Map = emptyMap(), + val variants: Map> = emptyMap(), +) + +/** Raw JSON shape of a single theme state; fields left `null` inherit from the state's `"default"` entry. */ +@Serializable +data class RawThemeState( + val texture: String? = null, + @SerialName("texture_size") val textureSize: Size? = null, + val u: Int? = null, + val v: Int? = null, + val width: Int? = null, + val height: Int? = null, + @SerialName("u_width") val uWidthSnake: Int? = null, + @SerialName("v_height") val vHeightSnake: Int? = null, + val uWidth: Int? = null, + val vHeight: Int? = null, +) + +@Serializable +private data class GuiTextureMetadata( + val gui: GuiMetadataSection? = null, +) + +@Serializable +private data class GuiMetadataSection( + val scaling: GuiScalingMetadata? = null, +) + +@Serializable +private data class GuiScalingMetadata( + val type: String? = null, +) + +/** + * The full theme definition for a single composable type (e.g. `button`, `slot`). + * + * Contains base [states] and optional named [variants] (e.g. `"dark"`). + * + * @property isNineslice Whether [states]' default texture is nine-slice scaled, per its + * `.mcmeta` sprite metadata. When `false`, composables using this theme get a minimum + * size matching the sprite's own pixel dimensions instead of stretching arbitrarily. + * @property states Base state map (always contains at least `"default"`). + * @property variants Named variant overrides (e.g. `"dark"` → its own state map). + */ +@Serializable +data class ComposableTheme( + val isNineslice: Boolean = false, + val states: Map, + val variants: Map = emptyMap(), +) { + companion object { + /** + * Retrieves the [ComposableTheme] for [loc] from the loaded registry. + * + * @throws IllegalStateException if no theme was loaded for [loc]. + */ + operator fun get(loc: ResourceLocation): ComposableTheme = + ThemeResourceListener.COMPOSABLES[loc] + ?: throw IllegalStateException("No theme found for composable: $loc") + } + + /** + * Returns the [ThemeState] for [stateName] in [variantName], falling back to the base + * state map and ultimately the `"default"` state. + */ + @Suppress("NOTHING_TO_INLINE") + inline fun getState(stateName: String, variantName: String): ThemeState = + variants[variantName]?.states?.get(stateName) + ?: states[stateName] + ?: states[TextureStates.DEFAULT]!! + + /** + * Returns `true` if [stateName] exists in [variantName] or the base state map. + */ + @Suppress("NOTHING_TO_INLINE") + inline fun hasState(stateName: String, variantName: String?): Boolean = + (variantName?.let { variants[it] }?.states?.get(stateName) ?: states[stateName]) != null +} + +/* ─────────────────────── Reload listener ─────────────────────── */ + +/** + * A client resource-reload listener that loads [ComposableTheme] definitions from + * `assets//archie_themes/` directories in resource packs. + * + * Theme files are JSON objects matching the structure of [ComposableTheme]. Register via + * [ReloadListenerRegistry.register] during `initClient()`. + * + * ### File format + * ```json + * { + * "states": { + * "default": { + * "texture": "archie:java/button", + * "texture_size": { "width": 64, "height": 64 }, + * "width": 64, + * "height": 20 + * }, + * "hovered": { "texture": "archie:java/button_highlighted" } + * } + * } + * ``` + */ +class ThemeResourceListener : + SerializationReloadListener( + format = SerializationManager.json, + serializer = RawComposableTheme.serializer(), + directory = "archie_themes", + fileExtension = ".json", + ), + PreparableReloadListener { + + companion object { + /** All registered [ComposableTheme]s keyed by their [ResourceLocation]. */ + internal val COMPOSABLES = mutableMapOf() + } + + /** Excludes `*.theme.json` [ThemeManifest] files, which this listener does not parse. */ + override fun shouldLoadResource(fileLocation: ResourceLocation): Boolean = + !fileLocation.path.endsWith(".theme.json") + + override fun apply( + objs: Map, + resourceManager: ResourceManager, + profiler: ProfilerFiller, + ) { + COMPOSABLES.clear() + for ((location, root) in objs) { + if (location.path.endsWith(".theme")) continue + try { + val statesObj = root.states + if (statesObj.isEmpty()) { + throw IllegalStateException("Theme must have a valid states object: $location") + } + val defaultObj = statesObj[TextureStates.DEFAULT] + ?: throw IllegalStateException("Theme must have a \"default\" state: $location") + + val defaultState = parseSimple(location, "default", defaultObj) + + val states = mutableMapOf() + for ((name, stateEl) in statesObj) { + states[name] = parseSimple(location, name, stateEl, defaultState) + } + + val variants = mutableMapOf() + root.variants.forEach { (variantName, variantEl) -> + val vs = mutableMapOf() + variantEl.forEach { (sName, sEl) -> + vs[sName] = parseSimple(location, sName, sEl, defaultState) + } + variants[variantName] = StatefulTheme(vs) + } + + val isNineslice = resourceManager.isNineSliceTexture(defaultState.texture) + COMPOSABLES[location] = ComposableTheme(isNineslice, states, variants) + Archie.LOGGER.info( + "Theme \"{}\" loaded ({} states, {} variants, nineslice={})", + location, states.size, variants.size, isNineslice, + ) + } catch (e: Exception) { + Archie.LOGGER.warn("Error processing theme at {}: {}", location, e.message, e) + } + } + } + + private data class BaseFields( + val texture: ResourceLocation, + val textureSize: Size, + val u: Int, + val v: Int, + ) + + private fun baseFields(loc: ResourceLocation, name: String, el: RawThemeState, default: ThemeState?): BaseFields { + val texture = el.texture?.let { ResourceLocation.parse(it) } + ?: default?.texture + ?: throw IllegalStateException("Missing texture for state \"$name\" in: $loc") + val textureSize = el.textureSize + ?: default?.textureSize + ?: throw IllegalStateException("Missing texture_size for state \"$name\" in: $loc") + return BaseFields( + texture = texture, + textureSize = textureSize, + u = el.u ?: default?.u ?: 0, + v = el.v ?: default?.v ?: 0, + ) + } + + + private fun parseSimple(loc: ResourceLocation, name: String, el: RawThemeState, default: SimpleThemeState? = null): SimpleThemeState { + val base = baseFields(loc, name, el, default) + val width = el.width ?: default?.width ?: throw IllegalStateException("Missing width for state \"$name\" in: $loc") + val height = el.height ?: default?.height ?: throw IllegalStateException("Missing height for state \"$name\" in: $loc") + return SimpleThemeState( + base.texture, + base.textureSize, + base.u, + base.v, + width, height, + el.uWidthSnake ?: el.uWidth ?: default?.uWidth ?: width, + el.vHeightSnake ?: el.vHeight ?: default?.vHeight ?: height, + ) + } + + private fun ResourceManager.isNineSliceTexture(texture: ResourceLocation): Boolean { + val candidates = if (texture.path.startsWith("textures/") && texture.path.endsWith(".png")) { + listOf(ResourceLocation.fromNamespaceAndPath(texture.namespace, "${texture.path}.mcmeta")) + } else { + listOf( + ResourceLocation.fromNamespaceAndPath(texture.namespace, "textures/gui/sprites/${texture.path}.png.mcmeta"), + ResourceLocation.fromNamespaceAndPath(texture.namespace, "textures/${texture.path}.png.mcmeta"), + ) + } + + for (candidate in candidates) { + val resource = getResource(candidate).orElse(null) ?: continue + try { + resource.openAsReader().use { reader -> + val metadata = SerializationManager.json.decodeFromString(reader.readText()) + val type = metadata.gui?.scaling?.type + if (type == "nine_slice") return true + } + } catch (_: Exception) { + // Ignore malformed metadata and continue trying other candidates. + } + } + return false + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/Theme.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/Theme.kt new file mode 100644 index 000000000..ecc8dd081 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/Theme.kt @@ -0,0 +1,193 @@ +package net.kernelpanicsoft.archie.gui.theme + +import androidx.compose.runtime.Composable +import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.compositionLocalOf +import kotlinx.serialization.SerialName +import kotlinx.serialization.Serializable +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.resourcepacks.SerializationReloadListener +import net.kernelpanicsoft.archie.serialization.SerializationManager +import net.kernelpanicsoft.archie.gui.util.KColor +import net.kernelpanicsoft.archie.util.div +import net.kernelpanicsoft.archie.util.rem +import net.minecraft.resources.ResourceLocation +import net.minecraft.server.packs.resources.PreparableReloadListener +import net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.util.profiling.ProfilerFiller + +/** + * Constants for the built-in theme variant names. + * + * Pass these as the `mode` parameter of [ThemeData] to switch between light and dark variants. + */ +object ThemeVariants { + /** The default (light) theme variant. */ + const val DEFAULT = "" + /** The dark theme variant. */ + const val DARK = "dark" +} + +/** + * Resource-pack-defined metadata for a theme (namespace + type), declaring which variant + * names are valid and how requested modes should resolve to them. + * + * @property variants The set of variant names this theme actually defines resources for. + * @property defaultVariant The variant to fall back to when a requested mode isn't in [variants]. + * @property aliases Maps a requested mode name to a canonical variant name before + * validating it against [variants] (e.g. letting a pack expose `"night"` as an alias for `"dark"`). + */ +@Serializable +data class ThemeManifest( + val variants: Set = setOf(ThemeVariants.DEFAULT), + @SerialName("default_variant") val defaultVariant: String = ThemeVariants.DEFAULT, + val aliases: Map = emptyMap(), +) + +/** + * Loads `*.theme.json` [ThemeManifest] resources from the `archie_themes` directory on + * resource pack reload, keyed by their resource location. + * + * [resolveMode] is what [ThemeData.resolvedMode] calls to turn a requested variant name into + * one the active resource pack(s) actually support. + */ +class ThemeManifestResourceListener : + SerializationReloadListener( + format = SerializationManager.json, + serializer = ThemeManifest.serializer(), + directory = "archie_themes", + fileExtension = ".theme.json", + ), + PreparableReloadListener { + + companion object { + internal val MANIFESTS = mutableMapOf() + + /** + * Resolves [requestedMode] against the [ThemeManifest] loaded for [namespace]/[type], + * applying [ThemeManifest.aliases] then falling back to [ThemeManifest.defaultVariant] + * if the (possibly aliased) mode isn't in [ThemeManifest.variants]. + * + * Returns [requestedMode] unchanged when no manifest is registered for that + * namespace/type (i.e. the theme doesn't declare one). + */ + fun resolveMode(namespace: String, type: String, requestedMode: String): String { + val key = ResourceLocation.fromNamespaceAndPath(namespace, type.ifEmpty { "default" }) + val manifest = MANIFESTS[key] ?: return requestedMode + val canonical = manifest.aliases[requestedMode] ?: requestedMode + return if (canonical in manifest.variants) canonical else manifest.defaultVariant + } + } + + /** Replaces the registered [MANIFESTS] with the newly loaded [prepared] set. */ + override fun apply( + prepared: Map, + resourceManager: ResourceManager, + profiler: ProfilerFiller, + ) { + MANIFESTS.clear() + MANIFESTS.putAll(prepared) + } +} + +/** + * Immutable data holder describing the active theme context for composables. + * + * Provided through [LocalTheme] to all composables under a [Theme] wrapper. + * + * @property mode The active variant name (e.g. [ThemeVariants.DARK]). Empty string = default. + * @property type The platform type (e.g. `"java"`). Used as a path prefix for theme files. + * @property darkTextColor The text color used on light/bright surfaces. + * @property lightTextColor The text color used on dark surfaces. + * @property namespace The resource namespace to look up theme definitions in. + */ +@Immutable +data class ThemeData( + val mode: String, + val type: String, + val darkTextColor: KColor, + val lightTextColor: KColor, + val namespace: String = Archie.MOD_ID, +) { + val resolvedMode: String + get() = ThemeManifestResourceListener.resolveMode(namespace, type, mode) + + /** + * Resolves the [ComposableTheme] for the given composable name using the active namespace, + * type, and global mode. + * + * @param composable The composable theme name (e.g. `"button"`, `"slot"`). + * @return The [ComposableTheme] definition loaded from resources. + */ + fun getComposableTheme(composable: String): ComposableTheme { + val mode = resolvedMode + if (mode.isNotEmpty()) { + val globalVariantLocation = composableThemeLocation(namespace, type, mode, composable) + ThemeResourceListener.COMPOSABLES[globalVariantLocation]?.let { return it } + } + return ComposableTheme[composableThemeLocation(namespace, type, composable)] + } +} + +/** Provides the current [ThemeData] to composables in the tree. */ +val LocalTheme = compositionLocalOf { ThemeData(ThemeVariants.DEFAULT, "java", KColor.DARK_GRAY, KColor.WHITE, Archie.MOD_ID) } + +/** + * Builds the [ResourceLocation] used to look up a composable's default-variant theme + * definition. + * + * The resulting path is `:` with an optional `/` prefix when + * [type] is non-empty (e.g. `archie:java/button`). + */ +@Suppress("NOTHING_TO_INLINE") +inline fun composableThemeLocation( + namespace: String, + type: String, + composable: String, +): ResourceLocation = if (type.isNotEmpty()) namespace % type / composable else namespace % composable + +/** + * Builds the [ResourceLocation] used to look up a composable's theme definition for a + * specific non-default [mode] (variant). + * + * The resulting path is `:` with an optional `/` and `/` + * prefix, in that order, for each that is non-empty (e.g. `archie:java/dark/button`). + */ +@Suppress("NOTHING_TO_INLINE") +inline fun composableThemeLocation( + namespace: String, + type: String, + mode: String, + composable: String, +): ResourceLocation { + var ret = namespace % composable + if (mode.isNotEmpty()) ret = mode / ret + if (type.isNotEmpty()) ret = type / ret + return ret +} + +/** + * Sets the active [ThemeData] for all composables in [content]. + * + * @param mode The variant name to activate (`""` for default, `"dark"` for dark mode). + * @param type The platform type (`"java"` by default). + * @param namespace The resource namespace for theme files. + * @param content The composable tree that will receive the theme. + */ +@Composable +fun Theme( + mode: String = ThemeVariants.DEFAULT, + type: String = "java", + darkTextColor: KColor = KColor.DARK_GRAY, + lightTextColor: KColor = KColor.WHITE, + namespace: String = Archie.MOD_ID, + content: @Composable () -> Unit, +) = CompositionLocalProvider(LocalTheme provides ThemeData(mode, type, darkTextColor, lightTextColor, namespace)) { content() } + +/** + * Sets the active theme using a pre-built [ThemeData]. + */ +@Composable +fun Theme(data: ThemeData, content: @Composable () -> Unit) = + CompositionLocalProvider(LocalTheme provides data) { content() } diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/HsvColor.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/HsvColor.kt new file mode 100644 index 000000000..372c6ef8d --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/HsvColor.kt @@ -0,0 +1,55 @@ +package net.kernelpanicsoft.archie.gui.util + +import androidx.compose.runtime.Immutable +import kotlinx.serialization.Serializable + +/** + * An immutable colour representation using the Hue-Saturation-Value model with an alpha channel. + * + * This representation is primarily intended for use with [net.kernelpanicsoft.archie.gui.composables.input.ColorPicker], + * which operates natively in HSV space to avoid lossy round-trip conversions. + * + * All component values are in the range **[0.0, 1.0]**. + * + * ### Example + * ```kotlin + * val red = HsvColor(hue = 0f, saturation = 1f, value = 1f, alpha = 1f) + * val fromKColor = HsvColor.from(KColor.CYAN) + * val backToKColor = red.toKColor() + * ``` + * + * @property hue The hue angle normalized to [0, 1] (0 and 1 both represent red). + * @property saturation The saturation (0 = grey, 1 = fully saturated). + * @property value The brightness value (0 = black, 1 = maximum brightness). + * @property alpha The alpha transparency (0 = fully transparent, 1 = fully opaque). + */ +@Immutable +data class HsvColor( + val hue: Float, + val saturation: Float, + val value: Float, + val alpha: Float, +) { + /** + * Converts this HSV colour to an equivalent [KColor] (ARGB). + */ + fun toKColor(): KColor = KColor.ofHsv(hue, saturation, value, alpha) + + companion object { + /** + * Creates an [HsvColor] from an existing [KColor] by converting its RGB components + * to HSV using the JVM's [java.awt.Color.RGBtoHSB] utility. + * + * @param color The source [KColor] to convert. + */ + fun from(color: KColor): HsvColor { + val hsb = java.awt.Color.RGBtoHSB(color.red, color.green, color.blue, null) + return HsvColor( + hue = hsb[0], + saturation = hsb[1], + value = hsb[2], + alpha = color.alpha / 255f, + ) + } + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/KColor.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/KColor.kt new file mode 100644 index 000000000..17a2c9dd4 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/KColor.kt @@ -0,0 +1,135 @@ +package net.kernelpanicsoft.archie.gui.util + +import kotlinx.serialization.Serializable +import net.minecraft.ChatFormatting +import net.minecraft.network.chat.TextColor +import net.minecraft.util.Mth +import net.minecraft.world.item.DyeColor +import kotlin.random.Random + +/** + * A serializable, immutable ARGB colour value for use in GUI composables. + * + * [KColor] is a pure-Kotlin alternative to [java.awt.Color] that works with + * Minecraft's integer-packed colour conventions. All component values are in + * the range 0–255. + * + * ### Creating colours + * ```kotlin + * val red = KColor.RED + * val custom = KColor.ofRgb(0xFF8C00) // opaque dark orange + * val semi = KColor.ofArgb(0x80FF0000L) // 50 % transparent red + * val hsv = KColor.ofHsv(0.33f, 1f, 0.8f) // dark green via HSV + * ``` + * + * @property red The red channel (0–255). + * @property green The green channel (0–255). + * @property blue The blue channel (0–255). + * @property alpha The alpha channel (0–255, where 0 is fully transparent and 255 is opaque). + */ +@Serializable +data class KColor( + val red: Int = 0, + val green: Int = 0, + val blue: Int = 0, + val alpha: Int = 255, +) { + companion object { + // ── Predefined colours ────────────────────────────────── + val WHITE = ofRgb(0xFFFFFF) + val LIGHT_GRAY = ofRgb(0xC0C0C0) + val GRAY = ofRgb(0x808080) + val DARK_GRAY = ofRgb(0x404040) + val BLACK = ofRgb(0x000000) + val RED = ofRgb(0xFF0000) + val PINK = ofRgb(0xFFAFAF) + val ORANGE = ofRgb(0xFFA500) + val YELLOW = ofRgb(0xFFFF00) + val GREEN = ofRgb(0x4CAF50) + val MAGENTA = ofRgb(0xFF00FF) + val CYAN = ofRgb(0x00FFFF) + val LIGHT_BLUE = ofRgb(0x2196F3) + val BLUE = ofRgb(0x0000FF) + + /** + * Creates a [KColor] from a packed ARGB [Long] in the form `0xAARRGGBB`. + * + * @param argb The packed ARGB value. + */ + fun ofArgb(argb: Long): KColor = KColor( + red = (argb shr 16 and 255).toInt(), + green = (argb shr 8 and 255).toInt(), + blue = (argb and 255).toInt(), + alpha = (argb shr 24).toInt(), + ) + + /** + * Creates an opaque [KColor] from a packed RGB [Int] in the form `0xRRGGBB`. + * + * @param rgb The packed RGB value (alpha is set to 255). + */ + fun ofRgb(rgb: Int): KColor = KColor( + red = rgb shr 16 and 255, + green = rgb shr 8 and 255, + blue = rgb and 255, + ) + + /** + * Creates a [KColor] from HSV (Hue, Saturation, Value) components with full opacity. + * + * @param hue Hue in the range [0, 1]. + * @param saturation Saturation in the range [0, 1]. + * @param value Value (brightness) in the range [0, 1]. + */ + fun ofHsv(hue: Float, saturation: Float, value: Float): KColor = + ofRgb(Mth.hsvToRgb(hue - 0.5e-7f, saturation, value)) + + /** + * Creates a [KColor] from HSV components with a custom alpha. + * + * @param hue Hue in the range [0, 1]. + * @param saturation Saturation in the range [0, 1]. + * @param value Value (brightness) in the range [0, 1]. + * @param alpha Alpha in the range [0, 1] (0 = fully transparent, 1 = opaque). + */ + fun ofHsv(hue: Float, saturation: Float, value: Float, alpha: Float): KColor = + ofArgb(((alpha * 255).toInt().toLong() shl 24 or Mth.hsvToRgb(hue - 0.5e-7f, saturation, value).toLong())) + + /** + * Creates a [KColor] from the colour associated with a [ChatFormatting] constant. + * + * @param formatting A [ChatFormatting] value with an associated colour. + */ + fun ofFormatting(formatting: ChatFormatting): KColor = ofRgb(formatting.color ?: 0) + + /** + * Creates a [KColor] from a Minecraft [DyeColor]. + * + * @param dye The dye colour. + */ + fun ofDye(dye: DyeColor): KColor = ofArgb(dye.textureDiffuseColor.toLong()) + + /** + * Generates a random [KColor]. + * + * @param alpha Whether the alpha channel should also be randomized. When `false` the + * colour is fully opaque. + */ + fun random(alpha: Boolean = true): KColor = + if (alpha) ofArgb(Random.nextLong(0x100000000) or 0xFF000000L) + else ofRgb(Random.nextInt(0x1000000)) + } + + /** + * The packed RGB integer representation (no alpha channel, in the form `0xRRGGBB`). + */ + val rgb: Int get() = (red shl 16) or (green shl 8) or blue + + /** + * The packed ARGB integer representation (in the form `0xAARRGGBB`). + */ + val argb: Int get() = (alpha shl 24) or (red shl 16) or (green shl 8) or blue + + /** Converts this to a vanilla [TextColor] (RGB only - [TextColor] carries no alpha channel). */ + fun toTextColor(): TextColor = TextColor.fromRgb(rgb) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/GuiGraphics.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/GuiGraphics.kt new file mode 100644 index 000000000..7ed42a12c --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/GuiGraphics.kt @@ -0,0 +1,164 @@ +package net.kernelpanicsoft.archie.gui.util.extension + +import com.mojang.blaze3d.vertex.PoseStack +import net.kernelpanicsoft.archie.gui.layout.IntRect +import net.kernelpanicsoft.archie.gui.theme.SimpleThemeState +import net.kernelpanicsoft.archie.gui.theme.ThemeState +import net.minecraft.client.gui.GuiGraphics +import net.minecraft.client.renderer.RenderType +import net.minecraft.resources.ResourceLocation + +private fun ResourceLocation.isAtlasSprite(): Boolean = + !path.startsWith("textures/") && !path.endsWith(".png") + +/** Draws a ThemeState to the screen. */ +fun GuiGraphics.drawThemeState(state: ThemeState, x: Int, y: Int, width: Int, height: Int) { + state as SimpleThemeState + if (state.texture.isAtlasSprite()) { + blitSprite(state.texture, x, y, width, height) + } else { + blit(state, x, y) + } +} + +/** A helper extension to blit a SimpleThemeState without manually extracting all its properties. */ +fun GuiGraphics.blit(state: SimpleThemeState, x: Int, y: Int) { + this.blit( + state.texture, + x, + y, + state.width, + state.height, + state.u.toFloat(), + state.v.toFloat(), + state.uWidth, + state.vHeight, + state.textureSize.width, + state.textureSize.height, + ) +} + +/** Fills a rectangle with a 4-corner color gradient using the default GUI [RenderType]. */ +fun GuiGraphics.fillGradient( + x: Int, + y: Int, + width: Int, + height: Int, + topLeftColor: Int, + topRightColor: Int, + bottomLeftColor: Int, + bottomRightColor: Int, +) = fillGradient( + RenderType.gui(), + x, + y, + width, + height, + topLeftColor, + topRightColor, + bottomLeftColor, + bottomRightColor, +) + +/** + * Fills a rectangle with a 4-corner color gradient, unlike vanilla's [GuiGraphics.fillGradient] + * (top-to-bottom only), by directly emitting one quad with a per-vertex ARGB color to [type]. + */ +fun GuiGraphics.fillGradient( + type: RenderType, + x: Int, + y: Int, + width: Int, + height: Int, + topLeftColor: Int, + topRightColor: Int, + bottomLeftColor: Int, + bottomRightColor: Int, +) { + val buffer = bufferSource().getBuffer(type) + val matrix = pose().last().pose() + + buffer.addVertex(matrix, x + width, y, 0).setColor(topRightColor) + buffer.addVertex(matrix, x, y, 0).setColor(topLeftColor) + buffer.addVertex(matrix, x, y + height, 0).setColor(bottomLeftColor) + buffer.addVertex(matrix, x + width, y + height, 0).setColor(bottomRightColor) +} + +/** Draws an unfilled rectangle outline of [thickness] pixels using the default GUI [RenderType]. */ +fun GuiGraphics.drawRectOutline( + x: Int, + y: Int, + width: Int, + height: Int, + color: Int, + thickness: Int = 1, +) = drawRectOutline(RenderType.gui(), x, y, width, height, color, thickness) + +/** Draws an unfilled rectangle outline of [thickness] pixels as four filled edge strips. */ +fun GuiGraphics.drawRectOutline( + type: RenderType, + x: Int, + y: Int, + width: Int, + height: Int, + color: Int, + thickness: Int = 1, +) { + fill(type, x, y, x + width, y + thickness, color) + fill(type, x, y + height - thickness, x + width, y + height, color) + + fill(type, x, y + thickness, x + thickness, y + height - thickness, color) + fill(type, x + width - thickness, y + thickness, x + width, y + height - thickness, color) +} + +/** + * Runs [block] with this [GuiGraphics]'s [PoseStack][com.mojang.blaze3d.vertex.PoseStack] pushed, + * popping it again afterwards (including when [block] throws). Saves the manual + * `pose().pushPose()` / `pose().popPose()` pairing renderers otherwise need around + * translate/scale/rotate calls. + */ +fun GuiGraphics.pose(block: PoseStack.() -> T): T +{ + val pose = pose() + pose.pushPose() + try + { + return pose.block() + } + finally + { + pose.popPose() + } +} + +/** + * Runs [block] with scissoring enabled to the `[minX, minY, maxX, maxY)` rectangle, disabling + * it again afterwards. Saves the manual `enableScissor(...)` / `disableScissor()` pairing + * renderers otherwise need around clipped content. + */ +fun GuiGraphics.scissor(minX: Int, minY: Int, maxX: Int, maxY: Int, block: () -> T): T +{ + enableScissor(minX, minY, maxX, maxY) + try + { + return block() + } + finally + { + disableScissor() + } +} + +/** Overload of [scissor] taking the clip bounds as an [IntRect]. */ +fun GuiGraphics.scissor(rect: IntRect, block: () -> T): T +{ + val (minX: Int, minY: Int, maxX: Int, maxY: Int) = rect + return scissor(minX, minY, maxX, maxY, block) +} + +/** + * Lets a [GuiGraphics] receiver be invoked like `guiGraphics { ... }`, running [block] with + * `this` as the receiver. Used throughout the built-in composables' `Renderer` implementations + * to avoid repeating the `guiGraphics.` prefix on every draw call. + */ +operator fun GuiGraphics.invoke(block: GuiGraphics.() -> Unit): Unit = block() diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/Screen.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/Screen.kt new file mode 100644 index 000000000..db1b6a0e3 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/Screen.kt @@ -0,0 +1,177 @@ +package net.kernelpanicsoft.archie.gui.util.extension + +import net.kernelpanicsoft.archie.gui.layout.LayoutNode +import net.kernelpanicsoft.archie.gui.modifiers.input.* +import net.kernelpanicsoft.archie.gui.nodes.UINode +import net.minecraft.client.gui.screens.Screen + +// ───────────────────────────────────────────────────────────────────────────── +// Generic traversal +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Recursively dispatches [event] through the [LayoutNode] tree starting at [node]. + * + * Children are processed in reverse z-index order (highest z first) so that the + * topmost visible node receives the event first. Propagation stops as soon as + * [InputEvent.isConsumed] becomes `true`. + * + * @param node The root of the subtree to traverse. + * @param event The [InputEvent] being dispatched. + * @param condition Optional per-node predicate; the [process] callback is only invoked + * when this returns `true` for a given node. + * @param process The callback invoked on each eligible node. + */ +internal fun Screen.processInputEvent( + node: LayoutNode, + event: T, + condition: (LayoutNode) -> Boolean = { true }, + process: (LayoutNode, T) -> Unit, +) { + for (child in node.childrenDescendingZ()) { + if (event.isConsumed) break + processInputEvent(child, event, condition, process) + } + if (!event.isConsumed && condition(node)) { + process(node, event) + } +} + +// ───────────────────────────────────────────────────────────────────────────── +// Pointer (mouse) events +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Dispatches a [PointerEvent] of [eventType] through the [node] tree. + * + * Only nodes that pass [condition] (default: bounded by the mouse position) receive + * the event. Pass `global = true` to dispatch to all nodes regardless of bounds. + * + * @return The dispatched [PointerEvent] (check [PointerEvent.bypassSuper] to decide + * whether to call the vanilla screen's `super` method). + */ +@Suppress("NOTHING_TO_INLINE") +internal inline fun Screen.processPointerEvent( + node: LayoutNode, + mouseX: Double, + mouseY: Double, + eventType: PointerEventType, + global: Boolean = false, + noinline condition: (LayoutNode) -> Boolean = { it.isBounded(mouseX.toInt(), mouseY.toInt()) }, +): PointerEvent { + val event = BasicPointerEvent(eventType, mouseX, mouseY) + processInputEvent(node, event, if (global) { _ -> true } else condition) { currentNode, currentEvent -> + currentNode.modifier.foldIn(Unit) { _, el -> + if (el is OnPointerEventModifier<*> && el.eventType == eventType && (global || !currentEvent.isConsumed)) + @Suppress("UNCHECKED_CAST") + (el.onEvent as (UINode, PointerEvent) -> Unit)(currentNode, event) + } + } + return event +} + +/** + * Dispatches a [ScrollEvent] through the [node] tree. + * + * @return The dispatched [ScrollEvent]. + */ +@Suppress("NOTHING_TO_INLINE") +internal inline fun Screen.processScrollEvent( + node: LayoutNode, + mouseX: Double, + mouseY: Double, + scrollX: Double, + scrollY: Double, + eventType: PointerEventType, + global: Boolean = false, +): ScrollEvent { + val event = ScrollEvent(eventType, mouseX, mouseY, scrollX, scrollY) + processInputEvent( + node, event, + if (global) { _ -> true } else { n -> n.isBounded(mouseX.toInt(), mouseY.toInt()) }, + ) { currentNode, currentEvent -> + currentNode.modifier.foldIn(Unit) { _, el -> + if (el is OnPointerEventModifier<*> && el.eventType == eventType && (global || !currentEvent.isConsumed)) + @Suppress("UNCHECKED_CAST") + (el.onEvent as (UINode, PointerEvent) -> Unit)(currentNode, event) + } + } + return event +} + +/** + * Dispatches a [DragEvent] through the [node] tree. + * + * @return The dispatched [DragEvent]. + */ +@Suppress("NOTHING_TO_INLINE") +internal inline fun Screen.processDragEvent( + node: LayoutNode, + mouseX: Double, + mouseY: Double, + button: Int, + dragX: Double, + dragY: Double, + eventType: PointerEventType, + global: Boolean = false, +): DragEvent { + val event = DragEvent(eventType, mouseX, mouseY, button, dragX, dragY) + processInputEvent( + node, event, + if (global) { _ -> true } else { n -> n.isBounded(mouseX.toInt(), mouseY.toInt()) }, + ) { currentNode, currentEvent -> + currentNode.modifier.foldIn(Unit) { _, el -> + if (el is OnPointerEventModifier<*> && el.eventType == eventType && (global || !currentEvent.isConsumed)) + @Suppress("UNCHECKED_CAST") + (el.onEvent as (UINode, PointerEvent) -> Unit)(currentNode, event) + } + } + return event +} + +// ───────────────────────────────────────────────────────────────────────────── +// Keyboard events +// ───────────────────────────────────────────────────────────────────────────── + +/** + * Dispatches a [KeyEvent] through the [node] tree, chaining all [OnKeyEventModifier]s. + * + * @return The dispatched [KeyEvent]. + */ +@Suppress("NOTHING_TO_INLINE") +internal inline fun Screen.processKeyEvent( + node: LayoutNode, + keyCode: Int, + scanCode: Int, + modifiers: Int, +): KeyEvent { + val event = KeyEvent(keyCode, scanCode, modifiers) + processInputEvent(node, event) { currentNode, currentEvent -> + currentNode.modifier.foldIn(Unit) { _, el -> + if (el is OnKeyEventModifier && !currentEvent.isConsumed) + el.onEvent(currentNode, currentEvent) + } + } + return event +} + +/** + * Dispatches a [CharEvent] through the [node] tree, chaining all [OnCharTypedModifier]s. + * + * @return The dispatched [CharEvent]. + */ +@Suppress("NOTHING_TO_INLINE") +internal inline fun Screen.processCharEvent( + node: LayoutNode, + codePoint: Char, + modifiers: Int, +): CharEvent { + val event = CharEvent(codePoint, modifiers) + processInputEvent(node, event) { currentNode, currentEvent -> + currentNode.modifier.foldIn(Unit) { _, el -> + if (el is OnCharTypedModifier && !currentEvent.isConsumed) + el.onEvent(currentNode, currentEvent) + } + } + return event +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/VertexConsumer.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/VertexConsumer.kt new file mode 100644 index 000000000..74c7307e6 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/VertexConsumer.kt @@ -0,0 +1,20 @@ +package net.kernelpanicsoft.archie.gui.util.extension + +import com.mojang.blaze3d.vertex.VertexConsumer +import org.joml.Matrix4f + +/* ─────────────────────────── VertexConsumer ─────────────────────────── */ + +/** + * Adds a vertex to this [VertexConsumer] using integer screen coordinates. + * + * This is a convenience overload that avoids repeated [Int.toFloat] casts when working + * with pixel-aligned UI geometry. + * + * @param matrix The current pose matrix. + * @param x The x position in screen pixels. + * @param y The y position in screen pixels. + * @param z The z (depth) position. + */ +fun VertexConsumer.addVertex(matrix: Matrix4f, x: Int, y: Int, z: Int): VertexConsumer = + addVertex(matrix, x.toFloat(), y.toFloat(), z.toFloat()) \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt new file mode 100644 index 000000000..04b7e3160 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt @@ -0,0 +1,27 @@ +package net.kernelpanicsoft.archie.networking + +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.gui.ComposeContainerMenuBase +import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacketRegistry +import net.kernelpanicsoft.archie.gui.item.ItemStatePacketRegistry +import net.kernelpanicsoft.archie.util.rem + +/** + * Archie's own [NetworkChannel], used for its internal packets (Compose container menu slot + * syncing, block entity/item state syncing). Not intended for use by downstream mods; create your + * own [NetworkChannel] instance instead. + */ +object ArchieNetworkChannel : NetworkChannel(Archie % "main") +{ + /** + * Registers Archie's built-in packet handlers, then [register]s the channel. Called once + * from [Archie.init]. + */ + fun init() + { + ComposeContainerMenuBase.register() + BlockEntityStatePacketRegistry.register() + ItemStatePacketRegistry.register() + register() + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/IPacketContext.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/IPacketContext.kt new file mode 100644 index 000000000..12ded35bd --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/IPacketContext.kt @@ -0,0 +1,35 @@ +package net.kernelpanicsoft.archie.networking + +import net.minecraft.client.Minecraft +import net.minecraft.core.RegistryAccess +import net.minecraft.world.entity.player.Player + +/** + * Provides contextual information available when handling a received network packet. + * + * Implementations are created by [NetworkChannel] when dispatching received payloads to + * their registered handlers. Both server-bound and client-bound handlers receive an instance + * of this interface, giving them access to the receiving player, the registry, and (on the + * client side) the [Minecraft] instance. + */ +interface IPacketContext { + /** + * The player associated with the packet. On the server this is the sending [net.minecraft.server.level.ServerPlayer]; + * on the client this is the local player. + */ + val player: Player + + /** + * The [RegistryAccess] for the current connection, providing access to dynamic registries. + */ + val registryAccess: RegistryAccess + + /** + * The client-side [Minecraft] instance. + * + * Only safe to access from client-bound packet handlers. Calling this from a server-bound + * handler will throw an [IllegalStateException] because the dedicated server has no + * [Minecraft] instance. + */ + val minecraft: Minecraft get() = Minecraft.getInstance() +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt new file mode 100644 index 000000000..4424c678c --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt @@ -0,0 +1,486 @@ +@file:OptIn(InternalSerializationApi::class) + +package net.kernelpanicsoft.archie.networking + +import dev.architectury.networking.NetworkManager +import dev.architectury.utils.Env +import dev.architectury.utils.EnvExecutor +import dev.architectury.utils.GameInstance +import kotlinx.coroutines.Runnable +import kotlinx.serialization.* +import net.kernelpanicsoft.archie.config.ConfigSpec +import net.kernelpanicsoft.archie.gui.util.KColor +import net.kernelpanicsoft.archie.serialization.SerializationManager +import net.kernelpanicsoft.archie.serialization.serializers.SResourceLocation +import net.kernelpanicsoft.archie.serialization.streamCodec +import net.kernelpanicsoft.archie.util.foldEnv +import net.kernelpanicsoft.archie.util.sendSystemMessage +import net.minecraft.core.RegistryAccess +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.TextColor +import net.minecraft.network.protocol.Packet +import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket +import net.minecraft.network.protocol.common.custom.CustomPacketPayload +import net.minecraft.network.protocol.game.ClientboundBundlePacket +import net.minecraft.resources.ResourceLocation +import net.minecraft.server.level.ServerChunkCache +import net.minecraft.server.level.ServerLevel +import net.minecraft.server.level.ServerPlayer +import net.minecraft.world.entity.Entity +import net.minecraft.world.entity.player.Player +import net.minecraft.world.level.ChunkPos +import kotlin.reflect.KClass + +/** + * A function type that handles a received packet of type [T] along with its [IPacketContext]. + * + * @param T The packet data class type. + */ +typealias PacketHandler = (T, IPacketContext) -> Unit + +/** + * Internal payload wrapper that carries an index into the registered packet list plus the + * CBOR-encoded packet bytes. Using a single payload type per channel keeps the number of + * registered Architectury payload types small. + */ +@Serializable +internal data class Payload( + val id: SResourceLocation, + val index: Int, + val data: ByteArray, +) : CustomPacketPayload { + override fun type(): CustomPacketPayload.Type = + CustomPacketPayload.Type(id) + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (javaClass != other?.javaClass) return false + other as Payload + if (index != other.index) return false + if (!data.contentEquals(other.data)) return false + return true + } + + override fun hashCode(): Int { + var result = index + result = 31 * result + data.contentHashCode() + return result + } +} + +internal val PayloadCodec = Payload.serializer().streamCodec + +/** + * Manages the registration and sending of strongly-typed, serialization-backed network packets. + * + * A single [NetworkChannel] can handle any number of server-bound and client-bound packet + * types. All packets are serialized with CBOR via kotlinx.serialization. + * + * Packet classes **must** be Kotlin data classes annotated with `@Serializable`. + * + * ### Example + * ```kotlin + * val CHANNEL = NetworkChannel(Archie["main"]) + * + * @Serializable + * data class SyncDataPacket(val value: Int) + * + * // During mod init: + * CHANNEL.clientbound(SyncDataPacket::class) { packet, ctx -> + * // handle on client + * } + * CHANNEL.register() + * + * // Sending: + * CHANNEL.toPlayer(player, SyncDataPacket(42)) + * ``` + * + * @param id The unique [ResourceLocation] identifier for this channel. + */ +@Suppress("unused") +@OptIn(ExperimentalSerializationApi::class) +open class NetworkChannel(private val id: ResourceLocation) { + private val clientPacketId = CustomPacketPayload.Type(id.withSuffix("_client")) + private val serverPacketId = CustomPacketPayload.Type(id.withSuffix("_server")) + + private val serverClasses = mutableListOf>() + private val clientClasses = mutableListOf>() + + private val serverConfigs = mutableListOf() + private val clientConfigs = mutableListOf() + + private val serverboundHandlers = mutableListOf>() + private val clientboundHandlers = mutableListOf>() + + /** + * Registers a server-bound packet type and its handler. + * + * The handler is invoked on the server when a client sends a packet of class [klass]. + * + * @param T The packet data class type. + * @param klass The [KClass] of the packet. Must be a data class with `@Serializable`. + * @param handler The handler invoked on the receiving side. + * @throws IllegalArgumentException if [klass] is not a data class, lacks a serializer, or is already registered. + */ + fun serverbound(klass: KClass, handler: PacketHandler) { + require(klass.isData) { "Only data classes can be used as packets" } + require(klass.serializerOrNull() != null) { "Data class doesn't have a serializer. Did you forget to add @Serializable?" } + require(serverClasses.find { it == klass } == null) { "Packet is already registered" } + serverboundHandlers.add(handler) + serverClasses.add(klass) + } + + /** + * Registers a server-bound packet type and its handler, inferring the packet class from the + * reified type parameter [T] instead of requiring `T::class` to be passed explicitly. + * + * Equivalent to `serverbound(T::class, handler)`. + * + * @param T The packet data class type. + * @param handler The handler invoked on the receiving side. + * @throws IllegalArgumentException if [T] is not a data class, lacks a serializer, or is already registered. + */ + inline fun serverbound(noinline handler: PacketHandler) = serverbound(T::class, handler) + + /** + * Registers a client-bound packet type and its handler. + * + * The handler is invoked on the client when the server sends a packet of class [klass]. + * + * @param T The packet data class type. + * @param klass The [KClass] of the packet. Must be a data class with `@Serializable`. + * @param handler The handler invoked on the receiving side. + * @throws IllegalArgumentException if [klass] is not a data class, lacks a serializer, or is already registered. + */ + fun clientbound(klass: KClass, handler: PacketHandler) { + require(klass.isData) { "Only data classes can be used as packets." } + require(klass.serializerOrNull() != null) { "Data class doesn't have a serializer. Did you forget to add @Serializable?" } + require(clientClasses.find { it == klass } == null) { "Packet is already registered" } + clientboundHandlers.add(handler) + clientClasses.add(klass) + } + + /** + * Registers a client-bound packet type and its handler, inferring the packet class from the + * reified type parameter [T] instead of requiring `T::class` to be passed explicitly. + * + * Equivalent to `clientbound(T::class, handler)`. + * + * @param T The packet data class type. + * @param handler The handler invoked on the receiving side. + * @throws IllegalArgumentException if [T] is not a data class, lacks a serializer, or is already registered. + */ + inline fun clientbound(noinline handler: PacketHandler) = clientbound(T::class, handler) + + /** + * Registers [spec] as a server-bound "save my changes" packet: when the server receives one, + * it is decoded with [spec]'s own [kotlinx.serialization.KSerializer] (via + * [net.kernelpanicsoft.archie.config.ConfigSpec.serializer]) rather than the reflective one + * used for ordinary packet classes, since a `ConfigSpec` singleton isn't itself + * `@Serializable`. The permission check, in-memory decode (mutating [spec]'s fields directly), + * and broadcast-or-reject all happen in `decodeDispatchData`, before the handler registered + * here ever runs - that handler is the one place that actually persists the result, calling + * [net.kernelpanicsoft.archie.config.ConfigSpec.save] to write it to disk. No-op if [spec] is + * already registered. + * + * Internal: used by [net.kernelpanicsoft.archie.config.ConfigSpec.init] to wire up + * server/client config sync. Not part of the public packet API. + */ + internal fun configServerbound(klass: KClass, spec: T) + { + if (spec in serverConfigs) return + serverConfigs.add(spec) + serverboundHandlers.add { config: T, context -> config.save() } + serverClasses.add(klass) + } + + internal inline fun configServerbound(spec: T) = configServerbound(spec::class, spec) + + /** + * Registers [spec] as a client-bound config-sync packet: when the client receives one, it is + * decoded with [spec]'s own [kotlinx.serialization.KSerializer] and saved locally via + * [net.kernelpanicsoft.archie.config.ConfigSpec.save]. Used to push a + * [net.kernelpanicsoft.archie.config.ConfigSpec.Server] config's values to a joining player. + * No-op if [spec] is already registered. + * + * Internal: used by [net.kernelpanicsoft.archie.config.ConfigSpec.init] to wire up + * server/client config sync. Not part of the public packet API. + */ + internal fun configClientbound(klass: KClass, spec: T) + { + if (spec in clientConfigs) return + clientConfigs.add(spec) + clientboundHandlers.add { config: T, context -> + config.save() + } + clientClasses.add(klass) + } + + internal inline fun configClientbound(spec: T) = configClientbound(spec::class, spec) + + /** + * Sends one or more packets from the client to the server. + * + * @param packets The packets to send. All must have been registered via [serverbound]. + * @throws IllegalArgumentException if no packets are provided. + * @throws IllegalStateException if a packet type was not registered. + */ + fun toServer(vararg packets: T) { + require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } + packets.map { + createPayload( + packet = it, + classes = serverClasses, + configs = serverConfigs, + payloadId = id.withSuffix("_client"), + missingMessage = "Trying to send a packet to server but it hasn't registered the packet and its handler", + ) + }.forEach { NetworkManager.sendToServer(it) } + } + + /** + * Sends one or more packets from the server to a specific [player]. + * + * @param player The target [ServerPlayer]. + * @param packets The packets to send. All must have been registered via [clientbound]. + * @throws IllegalArgumentException if no packets are provided. + */ + fun toPlayer(player: ServerPlayer, vararg packets: T) { + require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } + createPayloads(packets).forEach { NetworkManager.sendToPlayer(player, it) } + } + + /** + * Sends one or more packets from the server to a list of [players]. + * + * @param players The list of target [ServerPlayer]s. + * @param packets The packets to send. + * @throws IllegalArgumentException if no packets are provided. + */ + fun toPlayers(players: List, vararg packets: T) { + require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } + createPayloads(packets).forEach { NetworkManager.sendToPlayers(players, it) } + } + + /** + * Sends one or more packets from the server to **all** connected players. + * + * @param packets The packets to send. + * @throws IllegalArgumentException if no packets are provided. + * @throws IllegalStateException if called from the client side. + */ + fun toAllPlayers(vararg packets: T) { + require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } + val server = GameInstance.getServer() + ?: throw IllegalStateException("Cannot send clientbound payloads on the client") + createPayloads(packets).forEach { NetworkManager.sendToPlayers(server.playerList.players, it) } + } + + /** + * Sends one or more packets to all players currently in the given [level] (dimension). + * + * @param level The [ServerLevel] whose players should receive the packets. + * @param packets The packets to send. + * @throws IllegalArgumentException if no packets are provided. + */ + fun toPlayersInDimension(level: ServerLevel, vararg packets: T) { + require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } + createPayloads(packets).forEach { NetworkManager.sendToPlayers(level.players(), it) } + } + + /** + * Sends one or more packets to all players within [radius] blocks of the given coordinates + * in [level], optionally excluding [exclude]. + * + * @param level The [ServerLevel] to broadcast within. + * @param exclude A [ServerPlayer] to exclude, or `null` to include all nearby players. + * @param x The X coordinate of the broadcast origin. + * @param y The Y coordinate of the broadcast origin. + * @param z The Z coordinate of the broadcast origin. + * @param radius The broadcast radius in blocks. + * @param packets The packets to send. + * @throws IllegalArgumentException if no packets are provided. + */ + fun toNearPlayers( + level: ServerLevel, + exclude: ServerPlayer? = null, + x: Double, + y: Double, + z: Double, + radius: Double, + vararg packets: T, + ) { + require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } + val payloads = createPayloads(packets) + level.server.playerList.broadcast( + exclude, x, y, z, radius, level.dimension(), + makeClientboundPacket(*payloads.toTypedArray()), + ) + } + + /** + * Sends one or more packets to all players tracking [entity] (i.e., the entity is loaded + * on their client). + * + * @param entity The entity being tracked. + * @param self Whether to also send the packet to the entity itself if it is a [ServerPlayer]. + * @param packets The packets to send. + * @throws IllegalArgumentException if no packets are provided. + * @throws IllegalStateException if called from the client side. + */ + fun toPlayersTrackingEntity(entity: Entity, self: Boolean = false, vararg packets: T) { + require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } + val payloads = createPayloads(packets) + val chunk = entity.level().chunkSource as? ServerChunkCache + ?: throw IllegalStateException("Cannot send clientbound payloads on the client") + if (self) chunk.broadcastAndSend(entity, makeClientboundPacket(*payloads.toTypedArray())) + else chunk.broadcast(entity, makeClientboundPacket(*payloads.toTypedArray())) + } + + /** + * Sends one or more packets to all players tracking chunk [pos] in [level]. + * + * @param level The [ServerLevel] containing the chunk. + * @param pos The [ChunkPos] of the chunk being tracked. + * @param packets The packets to send. + */ + fun toPlayersTrackingChunk(level: ServerLevel, pos: ChunkPos, vararg packets: T) = + toPlayers(level.chunkSource.chunkMap.getPlayers(pos, false), *packets) + + @Suppress("UNCHECKED_CAST") + private fun createPayloads(packets: Array): List { + return packets.map { + createPayload( + packet = it, + classes = clientClasses, + configs = clientConfigs, + payloadId = id.withSuffix("_server"), + missingMessage = "Trying to send a packet to clients but client hasn't registered the packet and its handler", + ) + } + } + + @Suppress("UNCHECKED_CAST") + private fun createPayload( + packet: T, + classes: List>, + configs: List, + payloadId: ResourceLocation, + missingMessage: String, + ): Payload { + val klass = classes.find { it == packet::class } as? KClass + ?: throw IllegalStateException(missingMessage) + val index = classes.indexOf(klass) + val config = configs.find { it::class == klass } + val bytes = if (config != null) { + SerializationManager.cbor.encodeToByteArray(config.serializer, packet as ConfigSpec) + } + else { + SerializationManager.cbor.encodeToByteArray(klass.serializer(), packet) + } + return Payload(payloadId, index, bytes) + } + + @Suppress("UNCHECKED_CAST") + private fun decodeDispatchData( + payload: Payload, + classes: List>, + handlers: List>, + configs: List, + missingClassMessage: String, + missingHandlerMessage: String, + ctx: NetworkManager.PacketContext + ): Pair> { + val klass = classes.getOrNull(payload.index) + ?: throw NoSuchElementException(missingClassMessage) + val handler = handlers.getOrNull(payload.index) as? PacketHandler + ?: throw NoSuchElementException(missingHandlerMessage) + val config = configs.find { it::class == klass } + val msg = if (config != null) { + foldEnv( + client = { + SerializationManager.cbor.decodeFromByteArray(config.serializer, payload.data) + }, + server = { + if (ctx.player.hasPermissions(3)) + { + SerializationManager.cbor.decodeFromByteArray(config.serializer, payload.data) + toAllPlayers(config) + config + } else + { + ctx.player.sendSystemMessage { + style { + color = KColor.RED.toTextColor() + underlined = true + } + translate("archie.networking.config.no_permissions") + } + toPlayer(ctx.player as ServerPlayer, config) + config + } + }) + } else { + SerializationManager.cbor.decodeFromByteArray(klass.serializer(), payload.data) + } + return msg to handler + } + + private fun makeClientboundPacket(vararg payloads: CustomPacketPayload): Packet<*> { + return if (payloads.size == 1) ClientboundCustomPayloadPacket(payloads.first()) + else ClientboundBundlePacket(payloads.map { ClientboundCustomPayloadPacket(it) }) + } + + /** + * Registers this channel with the Architectury networking layer. + * + * Must be called once during mod initialization (before any packets are sent or received). + * Both [serverbound] and [clientbound] handlers should be registered before calling this. + */ + @Suppress("UNCHECKED_CAST") + fun register() { + EnvExecutor.runInEnv(Env.SERVER) { + Runnable { + NetworkManager.registerS2CPayloadType(serverPacketId, PayloadCodec) + } + } + EnvExecutor.runInEnv(Env.CLIENT) { + Runnable { + NetworkManager.registerReceiver(NetworkManager.Side.S2C, serverPacketId, PayloadCodec) { payload, ctx -> + val (msg, handler) = decodeDispatchData( + payload = payload, + classes = clientClasses, + handlers = clientboundHandlers, + configs = clientConfigs, + missingClassMessage = "No class was found on the clientside. Did you forget to do clientbound?", + missingHandlerMessage = "No handler was found on the clientside. Did you forget to do clientbound?", + ctx = ctx + ) + handler(msg, object : IPacketContext + { + override val player: Player get() = ctx.player + override val registryAccess: RegistryAccess get() = ctx.registryAccess() + }) + } + } + } + + + NetworkManager.registerReceiver(NetworkManager.Side.C2S, clientPacketId, PayloadCodec) { payload, ctx -> + val (msg, handler) = decodeDispatchData( + payload = payload, + classes = serverClasses, + handlers = serverboundHandlers, + configs = serverConfigs, + missingClassMessage = "No class was found on the serverside. Did you forget to do serverbound?", + missingHandlerMessage = "No handler was found on the serverside. Did you forget to do serverbound?", + ctx = ctx + ) + handler(msg, object : IPacketContext + { + override val player: Player get() = ctx.player + override val registryAccess: RegistryAccess get() = ctx.registryAccess() + }) + } + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.common.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.common.kt new file mode 100644 index 000000000..13d6e55bd --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.common.kt @@ -0,0 +1,24 @@ +package net.kernelpanicsoft.archie.registries + +import dev.architectury.platform.Mod + +/** + * Schedules [block] to run on the client at the earliest point client-side registration APIs + * that depend on registries already being populated - like Architectury's + * `MenuRegistry.registerScreenFactory` - are safe to call. Backed by an `actual` per mod loader, + * since the loaders genuinely differ on where that point is; only call this from inside a + * client-only guard (e.g. [net.kernelpanicsoft.archie.util.onClient]) - it does no environment + * checking of its own. + * + * On Fabric there's no staged registry-event model to race, so this runs [block] effectively + * immediately. On NeoForge, [dev.architectury.event.events.common.LifecycleEvent.SETUP]/ + * `FMLCommonSetupEvent` - the timing [ADeferredRegistryHolder.initClient] used to schedule on + * unconditionally - actually runs *after* several client registration-stage events (e.g. + * `RegisterMenuScreensEvent`), so calling `MenuRegistry.registerScreenFactory` from there + * silently never fires: it internally attaches a listener for that exact event, which has + * already fired and moved on by the time Common Setup runs. + * + * @param mod The mod whose event bus [block] should run on (NeoForge only needs this - Fabric's + * `actual` ignores it, since Fabric has no per-mod bus to look up). + */ +expect fun scheduleEarlyClientRegistration(mod: Mod, block: () -> Unit) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ACreativeTabRegistry.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ACreativeTabRegistry.kt new file mode 100644 index 000000000..eea74c7bc --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ACreativeTabRegistry.kt @@ -0,0 +1,11 @@ +package net.kernelpanicsoft.archie.registries + +import dev.architectury.registry.CreativeTabRegistry +import net.minecraft.world.item.CreativeModeTab + +/** Thin wrapper over Architectury's [CreativeTabRegistry] for one-off creative tab creation. */ +object ACreativeTabRegistry +{ + /** Builds a [CreativeModeTab] via [block] without registering it. See [CreativeTabRegistryHelper] to register one. */ + fun create(block: CreativeModeTab.Builder.() -> Unit): CreativeModeTab = CreativeTabRegistry.create(block) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ADeferredRegistryHolder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ADeferredRegistryHolder.kt new file mode 100644 index 000000000..04a66b104 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ADeferredRegistryHolder.kt @@ -0,0 +1,89 @@ +package net.kernelpanicsoft.archie.registries + +import dev.architectury.platform.Mod +import dev.architectury.registry.registries.DeferredRegister +import dev.architectury.registry.registries.RegistrySupplier +import net.kernelpanicsoft.archie.util.onClient +import net.kernelpanicsoft.archie.util.rem +import net.minecraft.core.Registry +import net.minecraft.resources.ResourceKey +import net.minecraft.resources.ResourceLocation +import kotlin.reflect.KProperty + +/** + * A registry holder that stores every registered entry in a [Map] keyed by its [ResourceLocation], + * providing O(1) lookup by id string as well as property delegation via `by register(...)`. + * + * This class wraps an Architectury [DeferredRegister] and exposes the registered suppliers + * through the [Map] interface. Use it as a base for per-registry object singletons. + * + * **Note:** Always use `by register(...)` (delegation) rather than calling `.get()` eagerly, + * to avoid touching the registry before it is unfrozen. + * + * ### Example + * ```kotlin + * object MyItems : ADeferredRegistryHolder(MyMod.MOD, Registries.ITEM) { + * val MY_ITEM by register("my_item") { Item(Item.Properties()) } + * } + * // In mod init: + * MyItems.init() + * ``` + * + * @param T The registry entry type. + */ +abstract class ADeferredRegistryHolder private constructor( + private val mod: Mod, + registryKey: ResourceKey>, + private val map: MutableMap> +) : + Map> by map +{ + constructor(mod: Mod, registryKey: ResourceKey>) : this(mod, registryKey, mutableMapOf()) + + private val registry: DeferredRegister = DeferredRegister.create(mod.modId, registryKey) + + /** + * Registers the underlying [DeferredRegister], then schedules [initClient] to run on the + * client, at the earliest point registration APIs that depend on registries already being + * populated (e.g. Architectury's `MenuRegistry.registerScreenFactory`) are safe to call - see + * [scheduleEarlyClientRegistration]. Must be called once during mod initialization. + */ + fun init() + { + registry.register() + onClient { + scheduleEarlyClientRegistration(mod) { + initClient() + } + } + } + + /** Client-only setup run after [init] - see [scheduleEarlyClientRegistration] for exactly when. No-op by default. */ + open fun initClient() = Unit + + /** Looks up a registered entry by its unqualified [id] (namespaced under [mod] automatically). */ + operator fun get(id: String): RegistrySupplier? = map[mod % id] + + + /** Property delegate operator that unwraps a [RegistrySupplier] to its concrete value. */ + operator fun RegistrySupplier.getValue(any: Any?, property: KProperty<*>): R + { + return get() + } + + /** Registers an entry under [id] (namespaced under [mod]) and records it in [map]. */ + protected fun register(id: String, supplier: () -> R): RegistrySupplier + { + val ret = registry.register(id, supplier) + map[ret.registryId] = ret + return ret + } + + /** Registers an entry under the fully-qualified [id] and records it in [map]. */ + protected fun register(id: ResourceLocation, supplier: () -> R): RegistrySupplier + { + val ret = registry.register(id, supplier) + map[ret.registryId] = ret + return ret + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/BlockRegistryHelper.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/BlockRegistryHelper.kt new file mode 100644 index 000000000..70e2e3712 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/BlockRegistryHelper.kt @@ -0,0 +1,63 @@ +package net.kernelpanicsoft.archie.registries + +import dev.architectury.registry.registries.DeferredRegister +import dev.architectury.registry.registries.RegistrySupplier +import net.minecraft.core.registries.Registries +import net.minecraft.world.item.BlockItem +import net.minecraft.world.item.Item +import net.minecraft.world.level.block.Block + +/** + * A [RegistryHelper] specialised for [Block] registration that also automatically registers + * a corresponding [BlockItem] in the item registry. + * + * Extend this class for each set of blocks in your mod. Each call to [block] registers both + * the block and (optionally) its item form. + * + * ### Example + * ```kotlin + * object MyBlocks : BlockRegistryHelper(modId) { + * val MY_BLOCK by block("my_block") { MyBlock(BlockBehaviour.Properties.of()) } + * } + * + * // In mod init: + * MyBlocks.init() + * ``` + * + * @param T The base block type; must extend [Block]. + * @param modId The mod ID used as the namespace for registered entries. + */ +@Suppress("UNCHECKED_CAST") +open class BlockRegistryHelper(modId: String) : RegistryHelper( + DeferredRegister.create(modId, Registries.BLOCK) as DeferredRegister, +) { + /** The [DeferredRegister] for the item registry, used to register [BlockItem]s. */ + open val itemRegistry: DeferredRegister = DeferredRegister.create(modId, Registries.ITEM) + + override fun init() = super.init().also { itemRegistry.register() } + + /** + * Registers a block and its associated [BlockItem]. + * + * The [itemSupplier] defaults to a plain [BlockItem]. Pass `null` to suppress item + * registration entirely (useful for technical blocks that should not appear in inventories). + * + * @param id The registry name for both the block and its item. + * @param itemSupplier A factory that produces the [BlockItem] given the registered block + * and default [Item.Properties]. Pass `null` to skip item registration. + * @param supplier Factory that produces the block instance. + * @return A [RegistrySupplier] for the registered block. + */ + open fun block( + id: String, + itemSupplier: ((V, Item.Properties) -> BlockItem)? = { block, props -> BlockItem(block, props) }, + supplier: () -> V, + ): RegistrySupplier { + val holder = register(id, supplier) + itemRegistry.register(id) { + val block = holder.get() + itemSupplier?.invoke(block, Item.Properties()) ?: BlockItem(block, Item.Properties()) + } + return holder + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/CreativeTabRegistryHelper.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/CreativeTabRegistryHelper.kt new file mode 100644 index 000000000..9e5fbd621 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/CreativeTabRegistryHelper.kt @@ -0,0 +1,45 @@ +package net.kernelpanicsoft.archie.registries + +import dev.architectury.registry.CreativeTabRegistry +import dev.architectury.registry.registries.DeferredRegister +import dev.architectury.registry.registries.RegistrySupplier +import net.minecraft.core.registries.Registries +import net.minecraft.world.item.CreativeModeTab + +/** + * A [RegistryHelper] specialised for [CreativeModeTab] registration via Architectury's + * [CreativeTabRegistry]. + * + * ### Example + * ```kotlin + * object MyTabs : CreativeTabRegistryHelper(modId) { + * val MY_TAB by create("my_tab") { + * title(Component.translatable("itemGroup.mymod.my_tab")) + * icon { ItemStack(MyBlocks.MY_BLOCK) } + * displayItems { _, output -> + * output.accept(MyBlocks.MY_BLOCK) + * } + * } + * } + * + * // In mod init: + * MyTabs.init() + * ``` + * + * @param T The creative tab type; must extend [CreativeModeTab]. + * @param modId The mod ID used as the namespace for registered entries. + */ +@Suppress("UNCHECKED_CAST") +open class CreativeTabRegistryHelper(modId: String) : RegistryHelper( + DeferredRegister.create(modId, Registries.CREATIVE_MODE_TAB) as DeferredRegister, +) { + /** + * Registers a new [CreativeModeTab] using an Architectury [CreativeTabRegistry] builder. + * + * @param id The registry name of the creative tab. + * @param block A builder lambda applied to [CreativeModeTab.Builder] to configure the tab. + * @return A [RegistrySupplier] for the registered creative tab. + */ + open fun create(id: String, block: CreativeModeTab.Builder.() -> Unit): RegistrySupplier = + register(id) { CreativeTabRegistry.create(block) as V } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistrarHelper.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistrarHelper.kt new file mode 100644 index 000000000..64af3dc65 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistrarHelper.kt @@ -0,0 +1,42 @@ +package net.kernelpanicsoft.archie.registries + +import dev.architectury.registry.registries.Registrar +import dev.architectury.registry.registries.RegistrarBuilder +import dev.architectury.registry.registries.RegistrarManager +import net.kernelpanicsoft.archie.util.rem + +/** + * A base for declaring **custom** Architectury registries (i.e. a whole new registry, the way + * [net.minecraft.core.registries.Registries.ITEM] is a registry) via lazily-built [Registrar]s. + * + * This is unrelated to registering *entries* into an existing registry - for that, use + * [RegistryHelper]/[ADeferredRegistryHolder] with a [dev.architectury.registry.registries.DeferredRegister]. + * Once a custom registry declared here exists, populating it with entries still requires its + * own [dev.architectury.registry.registries.DeferredRegister] targeting the [Registrar]'s + * registry key, created separately from this helper. + * + * @param modId The mod id passed to [RegistrarManager.get] and used as the namespace for each [registry]. + */ +abstract class RegistrarHelper(private val modId: String) +{ + private val manager = RegistrarManager.get(modId) + private val lazies = mutableListOf>>() + + /** + * Declares a lazily-built custom [Registrar] (registry) for [id], configured via [block]. + * + * @param id The unqualified registry id, namespaced under [modId]. + */ + fun registry(id: String, block: RegistrarBuilder.() -> Unit = {}): Lazy> + { + return lazy { + manager.builder(modId % id).apply(block).build() + }.also { lazies += it } + } + + /** Forces every custom registry declared via [registry] to be built. */ + fun init() + { + lazies.forEach { it.value } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistryHelper.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistryHelper.kt new file mode 100644 index 000000000..2bb4923b7 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistryHelper.kt @@ -0,0 +1,66 @@ +package net.kernelpanicsoft.archie.registries + +import dev.architectury.registry.registries.DeferredRegister +import dev.architectury.registry.registries.RegistrySupplier +import net.minecraft.resources.ResourceLocation +import kotlin.reflect.KProperty + +/** + * A convenience base class for managing a collection of registry entries backed by an + * Architectury [DeferredRegister]. + * + * Subclass this object (or class) for each registry you want to populate, declare your + * entries with `by register(...)`, and call [init] during your mod's initialization phase. + * + * **Important:** Always use the `by` delegation operator when declaring entries to avoid + * "Registry is frozen" errors that occur when values are accessed before the registry tick. + * + * ### Example + * ```kotlin + * object MyItems : RegistryHelper(DeferredRegister.create(modId, Registries.ITEM)) { + * val MY_ITEM by register("my_item") { Item(Item.Properties()) } + * } + * + * // In mod init: + * MyItems.init() + * ``` + * + * @param T The base type stored in the target registry. + * @property registry The [DeferredRegister] to which entries will be submitted. + */ +abstract class RegistryHelper(val registry: DeferredRegister) { + + /** + * Registers this helper's [DeferredRegister] with the game's registry system. + * + * Must be called once during mod initialization. + */ + open fun init() = registry.register() + + /** + * Registers a new entry and returns a [RegistrySupplier] for lazy access. + * + * @param id The entry's registry name (without namespace). The mod namespace is prepended automatically. + * @param supplier Factory producing the entry. Must not cache the returned instance. + * @return A [RegistrySupplier] wrapping the registered entry. + */ + open fun register(id: String, supplier: () -> V): RegistrySupplier = + registry.register(id, supplier) + + /** + * Registers a new entry using a fully-qualified [ResourceLocation] and returns a [RegistrySupplier]. + * + * @param id The fully-qualified [ResourceLocation] for the entry. + * @param supplier Factory producing the entry. Must not cache the returned instance. + * @return A [RegistrySupplier] wrapping the registered entry. + */ + open fun register(id: ResourceLocation, supplier: () -> V): RegistrySupplier = + registry.register(id, supplier) + + /** + * Kotlin property delegate operator that unwraps a [RegistrySupplier] to its concrete value. + * + * This enables the idiomatic `val MY_ENTRY by register(...)` pattern. + */ + operator fun RegistrySupplier.getValue(any: Any?, property: KProperty<*>): V = get() +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/extensions.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/extensions.kt new file mode 100644 index 000000000..05f57a0cb --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/extensions.kt @@ -0,0 +1,16 @@ +package net.kernelpanicsoft.archie.registries + +import dev.architectury.extensions.injected.InjectedRegistryEntryExtension +import dev.architectury.registry.registries.RegistrySupplier +import net.minecraft.core.Holder +import net.minecraft.resources.ResourceLocation +import kotlin.reflect.KProperty + +/** The registry name Architectury injected into this registry entry. Only valid once registered. */ +val InjectedRegistryEntryExtension.id: ResourceLocation + get() = `arch$registryName`()!! + +/** The [Holder] Architectury injected into this registry entry. Only valid once registered. */ +val InjectedRegistryEntryExtension.holder: Holder + get() = `arch$holder`() + diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/resourcepacks/SerializationReloadListener.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/resourcepacks/SerializationReloadListener.kt new file mode 100644 index 000000000..730a6b2a7 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/resourcepacks/SerializationReloadListener.kt @@ -0,0 +1,88 @@ +package net.kernelpanicsoft.archie.resourcepacks + +import com.mojang.logging.LogUtils +import kotlinx.serialization.KSerializer +import kotlinx.serialization.StringFormat +import net.minecraft.resources.FileToIdConverter +import net.minecraft.resources.ResourceLocation +import net.minecraft.server.packs.resources.ResourceManager +import net.minecraft.server.packs.resources.SimplePreparableReloadListener +import net.minecraft.util.profiling.ProfilerFiller +import org.slf4j.Logger +import java.io.InputStreamReader + +/** + * An abstract [SimplePreparableReloadListener] that automatically discovers resource files + * under a given [directory] and deserializes them using kotlinx.serialization. + * + * Override [apply] to store or process the resulting map of [ResourceLocation] to [T] after + * each reload. Register instances of subclasses with Architectury's `ReloadListenerRegistry` + * during `initClient()`. + * + * ### Example + * ```kotlin + * class MyDataListener : SerializationReloadListener( + * format = Json { ignoreUnknownKeys = true }, + * serializer = MyData.serializer(), + * directory = "my_data", + * fileExtension = ".json", + * ) { + * override fun apply(prepared: Map, ...) { + * MyDataRegistry.ENTRIES.clear() + * MyDataRegistry.ENTRIES += prepared + * } + * } + * ``` + * + * @param T The data class type that each resource file deserializes into. + * @param format The kotlinx.serialization [StringFormat] to use (e.g. `Json`, `Toml`). + * @param serializer The [KSerializer] for [T]. + * @param directory The resource-pack directory to scan (e.g. `"archie_themes"`). + * @param fileExtension The file extension to match, including the leading dot (e.g. `".json"`). + */ +abstract class SerializationReloadListener( + private val format: StringFormat, + private val serializer: KSerializer, + private val directory: String, + private val fileExtension: String, +) : SimplePreparableReloadListener>() { + + companion object { + private val LOGGER: Logger = LogUtils.getLogger() + } + + /** + * Returns `true` when [fileLocation] should be decoded by this listener. + * + * Subclasses can override this to skip known non-data resources that share the same + * directory and extension pattern. + */ + protected open fun shouldLoadResource(fileLocation: ResourceLocation): Boolean = true + + /** + * Scans [resourceManager] for all files matching [directory] / * [fileExtension], + * deserializes each one, and returns the resulting map keyed by entry id. + * + * Errors in individual files are logged and that file is skipped; other entries still load. + */ + override fun prepare( + resourceManager: ResourceManager, + profiler: ProfilerFiller, + ): Map { + val dataMap = mutableMapOf() + val fileToIdConverter = FileToIdConverter(directory, fileExtension) + + for ((fileLocation, resource) in fileToIdConverter.listMatchingResources(resourceManager)) { + if (!shouldLoadResource(fileLocation)) continue + val resourceId = fileToIdConverter.fileToId(fileLocation) + try { + InputStreamReader(resource.open()).use { reader -> + dataMap[resourceId] = format.decodeFromString(serializer, reader.readText()) + } + } catch (e: Exception) { + LOGGER.error("Couldn't parse data file {} from {}", resourceId, fileLocation, e) + } + } + return dataMap + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ArchieDataAttachmentImpl.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ArchieDataAttachmentImpl.kt new file mode 100644 index 000000000..4696d61a2 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ArchieDataAttachmentImpl.kt @@ -0,0 +1,24 @@ +package net.kernelpanicsoft.archie.serialization + +import earth.terrarium.common_storage_lib.data.DataManager + +/** + * Thin wrapper around a single Common Storage Lib [DataManager], backing [AttachmentRegistry.attachment]. + * See [ArchieDataAttachment] for the full contract this implements. + */ +internal class ArchieDataAttachmentImpl(private val manager: DataManager) : ArchieDataAttachment +{ + override fun get(holder: Any): T = manager.get(holder) + override fun getOrThrow(holder: Any): T = manager.getOrThrow(holder) + override fun getOrCreate(holder: Any, default: T): T = manager.getOrCreate(holder, default) + + override fun set(holder: Any, value: T): T + { + manager.set(holder, value) + return value + } + + override fun remove(holder: Any): T = manager.remove(holder) + override fun has(holder: Any): Boolean = manager.has(holder) + override fun modify(holder: Any, block: (T) -> T): T = manager.modify(holder, block) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/AttachmentRegistry.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/AttachmentRegistry.kt new file mode 100644 index 000000000..47f4dc59c --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/AttachmentRegistry.kt @@ -0,0 +1,102 @@ +package net.kernelpanicsoft.archie.serialization + +import earth.terrarium.common_storage_lib.data.DataManagerRegistry +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.serializer +import net.kernelpanicsoft.archie.config.toSnakeCase +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadOnlyProperty + +/** + * Base class for declaring a mod's [ArchieDataAttachment]s, wrapping a single Common Storage Lib + * `DataManagerRegistry`. Declare one `object` per mod extending this class, declare attachments + * as delegated properties on it via [attachment] (or one of the primitive convenience wrappers), + * then call [init] once at mod-init time - after those property initializers have already run, + * same ordering as [net.kernelpanicsoft.archie.networking.NetworkChannel]/`Config.init()`. + * + * ```kotlin + * object MyAttachments : AttachmentRegistry(MyMod.MOD_ID) { + * val mana by intAttachment(sync = true, default = { 0 }) + * } + * + * // mod init: + * MyAttachments.init() + * ``` + */ +abstract class AttachmentRegistry(modId: String) +{ + @PublishedApi + internal val registry = DataManagerRegistry(modId) + + /** + * Declares an [ArchieDataAttachment] backed by [serializer], keyed by the delegated property's + * snake_case name. See [ArchieDataAttachment] for the full contract, including exactly what + * [sync] and [itemComponent] each do and don't cover. + * + * @param sync Reactively push updates to tracking players on every [ArchieDataAttachment.set]/ + * [ArchieDataAttachment.remove] - Entity/BlockEntity on both loaders, ServerLevel on NeoForge + * only. + * @param copyOnDeath Preserve the value across a player's death/respawn. Entity/BlockEntity + * holders only; meaningless (and untested by Archie) for `itemComponent`-only attachments. + * @param itemComponent Additionally back this attachment with a vanilla `DataComponentType`, + * making it usable on `ItemStack` holders too. + * @param default Supplies the value used before a holder has anything explicitly set. + */ + fun attachment( + serializer: KSerializer, + sync: Boolean = false, + copyOnDeath: Boolean = false, + itemComponent: Boolean = false, + default: () -> T, + ): PropertyDelegateProvider>> = PropertyDelegateProvider { _, property -> + val builder = registry.builder(default).serialize(serializer.codec) + // itemComponent needs a client codec regardless of `sync`: CSL's own builder unconditionally + // calls `.networkSynchronized(clientCodec)` when building the DataComponentType, and leaves + // clientCodec null unless networkSerializer(...) was called - passing null there breaks at + // registration time. Always supplying our own explicit StreamCodec here (rather than CSL's + // no-arg networkSerializer(), which derives one from the Codec instead) keeps this consistent + // with the rest of Archie's serialization, which encodes over the network via kotlinx CBOR + // directly rather than round-tripping through a Codec. + if (sync || itemComponent) builder.networkSerializer(serializer.streamCodec) + if (copyOnDeath) builder.copyOnDeath() + if (itemComponent) builder.withDataComponent() + // ArchieDataAttachment itself implements ReadWriteProperty (so it can *also* back a `var + // Holder.x by MyAttachments.x` extension property once resolved) - if `attachment(...)` + // returned it directly as the PropertyDelegateProvider's own delegate type, `by attachment(...)` + // here would unwrap straight through to ArchieDataAttachment's getValue() and bind `mana`'s + // type to T, not to ArchieDataAttachment itself. Wrapping it in a plain ReadOnlyProperty + // stops that second unwrap, the same way NBTHolderImpl.itemField/fluidField/energyField wrap + // their storage objects for the exact same reason. + val attachment = ArchieDataAttachmentImpl(builder.buildAndRegister(property.name.toSnakeCase())) + ReadOnlyProperty { _, _ -> attachment } + } + + fun booleanAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Boolean = { false }) = + attachment(Boolean.serializer(), sync, copyOnDeath, itemComponent, default) + fun byteAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Byte = { 0 }) = + attachment(Byte.serializer(), sync, copyOnDeath, itemComponent, default) + fun shortAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Short = { 0 }) = + attachment(Short.serializer(), sync, copyOnDeath, itemComponent, default) + fun intAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Int = { 0 }) = + attachment(Int.serializer(), sync, copyOnDeath, itemComponent, default) + fun longAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Long = { 0 }) = + attachment(Long.serializer(), sync, copyOnDeath, itemComponent, default) + fun floatAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Float = { 0.0f }) = + attachment(Float.serializer(), sync, copyOnDeath, itemComponent, default) + fun doubleAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Double = { 0.0 }) = + attachment(Double.serializer(), sync, copyOnDeath, itemComponent, default) + fun stringAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> String = { "" }) = + attachment(String.serializer(), sync, copyOnDeath, itemComponent, default) + + /** Registers every attachment declared through this registry against the mod event bus. Call once, at mod-init time, after all of this object's `by attachment(...)` properties have already run. */ + fun init() = registry.init() +} + +/** Reified variant of [AttachmentRegistry.attachment] that resolves the [KSerializer] for [T] automatically. */ +inline fun AttachmentRegistry.attachment( + sync: Boolean = false, + copyOnDeath: Boolean = false, + itemComponent: Boolean = false, + noinline default: () -> T, +): PropertyDelegateProvider>> = attachment(serializer(), sync, copyOnDeath, itemComponent, default) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/DataAttachment.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/DataAttachment.kt new file mode 100644 index 000000000..c9fb30ca1 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/DataAttachment.kt @@ -0,0 +1,106 @@ +package net.kernelpanicsoft.archie.serialization + +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty + +/** + * A [DataManager][earth.terrarium.common_storage_lib.data.DataManager]-backed attachment. Unlike + * [NBTHolder], which owns its own per-instance field storage, a single [ArchieDataAttachment] + * instance is stateless and reusable as the delegate for a `var Holder.property by ...` + * extension property on *any* number of holder instances - the holder passed to each method + * (or, via [getValue]/[setValue], the property's receiver) is where the data actually lives. + * Obtain instances via [AttachmentRegistry.attachment]. + * + * ### Usage + * ```kotlin + * object MyAttachments : AttachmentRegistry(MyMod.MOD_ID) { + * val mana by intAttachment(sync = true, default = { 0 }) + * } + * var Entity.mana by MyAttachments.mana + * + * // in mod init, after MyAttachments' properties above have already run: + * MyAttachments.init() + * ``` + * + * ### Supported holder types + * What's supported depends on the platform and on whether the attachment was declared with + * `itemComponent = true`: + * - **Entity / BlockEntity**: a NeoForge attachment / Fabric `AttachmentTarget`, on both platforms. + * - **ServerLevel**: NeoForge only. Fabric's `updateTarget` dispatch has no `Level`/`ServerLevel` + * case at all, so even where the underlying `get`/`set`/`has` calls happen to succeed there, + * `sync = true` will silently never push an update. Don't rely on world-level attachments if + * you need Fabric parity. + * - **ItemStack**: only if declared with `itemComponent = true` (backed by a vanilla + * [net.minecraft.core.component.DataComponentType] instead of an attachment). Calling any + * method here against an `ItemStack` for an attachment that *wasn't* declared with + * `itemComponent = true` throws [NullPointerException] (its backing `DataComponentType` is + * null) rather than [IllegalArgumentException] - `ItemStack` still passes CSL's holder-kind + * check either way, it just has nowhere to actually read/write. + * + * Any other object type throws [IllegalArgumentException] from every method here except + * [getValue]/[setValue], which forward straight into [get]/[set]. + * + * ### `sync` vs. `itemComponent` + * These are two genuinely different mechanisms, not two flavors of one thing: + * - Entity/BlockEntity/ServerLevel sync (`sync = true` on [AttachmentRegistry.attachment]) is a + * reactive push straight out of [set]/[remove] to tracking players, driven by CSL's own + * `DataManagerImpl`/packets. + * - `itemComponent` attachments are **not** covered by that push at all - [set] on an `ItemStack` + * never sends anything itself. They ride vanilla's normal item/component replication instead + * (the same mechanism as vanilla's own `BundleContents`), which isn't reactive the same way. + * + * Declaring `itemComponent = true` without `sync = true` still needs a network codec under the + * hood (vanilla's `DataComponentType` always carries one) - [AttachmentRegistry.attachment] + * handles that for you regardless of what you pass for `sync`. + * + * ### `has()` after `get()` on Entity/BlockEntity/ServerLevel holders + * Confirmed on real Fabric/NeoForge attachment internals, not documented by Common Storage Lib + * itself: [get] on these holder kinds silently creates *and persists* the default value on first + * read (Fabric's `AttachmentTarget.getAttachedOrCreate`, NeoForge's `AttachmentHolder.getData` - + * both write-through on a miss, they don't just compute-and-discard). That means [has] can only + * tell "never touched" apart from "read once" if you call it *before* the first [get] - calling + * [get] first, then [has], will report `true` even though nothing was ever explicitly [set]. + * `ItemStack`/`itemComponent` holders don't have this quirk - `DataComponentHolder.getOrDefault` + * genuinely doesn't persist on read. + */ +interface ArchieDataAttachment : ReadWriteProperty +{ + /** + * Reads [holder]'s current value, falling back to the attachment's default if unset. Never + * throws for an unset value - only for an unsupported [holder]. + * + * On Entity/BlockEntity/ServerLevel holders, an unset read silently creates *and persists* the + * default - see the class-level "`has()` after `get()`" note before relying on [has] afterward. + */ + fun get(holder: Any): T + + /** Reads [holder]'s current value, throwing if it's never been explicitly [set]. The exact exception type (`NullPointerException` vs. `RuntimeException`) differs by platform for `ItemStack` holders - don't match on a specific type for that case. */ + fun getOrThrow(holder: Any): T + + /** Reads [holder]'s current value if [has] is true, otherwise [set]s it to [default] first. Returns the (possibly just-written) current value either way. */ + fun getOrCreate(holder: Any, default: T): T + + /** Writes [value] onto [holder], returning [value]. */ + fun set(holder: Any, value: T): T + + /** + * Removes [holder]'s explicitly-set value, if any, reverting subsequent [get] calls to the + * default. The return value mirrors the removed data 1:1 from the underlying Java API and can + * be a JVM-level null if nothing was set - prefer checking [has] first if you actually need it. + */ + fun remove(holder: Any): T + + /** True if [holder] has an explicitly-[set] value (as opposed to just reading back the default). */ + fun has(holder: Any): Boolean + + /** Reads [holder]'s current value, applies [block], writes the result back, and returns it. */ + fun modify(holder: Any, block: (T) -> T): T + + override operator fun getValue(thisRef: Any?, property: KProperty<*>): T = + get(thisRef ?: throw IllegalStateException("${property.name} has no receiver to read a data attachment from - it can't be a top-level property")) + + override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) + { + set(thisRef ?: throw IllegalStateException("${property.name} has no receiver to write a data attachment to - it can't be a top-level property"), value) + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt new file mode 100644 index 000000000..39cb03450 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt @@ -0,0 +1,244 @@ +package net.kernelpanicsoft.archie.serialization + +import net.kernelpanicsoft.archie.config.toSnakeCase +import net.kernelpanicsoft.archie.transfer.ArchieEnergyStorage +import net.kernelpanicsoft.archie.transfer.ArchieFluidStorage +import net.kernelpanicsoft.archie.transfer.ArchieItemStorage +import dev.architectury.fluid.FluidStack +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import net.benwoodworth.knbt.NbtTag +import net.minecraft.core.component.DataComponents +import net.minecraft.nbt.CompoundTag +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.component.CustomData +import net.minecraft.world.level.block.entity.BlockEntity +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadOnlyProperty +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty +import kotlin.reflect.full.hasAnnotation + +/** + * [NBTHolder] implementation backing [NBTHolder.fluid], persisting field values into [stack]'s + * [CustomData] component instead of an in-memory map. + */ +class FluidStackNBTHolderImpl(private val stack: FluidStack) : NBTHolder +{ + private val data: MutableMap = mutableMapOf() + private val itemStorage: MutableMap = mutableMapOf() + private val fluidStorage: MutableMap = mutableMapOf() + private val energyStorage: MutableMap = mutableMapOf() + + init + { + loadFromStack() + } + + override fun field( + serializer: KSerializer, + default: () -> T + ): PropertyDelegateProvider> + { + return PropertyDelegateProvider { thisRef, property -> + val delegate = object : ReadWriteProperty + { + override fun getValue(thisRef: Any?, property: KProperty<*>): T + { + loadFromStack() + return runCatching { + NBT.decodeFromNbtTagRootless(serializer, data.getOrPut(property.name.toSnakeCase()) { + NBT.encodeToNbtTagRootless(serializer, default()) + }) + }.recover { + val ret = default() + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(serializer, ret) + ret + }.getOrThrow() + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) + { + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(serializer, value) + saveToStack() + } + } + if (property.name.toSnakeCase() !in data) + delegate.setValue(thisRef, property, default()) + + delegate + } + } + + override fun listField( + serializer: KSerializer, + default: () -> List + ): PropertyDelegateProvider>> + { + return PropertyDelegateProvider { thisRef, property -> + val delegate = object : ReadWriteProperty> + { + override fun getValue(thisRef: Any?, property: KProperty<*>): MutableList + { + loadFromStack() + return ObservableList(runCatching { + NBT.decodeFromNbtTagRootless(ListSerializer(serializer), data.getOrPut(property.name.toSnakeCase()) { + NBT.encodeToNbtTagRootless(ListSerializer(serializer), default()) + }) + }.recover { + val ret = default() + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(ListSerializer(serializer), ret) + ret + }.getOrThrow().toMutableList()) { list -> setValue(thisRef, property, list)} + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: MutableList) + { + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(ListSerializer(serializer), value) + saveToStack() + } + } + if (property.name.toSnakeCase() !in data) + delegate.setValue(thisRef, property, default().toMutableList()) + + delegate + } + } + + override fun mapField( + serializer: KSerializer, + default: () -> Map + ): PropertyDelegateProvider>> + { + return PropertyDelegateProvider { thisRef, property -> + val delegate = object : ReadWriteProperty> + { + override fun getValue(thisRef: Any?, property: KProperty<*>): MutableMap + { + loadFromStack() + return ObservableMap(runCatching { + NBT.decodeFromNbtTagRootless(MapSerializer(String.serializer(), serializer), data.getOrPut(property.name.toSnakeCase()) { + NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), default()) + }) + }.recover { + val ret = default() + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), ret) + ret + }.getOrThrow().toMutableMap()) { map -> setValue(thisRef, property, map)} + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: MutableMap) + { + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), value) + saveToStack() + } + } + if (property.name.toSnakeCase() !in data) + delegate.setValue(thisRef, property, default().toMutableMap()) + + delegate + } + } + + override fun itemField(size: Int): PropertyDelegateProvider> + { + return PropertyDelegateProvider { thisRef, property -> + val onUpdate = { + saveToStack() + } + itemStorage[property.name.toSnakeCase()] = ArchieItemStorage(size, onUpdate) + ReadOnlyProperty { _, _ -> itemStorage[property.name.toSnakeCase()]!! } + } + } + + override fun fluidField(limit: Long, size: Int): PropertyDelegateProvider> + { + return PropertyDelegateProvider { thisRef, property -> + val onUpdate = { + saveToStack() + } + fluidStorage[property.name.toSnakeCase()] = ArchieFluidStorage(limit, size, onUpdate) + ReadOnlyProperty { _, _ -> fluidStorage[property.name.toSnakeCase()]!! } + } + } + + override fun energyField(capacity: Long): PropertyDelegateProvider> + { + return PropertyDelegateProvider { thisRef, property -> + val onUpdate = { + saveToStack() + } + energyStorage[property.name.toSnakeCase()] = ArchieEnergyStorage(capacity, onUpdate) + ReadOnlyProperty { _, _ -> energyStorage[property.name.toSnakeCase()]!! } + } + } + + override fun loadFromTag(compoundTag: CompoundTag) + { + forEachTag(compoundTag) { (key, value) -> + data[key] = value + } + itemStorage.forEach { (key, value) -> + value.readSnapshot(data.getOrPut(key) { + value.createSnapshot() + }) + } + fluidStorage.forEach { (key, value) -> + value.readSnapshot(data.getOrPut(key) { + value.createSnapshot() + }) + } + energyStorage.forEach { (key, value) -> + value.readSnapshot(data.getOrPut(key) { + value.createSnapshot() + }) + } + } + + override fun saveToTag(compoundTag: CompoundTag) + { + mergeToCompoundTag(compoundTag) { + itemStorage.forEach { (key, value) -> + data[key] = value.createSnapshot() + } + fluidStorage.forEach { (key, value) -> + data[key] = value.createSnapshot() + } + energyStorage.forEach { (key, value) -> + data[key] = value.createSnapshot() + } + data.forEach { (key, value) -> + put(key, value) + + } + } + } + + fun loadFromStack() + { + stack.get(DataComponents.CUSTOM_DATA)?.apply { + loadFromTag(copyTag()) + } + } + + fun saveToStack() + { + stack.applyComponents(buildComponentPatch { + set(DataComponents.CUSTOM_DATA, CustomData.of(CompoundTag().also { tag -> + saveToTag(tag) + })) + }) + } + + override fun getSyncTag(): CompoundTag + { + return CompoundTag() + } + + override fun updateProperty(propertyName: String, serializer: KSerializer, value: T) + { + this.data[propertyName] = NBT.encodeToNbtTagRootless(serializer, value) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt new file mode 100644 index 000000000..e97bfe7b7 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt @@ -0,0 +1,348 @@ +package net.kernelpanicsoft.archie.serialization + +import net.kernelpanicsoft.archie.config.toSnakeCase +import net.kernelpanicsoft.archie.gui.item.SyncedItemHolder +import net.kernelpanicsoft.archie.transfer.ArchieEnergyStorage +import net.kernelpanicsoft.archie.transfer.ArchieFluidStorage +import net.kernelpanicsoft.archie.transfer.ArchieItemStorage +import earth.terrarium.common_storage_lib.storage.base.UpdateManager +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import net.benwoodworth.knbt.NbtTag +import net.minecraft.core.component.DataComponents +import net.minecraft.nbt.CompoundTag +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.component.CustomData +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadOnlyProperty +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty +import kotlin.reflect.full.hasAnnotation + +/** + * [NBTHolder] implementation backing [NBTHolder.item], persisting field values into [stack]'s + * [CustomData] component instead of an in-memory map. + * + * `@Sync`-annotated fields additionally push updates through [SyncedItemHolder] when the + * delegating `thisRef` implements it - the [ItemStack]-holder equivalent of [NBTHolderImpl]'s + * `thisRef is BlockEntity` handling. + */ +class ItemStackNBTHolderImpl(private val stack: ItemStack) : NBTHolder +{ + private val data: MutableMap = mutableMapOf() + private val itemStorage: MutableMap = mutableMapOf() + private val fluidStorage: MutableMap = mutableMapOf() + private val energyStorage: MutableMap = mutableMapOf() + private val sync: MutableSet = mutableSetOf() + + init + { + loadFromStack() + } + + override fun field( + serializer: KSerializer, + default: () -> T + ): PropertyDelegateProvider> + { + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is SyncedItemHolder) + thisRef.registerSyncedProperty(property.name.toSnakeCase(), serializer) + } + + val delegate = object : ReadWriteProperty + { + override fun getValue(thisRef: Any?, property: KProperty<*>): T + { + loadFromStack() + return runCatching { + NBT.decodeFromNbtTagRootless(serializer, data.getOrPut(property.name.toSnakeCase()) { + NBT.encodeToNbtTagRootless(serializer, default()) + }) + }.recover { + val ret = default() + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(serializer, ret) + ret + }.getOrThrow() + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) + { + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(serializer, value) + if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) + thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), serializer, value) + saveToStack() + } + } + if (property.name.toSnakeCase() !in data) + delegate.setValue(thisRef, property, default()) + else if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) + // Value pre-existed on the stack, so setValue() above never ran - announce it now + // so a menu opened against pre-existing data doesn't start out unsynced. + thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), serializer, delegate.getValue(thisRef, property)) + + delegate + } + } + + override fun listField( + serializer: KSerializer, + default: () -> List + ): PropertyDelegateProvider>> + { + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is SyncedItemHolder) + thisRef.registerSyncedProperty(property.name.toSnakeCase(), ListSerializer(serializer)) + } + + val delegate = object : ReadWriteProperty> + { + override fun getValue(thisRef: Any?, property: KProperty<*>): MutableList + { + loadFromStack() + return ObservableList(runCatching { + NBT.decodeFromNbtTagRootless(ListSerializer(serializer), data.getOrPut(property.name.toSnakeCase()) { + NBT.encodeToNbtTagRootless(ListSerializer(serializer), default()) + }) + }.recover { + val ret = default() + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(ListSerializer(serializer), ret) + ret + }.getOrThrow().toMutableList()) { list -> setValue(thisRef, property, list) } + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: MutableList) + { + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(ListSerializer(serializer), value) + if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) + thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ListSerializer(serializer), value.toList()) + saveToStack() + } + } + if (property.name.toSnakeCase() !in data) + delegate.setValue(thisRef, property, default().toMutableList()) + else if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) + // See the equivalent branch in field() above - same pre-existing-data gap. + thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ListSerializer(serializer), delegate.getValue(thisRef, property).toList()) + + delegate + } + } + + override fun mapField( + serializer: KSerializer, + default: () -> Map + ): PropertyDelegateProvider>> + { + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is SyncedItemHolder) + thisRef.registerSyncedProperty(property.name.toSnakeCase(), MapSerializer(String.serializer(), serializer)) + } + + val delegate = object : ReadWriteProperty> + { + override fun getValue(thisRef: Any?, property: KProperty<*>): MutableMap + { + loadFromStack() + return ObservableMap(runCatching { + NBT.decodeFromNbtTagRootless(MapSerializer(String.serializer(), serializer), data.getOrPut(property.name.toSnakeCase()) { + NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), default()) + }) + }.recover { + val ret = default() + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), ret) + ret + }.getOrThrow().toMutableMap()) { map -> setValue(thisRef, property, map) } + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: MutableMap) + { + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), value) + if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) + thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), MapSerializer(String.serializer(), serializer), value.toMap()) + saveToStack() + } + } + if (property.name.toSnakeCase() !in data) + delegate.setValue(thisRef, property, default().toMutableMap()) + else if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) + // See the equivalent branch in field() above - same pre-existing-data gap. + thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), MapSerializer(String.serializer(), serializer), delegate.getValue(thisRef, property).toMap()) + + delegate + } + } + + override fun itemField(size: Int): PropertyDelegateProvider> + { + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is SyncedItemHolder) + thisRef.registerSyncedProperty(property.name.toSnakeCase(), ArchieItemStorage.serializer()) + } + val onUpdate = { + if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) + thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ArchieItemStorage.serializer(), itemStorage[property.name.toSnakeCase()]!!) + saveToStack() + } + val storage = ArchieItemStorage(size, onUpdate) + // init { loadFromStack() } already ran (before this delegate even existed to be + // hydrated by loadFromTag's itemStorage.forEach loop, unlike a BlockEntity's field + // declarations - which all run in its constructor, before NBTBlockEntity.load() ever + // calls loadFromTag) - so data may already hold this key's raw tag with nothing to + // apply it to yet. Apply it now, directly, instead. + data[property.name.toSnakeCase()]?.let { storage.readSnapshot(it) } + itemStorage[property.name.toSnakeCase()] = storage + // readSnapshot() above never calls onUpdate, so announce the starting contents now. + if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) + thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ArchieItemStorage.serializer(), storage) + ReadOnlyProperty { _, _ -> itemStorage[property.name.toSnakeCase()]!! } + } + } + + override fun fluidField(limit: Long, size: Int): PropertyDelegateProvider> + { + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is SyncedItemHolder) + thisRef.registerSyncedProperty(property.name.toSnakeCase(), ArchieFluidStorage.serializer()) + } + val onUpdate = { + if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) + thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ArchieFluidStorage.serializer(), fluidStorage[property.name.toSnakeCase()]!!) + saveToStack() + } + val storage = ArchieFluidStorage(limit, size, onUpdate) + data[property.name.toSnakeCase()]?.let { storage.readSnapshot(it) } + fluidStorage[property.name.toSnakeCase()] = storage + // See itemField() above - same "storage's initial contents never announced" gap. + if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) + thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ArchieFluidStorage.serializer(), storage) + ReadOnlyProperty { _, _ -> fluidStorage[property.name.toSnakeCase()]!! } + } + } + + override fun energyField(capacity: Long): PropertyDelegateProvider> + { + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is SyncedItemHolder) + thisRef.registerSyncedProperty(property.name.toSnakeCase(), ArchieEnergyStorage.serializer()) + } + val onUpdate = { + if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) + thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ArchieEnergyStorage.serializer(), energyStorage[property.name.toSnakeCase()]!!) + saveToStack() + } + val storage = ArchieEnergyStorage(capacity, onUpdate) + data[property.name.toSnakeCase()]?.let { storage.readSnapshot(it) } + energyStorage[property.name.toSnakeCase()] = storage + // See itemField() above - same "storage's initial contents never announced" gap. + if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) + thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ArchieEnergyStorage.serializer(), storage) + ReadOnlyProperty { _, _ -> energyStorage[property.name.toSnakeCase()]!! } + } + } + + override fun loadFromTag(compoundTag: CompoundTag) + { + forEachTag(compoundTag) { (key, value) -> + data[key] = value + } + itemStorage.forEach { (key, value) -> + value.readSnapshot(data.getOrPut(key) { + value.createSnapshot() + }) + } + fluidStorage.forEach { (key, value) -> + value.readSnapshot(data.getOrPut(key) { + value.createSnapshot() + }) + } + energyStorage.forEach { (key, value) -> + value.readSnapshot(data.getOrPut(key) { + value.createSnapshot() + }) + } + } + + override fun saveToTag(compoundTag: CompoundTag) + { + mergeToCompoundTag(compoundTag) { + itemStorage.forEach { (key, value) -> + data[key] = value.createSnapshot() + } + fluidStorage.forEach { (key, value) -> + data[key] = value.createSnapshot() + } + energyStorage.forEach { (key, value) -> + data[key] = value.createSnapshot() + } + data.forEach { (key, value) -> + put(key, value) + + } + } + } + + fun loadFromStack() + { + stack.get(DataComponents.CUSTOM_DATA)?.apply { + loadFromTag(copyTag()) + } + } + + fun saveToStack() + { + stack.applyComponents(buildComponentPatch { + set(DataComponents.CUSTOM_DATA, CustomData.of(CompoundTag().also { tag -> + saveToTag(tag) + })) + }) + } + + override fun getSyncTag(): CompoundTag + { + return buildCompoundTag { + data.filter { (key, _) -> key in sync } + .forEach { (key, value) -> put(key, value) } + } + } + + override fun updateProperty(propertyName: String, serializer: KSerializer, value: T) + { + // Storage-backed fields (item/fluid/energy) are canonically the *live* storage object, not + // `data` - write through readSnapshot(), or saveToTag() below would just re-derive `data` + // from the untouched live storage and clobber this write. + val storage: UpdateManager? = itemStorage[propertyName] ?: fluidStorage[propertyName] ?: energyStorage[propertyName] + if (storage != null && value is UpdateManager<*>) + { + @Suppress("UNCHECKED_CAST") + storage.readSnapshot((value as UpdateManager).createSnapshot()) + } + else + { + this.data[propertyName] = NBT.encodeToNbtTagRootless(serializer, value) + } + // Unlike NBTHolderImpl's `data` (a BlockEntity's own persisted state), `data` here is only + // a transient copy - must be flushed to the stack explicitly or a remote edit is lost. + saveToStack() + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/KOps.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/KOps.kt new file mode 100644 index 000000000..acaee5b5b --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/KOps.kt @@ -0,0 +1,674 @@ +package net.kernelpanicsoft.archie.serialization + +import com.mojang.datafixers.util.Pair +import com.mojang.serialization.DataResult +import com.mojang.serialization.DynamicOps +import com.mojang.serialization.MapLike +import kotlinx.serialization.json.* +import net.benwoodworth.knbt.* +import net.minecraft.nbt.ListTag +import net.minecraft.nbt.NumericTag +import net.peanuuutz.tomlkt.* +import java.math.BigDecimal +import java.nio.ByteBuffer +import java.util.* +import java.util.function.BiConsumer +import java.util.function.Consumer +import java.util.stream.IntStream +import java.util.stream.LongStream +import java.util.stream.Stream + +/** + * Mojang [DynamicOps] implementations for the tree formats used elsewhere in this package + * ([kotlinx.serialization.json.JsonElement], [net.peanuuutz.tomlkt.TomlElement], and knbt's + * [net.benwoodworth.knbt.NbtTag]), so [com.mojang.serialization.Codec]s can operate on them + * directly. Registered with [SerializationManager] and used internally by [SerializerCodec] + * and [CodecSerializer]; not usually needed directly. + */ +object KOps +{ + /** [DynamicOps] over [JsonElement]. */ + object Json : DynamicOps + { + override fun empty(): JsonElement = JsonNull + + override fun convertTo(outOps: DynamicOps, input: JsonElement): U + { + when (input) + { + is JsonObject -> return convertMap(outOps, input) + is JsonArray -> return convertList(outOps, input) + is JsonNull -> return outOps.empty() + else -> + { + val literal = input.jsonPrimitive + if (literal.isString) + return outOps.createString(literal.content) + literal.booleanOrNull?.let { return outOps.createBoolean(it) } + val decimal = BigDecimal(literal.content) + try + { + return when (val long = decimal.longValueExact()) + { + long.toByte().toLong() -> outOps.createByte(long.toByte()) + long.toShort().toLong() -> outOps.createShort(long.toShort()) + long.toInt().toLong() -> outOps.createInt(long.toInt()) + else -> outOps.createLong(long) + } + } catch (e: ArithmeticException) + { + return when (val double = decimal.toDouble()) + { + double.toFloat().toDouble() -> outOps.createFloat(double.toFloat()) + else -> outOps.createDouble(double) + } + } + } + } + } + + override fun getNumberValue(input: JsonElement): DataResult + { + val literal = input.jsonPrimitive + try + { + val decimal = BigDecimal(literal.content) + + try + { + return when (val long = decimal.longValueExact()) + { + long.toByte().toLong() -> DataResult.success(long.toByte()) + long.toShort().toLong() -> DataResult.success(long.toShort()) + long.toInt().toLong() -> DataResult.success(long.toInt()) + else -> DataResult.success(long) + } + } catch (e: ArithmeticException) + { + return when (val double = decimal.toDouble()) + { + double.toFloat().toDouble() -> DataResult.success(double.toFloat()) + else -> DataResult.success(double) + } + } + } + catch (e: NumberFormatException) + { + return DataResult.error { "Not a number: $input" } + } + } + + override fun createNumeric(i: Number): JsonElement + { + return JsonPrimitive(i) + } + + override fun getBooleanValue(input: JsonElement): DataResult + { + val literal = input.jsonPrimitive + literal.booleanOrNull?.let { return DataResult.success(it) } ?: return DataResult.error { "Not a boolean: $input" } + + } + + override fun createBoolean(value: Boolean): JsonElement + { + return JsonPrimitive(value) + } + + override fun getStringValue(input: JsonElement): DataResult + { + val literal = input.jsonPrimitive + literal.contentOrNull?.takeIf { literal.isString }?.let { return DataResult.success(it) } ?: return DataResult.error { "Not a string: $input" } + } + + override fun createString(value: String): JsonElement + { + return JsonPrimitive(value) + } + + override fun mergeToList(list: JsonElement, value: JsonElement): DataResult + { + if (list !is JsonArray && list != empty()) + return DataResult.error({ "mergeToList called with not a list: $list" }, list) + if (list != empty()) + { + return DataResult.success(JsonArray(list.jsonArray + value)) + } + return DataResult.success(JsonArray(listOf(value))) + } + + override fun mergeToMap(map: JsonElement, key: JsonElement, value: JsonElement): DataResult + { + if (map !is JsonObject && map != empty()) + return DataResult.error({ "mergeToMap called with not a map: $map" }, map) + if (key !is JsonPrimitive || !key.jsonPrimitive.isString) + return DataResult.error({ "key is not a string: $key" }, map) + if (map != empty()) + { + return DataResult.success(JsonObject(map.jsonObject + mapOf(key.content to value))) + } + return DataResult.success(JsonObject(mapOf(key.content to value))) + } + + override fun getMapValues(input: JsonElement): DataResult>> + { + if (input !is JsonObject) return DataResult.error { "Not a json object: $input" } + return DataResult.success(input.entries.stream().map { (key, value) -> Pair(createString(key), value) }) + } + + override fun getMapEntries(input: JsonElement): DataResult>> + { + if (input !is JsonObject) return DataResult.error { "Not a json object: $input" } + return DataResult.success(Consumer { c -> + input.entries.forEach { (key, value) -> c.accept(createString(key), value) } + }) + } + + override fun getMap(input: JsonElement): DataResult> + { + if (input !is JsonObject) return DataResult.error { "Not a json object: $input" } + return DataResult.success(object : MapLike + { + override fun get(key: JsonElement): JsonElement? + { + return input[key.jsonPrimitive.content] + } + + override fun get(key: String): JsonElement? + { + return input[key] + } + + override fun entries(): Stream> + { + return input.entries.stream().map { (key, value) -> Pair(createString(key), value) } + } + }) + } + + override fun createMap(map: Stream>): JsonElement + { + return JsonObject(map.map { it.first.jsonPrimitive.content to it.second }.toList().toMap()) + } + + override fun getStream(input: JsonElement): DataResult> + { + return if (input is JsonArray) + DataResult.success(input.stream()) + else + DataResult.error { "Not a json array: $input" } + } + + override fun getList(input: JsonElement): DataResult>> + { + return if (input is JsonArray) + DataResult.success(Consumer { c -> + input.forEach { + c.accept(it) + } + }) + else + DataResult.error { "Not a json array: $input" } + } + + override fun createList(input: Stream): JsonElement + { + return JsonArray(input.toList()) + } + + override fun remove(input: JsonElement, key: String): JsonElement + { + if (input is JsonObject) + { + return JsonObject(input - key) + } + return input + } + } + + /** [DynamicOps] over [TomlElement]. */ + object Toml : DynamicOps + { + private fun Number.toTomlElement(): TomlElement + { + return when (this) + { + is Byte -> TomlLiteral(this) + is Short -> TomlLiteral(this) + is Int -> TomlLiteral(this) + is Long -> TomlLiteral(this) + is Float -> TomlLiteral(this) + is Double -> TomlLiteral(this) + else -> error("Unsupported class: ${this::class.simpleName}") + } + } + + override fun empty(): TomlElement = TomlNull + + override fun convertTo(outOps: DynamicOps, input: TomlElement): U + { + when (input) + { + is TomlTable -> return convertMap(outOps, input) + is TomlArray -> return convertList(outOps, input) + is TomlNull -> return outOps.empty() + else -> + { + val literal = input.asTomlLiteral() + if (literal.type == TomlLiteral.Type.String) + return outOps.createString(literal.toString()) + if (literal.type == TomlLiteral.Type.Boolean) + return outOps.createBoolean(literal.toBoolean()) + val decimal = BigDecimal(literal.content) + try + { + return when (val long = decimal.longValueExact()) + { + long.toByte().toLong() -> outOps.createByte(long.toByte()) + long.toShort().toLong() -> outOps.createShort(long.toShort()) + long.toInt().toLong() -> outOps.createInt(long.toInt()) + else -> outOps.createLong(long) + } + } catch (e: ArithmeticException) + { + return when (val double = decimal.toDouble()) + { + double.toFloat().toDouble() -> outOps.createFloat(double.toFloat()) + else -> outOps.createDouble(double) + } + } + } + } + } + + override fun getNumberValue(input: TomlElement): DataResult + { + val literal = input.asTomlLiteral() + try + { + val decimal = BigDecimal(literal.content) + + try + { + return when (val long = decimal.longValueExact()) + { + long.toByte().toLong() -> DataResult.success(long.toByte()) + long.toShort().toLong() -> DataResult.success(long.toShort()) + long.toInt().toLong() -> DataResult.success(long.toInt()) + else -> DataResult.success(long) + } + } catch (e: ArithmeticException) + { + return when (val double = decimal.toDouble()) + { + double.toFloat().toDouble() -> DataResult.success(double.toFloat()) + else -> DataResult.success(double) + } + } + } + catch (e: NumberFormatException) + { + return DataResult.error { "Not a number: $input" } + } + } + + override fun createNumeric(i: Number): TomlElement + { + return i.toTomlElement() + } + + override fun getBooleanValue(input: TomlElement): DataResult + { + val literal = input.asTomlLiteral() + return if (literal.type == TomlLiteral.Type.Boolean) + DataResult.success(literal.toBoolean()) + else + DataResult.error { "Not a boolean: $input" } + } + + override fun createBoolean(value: Boolean): TomlElement + { + return TomlLiteral(value) + } + + override fun getStringValue(input: TomlElement): DataResult + { + val literal = input.asTomlLiteral() + return if (literal.type == TomlLiteral.Type.String) + DataResult.success(literal.toString()) + else + DataResult.error { "Not a string: $input" } + } + + override fun createString(value: String): TomlElement + { + return TomlLiteral(value) + } + + override fun mergeToList(list: TomlElement, value: TomlElement): DataResult + { + if (list !is TomlArray && list != empty()) + return DataResult.error({ "mergeToList called with not a list: $list" }, list) + if (list != empty()) + { + return DataResult.success(TomlArray(list.asTomlArray().plus(value))) + } + return DataResult.success(TomlArray(value)) + } + + override fun mergeToMap(map: TomlElement, key: TomlElement, value: TomlElement): DataResult + { + if (map !is TomlTable && map != empty()) + return DataResult.error({ "mergeToMap called with not a map: $map" }, map) + if (key !is TomlLiteral || key.asTomlLiteral().type != TomlLiteral.Type.String) + return DataResult.error({ "key is not a string: $key" }, map) + if (map != empty()) + { + return DataResult.success(TomlTable(map.asTomlTable().plus(mapOf(key.content to value)))) + } + return DataResult.success(TomlTable(mapOf(key.content to value))) + } + + override fun getMapValues(input: TomlElement): DataResult>> + { + if (input !is TomlTable) return DataResult.error { "Not a toml table: $input" } + return DataResult.success(input.entries.stream().map { (key, value) -> Pair(createString(key), value) }) + } + + override fun getMapEntries(input: TomlElement): DataResult>> + { + if (input !is TomlTable) return DataResult.error { "Not a toml table: $input" } + return DataResult.success(Consumer { c -> + input.entries.forEach { entry -> c.accept(createString(entry.key), entry.value) } + }) + } + + override fun getMap(input: TomlElement): DataResult> + { + if (input !is TomlTable) return DataResult.error { "Not a toml table: $input" } + return DataResult.success(object : MapLike + { + override fun get(key: TomlElement): TomlElement? + { + return input[key] + } + + override fun get(key: String): TomlElement? + { + return input[key] + } + + override fun entries(): Stream> + { + return input.entries.stream().map { (key, value) -> Pair(createString(key), value) } + } + }) + } + + override fun createMap(map: Stream>): TomlElement + { + return TomlTable(map.toList().associate { it.first.asTomlLiteral().toString() to it.second }) + } + + override fun getStream(input: TomlElement): DataResult> + { + return if (input is TomlArray) + DataResult.success(input.stream()) + else + DataResult.error { "Not a toml array: $input" } + } + + override fun getList(input: TomlElement): DataResult>> + { + return if (input is TomlArray) + DataResult.success(Consumer { c -> + input.forEach { + c.accept(it) + } + }) + else + DataResult.error { "Not a toml array: $input" } + } + + override fun createList(input: Stream): TomlElement + { + return TomlArray(input.toList()) + } + + override fun remove(input: TomlElement, key: String): TomlElement + { + if (input is TomlTable) + { + return TomlTable(input.minus(key)) + } + return input + } + } + + /** [DynamicOps] over knbt's [NbtTag]. */ + object Nbt : DynamicOps + { + override fun empty(): NbtTag? = null + + override fun convertTo(outOps: DynamicOps, input: NbtTag?): U + { + return when (input) + { + null -> outOps.empty() + is NbtByte -> outOps.createByte(input.value) + is NbtShort -> outOps.createShort(input.value) + is NbtInt -> outOps.createInt(input.value) + is NbtLong -> outOps.createLong(input.value) + is NbtFloat -> outOps.createFloat(input.value) + is NbtDouble -> outOps.createDouble(input.value) + is NbtByteArray -> outOps.createByteList(ByteBuffer.wrap(input.toByteArray())) + is NbtString -> outOps.createString(input.value) + is NbtList<*> -> convertList(outOps, input) + is NbtCompound -> convertMap(outOps, input) + is NbtIntArray -> outOps.createIntList(Arrays.stream(input.toIntArray())) + is NbtLongArray -> outOps.createLongList(Arrays.stream(input.toLongArray())) + } + } + + override fun getNumberValue(input: NbtTag): DataResult + { + return input.toMinecraft.takeIf { it is NumericTag }?.let { DataResult.success((it as NumericTag).asNumber) } ?: DataResult.error { "Not a number" } + } + + override fun createNumeric(i: Number): NbtTag + { + return NbtDouble(i.toDouble()) + } + + override fun createByte(value: Byte): NbtTag + { + return NbtByte(value) + } + + override fun createShort(value: Short): NbtTag + { + return NbtShort(value) + } + + override fun createInt(value: Int): NbtTag + { + return NbtInt(value) + } + + override fun createLong(value: Long): NbtTag + { + return NbtLong(value) + } + + override fun createFloat(value: Float): NbtTag + { + return NbtFloat(value) + } + + override fun createDouble(value: Double): NbtTag + { + return NbtDouble(value) + } + + override fun createBoolean(value: Boolean): NbtTag + { + return NbtByte(value) + } + + override fun getStringValue(input: NbtTag): DataResult + { + return input.takeIf { it is NbtString }?.nbtString?.let { DataResult.success(it.value) } ?: DataResult.error { "Not a string: $input" } + } + + override fun createString(value: String): NbtTag + { + return NbtString(value) + } + + private operator fun NbtList.Companion.invoke(content: List): NbtList<*> = ListTag().apply { addAll(content.map { it.toMinecraft })}.fromMinecraft!! + + override fun mergeToList(list: NbtTag?, value: NbtTag): DataResult + { + if (list !is NbtList<*> && list != empty()) + return DataResult.error({ "mergeToList called with not a list: $list" }, list) + if (list != empty()) + { + return DataResult.success(NbtList(list!!.nbtList + value)) + } + return DataResult.success(NbtList(listOf(value))) + } + + override fun mergeToMap(map: NbtTag?, key: NbtTag, value: NbtTag): DataResult + { + if (map !is NbtCompound && map != empty()) + return DataResult.error({ "mergeToMap called with not a map: $map" }, map) + if (key !is NbtString) + return DataResult.error({ "key is not a string: $key" }, map) + if (map != empty()) + { + return DataResult.success(NbtCompound(map!!.nbtCompound + mapOf(key.value to value))) + } + return DataResult.success(NbtCompound(mapOf(key.value to value))) + } + + override fun getMapValues(input: NbtTag): DataResult>> + { + if (input !is NbtCompound) return DataResult.error { "Not an nbt compound: $input" } + return DataResult.success(input.entries.stream().map { (key, value) -> Pair(createString(key), value) }) + } + + override fun getMapEntries(input: NbtTag): DataResult>> + { + if (input !is NbtCompound) return DataResult.error { "Not an nbt compound: $input" } + return DataResult.success(Consumer { c -> + input.entries.forEach { (key, value) -> c.accept(createString(key), value) } + }) + } + + override fun getMap(input: NbtTag): DataResult> + { + if (input !is NbtCompound) return DataResult.error { "Not an nbt compound: $input" } + return DataResult.success(object : MapLike + { + override fun get(key: NbtTag): NbtTag? + { + return input[key.nbtString.value] + } + + override fun get(key: String): NbtTag? + { + return input[key] + } + + override fun entries(): Stream> + { + return input.entries.stream().map { (key, value) -> Pair(createString(key), value) } + } + }) + } + + override fun createMap(map: Stream>): NbtTag + { + return NbtCompound(map.toList().associate { it.first.nbtString.value to it.second }) + } + + override fun getStream(input: NbtTag): DataResult> + { + return if (input is NbtList<*>) + DataResult.success(input.stream()) + else + DataResult.error { "Not an nbt list: $input" } + } + + override fun getList(input: NbtTag): DataResult>> + { + return if (input is NbtList<*>) + DataResult.success(Consumer { c -> + input.forEach { + c.accept(it) + } + }) + else + DataResult.error { "Not an nbt list: $input" } + } + + override fun createList(input: Stream): NbtTag + { + return NbtList(input.toList()) + } + + override fun remove(input: NbtTag, key: String): NbtTag + { + if (input is NbtCompound) + { + return NbtCompound(input - key) + } + return input + } + + override fun getByteBuffer(input: NbtTag): DataResult + { + if (input is NbtByteArray) + { + return DataResult.success(ByteBuffer.wrap(input.toByteArray())) + } + return super.getByteBuffer(input) + } + + override fun createByteList(input: ByteBuffer): NbtTag + { + val byteBuffer: ByteBuffer = input.duplicate().clear() + val bs = ByteArray(input.capacity()) + byteBuffer[0, bs, 0, bs.size] + return NbtByteArray(bs) + } + + override fun getIntStream(input: NbtTag): DataResult + { + if (input is NbtIntArray) + { + return DataResult.success(Arrays.stream(input.toIntArray())) + } + return super.getIntStream(input) + } + + override fun createIntList(input: IntStream): NbtTag + { + return NbtIntArray(input.toArray()) + } + + override fun getLongStream(input: NbtTag): DataResult + { + if (input is NbtLongArray) + { + return DataResult.success(Arrays.stream(input.toLongArray())) + } + return super.getLongStream(input) + } + + override fun createLongList(input: LongStream): NbtTag + { + return NbtLongArray(input.toArray()) + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBT.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBT.kt new file mode 100644 index 000000000..9cbff4cec --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBT.kt @@ -0,0 +1,270 @@ +package net.kernelpanicsoft.archie.serialization + +import net.kernelpanicsoft.archie.util.toMutableEntry +import kotlinx.serialization.DeserializationStrategy +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.InternalSerializationApi +import kotlinx.serialization.SerializationStrategy +import kotlinx.serialization.descriptors.StructureKind +import kotlinx.serialization.internal.AbstractPolymorphicSerializer +import kotlinx.serialization.serializer +import net.benwoodworth.knbt.* +import net.minecraft.core.component.DataComponentPatch +import net.minecraft.nbt.* +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.experimental.ExperimentalTypeInference + +/** A [Nbt] (knbt) instance pre-configured for Minecraft's Java-edition NBT format, uncompressed. */ +val NBT = Nbt { + variant = NbtVariant.Java + compression = NbtCompression.None +} + +/** + * Like [Nbt.encodeToNbtTag], but for class/polymorphic types unwraps the single top-level + * compound entry keyed by the serial name, returning its value directly instead of a + * one-entry [NbtCompound] wrapper. + */ +@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) +fun Nbt.encodeToNbtTagRootless(serializer: SerializationStrategy, value: T): NbtTag +{ + return if (serializer.descriptor.kind == StructureKind.CLASS || + serializer is AbstractPolymorphicSerializer + ) + encodeToNbtTag(serializer, value).nbtCompound[serializer.descriptor.serialName]!! + else + encodeToNbtTag(serializer, value) +} + +/** + * The inverse of [encodeToNbtTagRootless]: decodes [tag] as [T], re-wrapping it in a one-entry + * compound keyed by the serial name first if [T] is a class/polymorphic type. + */ +@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) +fun Nbt.decodeFromNbtTagRootless(deserializer: DeserializationStrategy, tag: NbtTag): T +{ + return if (deserializer.descriptor.kind == StructureKind.CLASS || + deserializer is AbstractPolymorphicSerializer + ) + decodeFromNbtTag(deserializer, buildNbtCompound { + put(deserializer.descriptor.serialName, tag) + }) + else + decodeFromNbtTag(deserializer, tag) +} + +/** Reified variant of [encodeToNbtTagRootless] that resolves [T]'s serializer automatically. */ +@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) +inline fun Nbt.encodeToNbtTagRootless(value: T): NbtTag = + encodeToNbtTagRootless(serializersModule.serializer(), value) + +/** Reified variant of [decodeFromNbtTagRootless] that resolves [T]'s serializer automatically. */ +@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) +inline fun Nbt.decodeFromNbtTagRootless(tag: NbtTag): T = + decodeFromNbtTagRootless(serializersModule.serializer(), tag) + + +/** Builds a Minecraft [ListTag] using knbt's [NbtListBuilder] DSL via [builderAction]. */ +@OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class) +inline fun buildListTag( + @BuilderInference builderAction: NbtListBuilder.() -> Unit, +): ListTag +{ + contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } + return buildNbtList(builderAction).toMinecraft +} + +/** Builds a Minecraft [CompoundTag] using knbt's [NbtCompoundBuilder] DSL via [builderAction]. */ +@OptIn(ExperimentalContracts::class) +inline fun buildCompoundTag(builderAction: NbtCompoundBuilder.() -> Unit): CompoundTag +{ + contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } + return buildNbtCompound(builderAction).toMinecraft +} + +/** Builds entries via knbt's [NbtCompoundBuilder] DSL and puts each of them into the existing [compoundTag]. */ +@OptIn(ExperimentalContracts::class) +inline fun mergeToCompoundTag(compoundTag: CompoundTag, builderAction: NbtCompoundBuilder.() -> Unit) +{ + contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } + buildNbtCompound(builderAction).forEach { (key, value) -> + compoundTag.put(key, value.toMinecraft) + } +} + +/** Runs [action] for each element of [listTag], converted to a knbt [NbtTag]. No-op for an empty/untyped list. */ +@OptIn(ExperimentalContracts::class) +inline fun forEachTag(listTag: ListTag, action: (NbtTag) -> Unit) +{ + contract { callsInPlace(action, InvocationKind.UNKNOWN) } + listTag.fromMinecraft?.forEach { value -> + action(value) + } +} + +/** Runs [action] for each key/value entry of [compoundTag], with the value converted to a knbt [NbtTag]. */ +@OptIn(ExperimentalContracts::class) +inline fun forEachTag(compoundTag: CompoundTag, action: (Map.Entry) -> Unit) +{ + contract { callsInPlace(action, InvocationKind.UNKNOWN) } + compoundTag.fromMinecraft.forEach { (key, value) -> + action((key to value).toMutableEntry()) + } +} + +/** Builds a [DataComponentPatch] using Minecraft's [DataComponentPatch.Builder] DSL via [builderAction]. */ +@OptIn(ExperimentalContracts::class) +inline fun buildComponentPatch(builderAction: DataComponentPatch.Builder.() -> Unit): DataComponentPatch +{ + contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } + return DataComponentPatch.builder().apply(builderAction).build() +} + +/** Converts a knbt tag to the equivalent Minecraft [Tag] (a `null` receiver becomes [EndTag]). */ +val NbtTag?.toMinecraft: Tag + get() = when (this) + { + null -> EndTag.INSTANCE + is NbtByte -> ByteTag.valueOf(value) + is NbtByteArray -> ByteArrayTag(this) + is NbtCompound -> toMinecraft + is NbtDouble -> DoubleTag.valueOf(value) + is NbtFloat -> FloatTag.valueOf(value) + is NbtInt -> IntTag.valueOf(value) + is NbtIntArray -> IntArrayTag(this) + is NbtList<*> -> toMinecraft + is NbtLong -> LongTag.valueOf(value) + is NbtLongArray -> LongArrayTag(this) + is NbtShort -> ShortTag.valueOf(value) + is NbtString -> StringTag.valueOf(value) + } + +/** Converts a knbt [NbtCompound] to the equivalent Minecraft [CompoundTag]. */ +val NbtCompound.toMinecraft: CompoundTag + get() = CompoundTag().apply { + mapValues { it.value.toMinecraft }.forEach { (key, value) -> + put(key, value) + } + } + +/** Converts a knbt [NbtList] to the equivalent Minecraft [ListTag]. */ +val NbtList<*>.toMinecraft: ListTag + get() = ListTag().apply { + this@toMinecraft.map { + it.toMinecraft + }.forEach { + add(it) + } + } + +/** Converts a Minecraft [Tag] to the equivalent knbt tag, or `null` for an [EndTag]. */ +val Tag.fromMinecraft: NbtTag? + get() = when (id.toInt()) + { + 0 -> null + 1 -> NbtByte((this as NumericTag).asByte) + 2 -> NbtShort((this as NumericTag).asShort) + 3 -> NbtInt((this as NumericTag).asInt) + 4 -> NbtLong((this as NumericTag).asLong) + 5 -> NbtFloat((this as NumericTag).asFloat) + 6 -> NbtDouble((this as NumericTag).asDouble) + 7 -> NbtByteArray((this as ByteArrayTag).asByteArray) + 8 -> NbtString(this.asString) + 9 -> (this as ListTag).fromMinecraft + 10 -> (this as CompoundTag).fromMinecraft + 11 -> NbtIntArray((this as IntArrayTag).asIntArray) + 12 -> NbtLongArray((this as LongArrayTag).asLongArray) + else -> throw IllegalStateException("Unknown tag type: $this") + } + +/** Converts a Minecraft [CompoundTag] to the equivalent knbt [NbtCompound]. */ +val CompoundTag.fromMinecraft: NbtCompound + get() = buildNbtCompound { + this@fromMinecraft.allKeys.associateWith { + this@fromMinecraft[it]?.fromMinecraft + }.forEach { (key, value) -> + if (value != null) + put(key, value) + } + } +/** Converts a Minecraft [ListTag] to the equivalent knbt [NbtList], or `null` for an untyped (empty) list. */ +val ListTag.fromMinecraft: NbtList<*>? + get() = when (elementType.toInt()) + { + 0 -> null + 1 -> buildNbtList { + forEach { + add(it.fromMinecraft as NbtByte) + } + } + + 2 -> buildNbtList { + forEach { + add(it.fromMinecraft as NbtShort) + } + } + + 3 -> buildNbtList { + forEach { + add(it.fromMinecraft as NbtInt) + } + } + + 4 -> buildNbtList { + forEach { + add(it.fromMinecraft as NbtLong) + } + } + + 5 -> buildNbtList { + forEach { + add(it.fromMinecraft as NbtFloat) + } + } + + 6 -> buildNbtList { + forEach { + add(it.fromMinecraft as NbtDouble) + } + } + + 7 -> buildNbtList { + forEach { + add(it.fromMinecraft as NbtByteArray) + } + } + + 8 -> buildNbtList { + forEach { + add(it.fromMinecraft as NbtString) + } + } + + 9 -> buildNbtList> { + forEach { + add(it.fromMinecraft as NbtList<*>) + } + } + + 10 -> buildNbtList { + forEach { + add(it.fromMinecraft as NbtCompound) + } + } + + 11 -> buildNbtList { + forEach { + add(it.fromMinecraft as NbtIntArray) + } + } + + 12 -> buildNbtList { + forEach { + add(it.fromMinecraft as NbtLongArray) + } + } + + else -> throw IllegalStateException("Unknown tag type: $this") + } \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt new file mode 100644 index 000000000..586e6f3b5 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt @@ -0,0 +1,119 @@ +package net.kernelpanicsoft.archie.serialization + +import net.kernelpanicsoft.archie.transfer.ArchieEnergyStorage +import net.kernelpanicsoft.archie.transfer.ArchieFluidStorage +import net.kernelpanicsoft.archie.transfer.ArchieItemStorage +import dev.architectury.fluid.FluidStack +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.serializer +import net.minecraft.nbt.CompoundTag +import net.minecraft.world.item.ItemStack +import kotlin.collections.listOf +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadOnlyProperty +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.full.memberProperties +import kotlin.reflect.jvm.isAccessible + +/** + * An interface for managing NBT-backed fields on block entities, item stacks, or fluid stacks. + * + * [NBTHolder] provides a property-delegation API that serializes field values to/from a + * [CompoundTag] using kotlinx.serialization. Each delegated field is keyed by its Kotlin + * property name. + * + * ### Usage on a block entity + * ```kotlin + * class MyBlockEntity(pos, state) : NBTBlockEntity(pos, state) { + * var count by nbt.intField() + * var label by nbt.stringField { "default" } + * val items by nbt.itemField(9) // 9-slot inventory + * val tank by nbt.fluidField(FluidStack.bucketAmount() * 4) // 1 tank slot, 4 buckets + * val energy by nbt.energyField(10_000) // a single energy buffer + * } + * ``` + * + * Obtain instances via [NBTHolder.create], [NBTHolder.item], or [NBTHolder.fluid]. + */ +@Suppress("unused") +interface NBTHolder +{ + /** + * Declares a read-write field backed by [serializer], keyed by the delegated property's name. + * [default] supplies the value used before the field has been loaded/set. + */ + fun field(serializer: KSerializer, default: () -> T): PropertyDelegateProvider> + + /** Declares a mutable-list field backed by [serializer], keyed by the delegated property's name. */ + fun listField(serializer: KSerializer, default: () -> List): PropertyDelegateProvider>> + /** Declares a mutable-map (keyed by [String]) field backed by [serializer], keyed by the delegated property's name. */ + fun mapField(serializer: KSerializer, default: () -> Map): PropertyDelegateProvider>> + + /** Declares an [ArchieItemStorage] field with [size] slots, keyed by the delegated property's name. */ + fun itemField(size: Int): PropertyDelegateProvider> + + /** Declares an [ArchieFluidStorage] field with [size] tank slots each capped at [limit], keyed by the delegated property's name. */ + fun fluidField(limit: Long, size: Int = 1): PropertyDelegateProvider> + + /** Declares an [ArchieEnergyStorage] field capped at [capacity], keyed by the delegated property's name. */ + fun energyField(capacity: Long): PropertyDelegateProvider> + + fun booleanField(default: () -> Boolean = { false }): PropertyDelegateProvider> = field(Boolean.serializer(), default) + fun byteField(default: () -> Byte = { 0 }): PropertyDelegateProvider> = field(Byte.serializer(), default) + fun ubyteField(default: () -> UByte = { 0u }): PropertyDelegateProvider> = field(UByte.serializer(), default) + fun shortField(default: () -> Short = { 0 }): PropertyDelegateProvider> = field(Short.serializer(), default) + fun ushortField(default: () -> UShort = { 0u }): PropertyDelegateProvider> = field(UShort.serializer(), default) + fun intField(default: () -> Int = { 0 }): PropertyDelegateProvider> = field(Int.serializer(), default) + fun uintField(default: () -> UInt = { 0u }): PropertyDelegateProvider> = field(UInt.serializer(), default) + fun longField(default: () -> Long = { 0 }): PropertyDelegateProvider> = field(Long.serializer(), default) + fun ulongField(default: () -> ULong = { 0u }): PropertyDelegateProvider> = field(ULong.serializer(), default) + fun floatField(default: () -> Float = { 0.0f }): PropertyDelegateProvider> = field(Float.serializer(), default) + fun doubleField(default: () -> Double = { 0.0 }): PropertyDelegateProvider> = field(Double.serializer(), default) + fun stringField(default: () -> String = { "" }): PropertyDelegateProvider> = field(String.serializer(), default) + + /** Loads every declared field's value from [compoundTag], overwriting current values. */ + fun loadFromTag(compoundTag: CompoundTag) + + /** Writes every declared field's current value into [compoundTag]. */ + fun saveToTag(compoundTag: CompoundTag) + + /** Builds a [CompoundTag] suitable for sending to the client to sync current field values. */ + fun getSyncTag(): CompoundTag + + /** Updates a single field, identified by [propertyName], from a client sync payload. */ + fun updateProperty(propertyName: String, serializer: KSerializer, value: T) + + companion object + { + /** Creates a standalone [NBTHolder] not backed by any particular [ItemStack]/[FluidStack]. */ + fun create(): NBTHolder = NBTHolderImpl() + + /** Creates an [NBTHolder] whose fields are persisted to [stack]'s NBT. */ + fun item(stack: ItemStack): NBTHolder = ItemStackNBTHolderImpl(stack) + + /** Creates an [NBTHolder] for [stack] and immediately runs [block] against it. */ + fun item(stack: ItemStack, block: NBTHolder.() -> R): R + { + return item(stack).block() + } + + /** Creates an [NBTHolder] whose fields are persisted to [stack]'s NBT. */ + fun fluid(stack: FluidStack): NBTHolder = FluidStackNBTHolderImpl(stack) + + /** Creates an [NBTHolder] for [stack] and immediately runs [block] against it. */ + fun fluid(stack: FluidStack, block: NBTHolder.() -> R): R + { + return fluid(stack).block() + } + } +} + +/** Reified variant of [NBTHolder.field] that resolves the [KSerializer] for [T] automatically. */ +inline fun NBTHolder.field(noinline default: () -> T): PropertyDelegateProvider> = field(serializer(), default) +/** Reified variant of [NBTHolder.listField] that resolves the [KSerializer] for [T] automatically. */ +inline fun NBTHolder.listField(noinline default: () -> List): PropertyDelegateProvider>> = listField(serializer(), default) +/** Reified variant of [NBTHolder.mapField] that resolves the [KSerializer] for [T] automatically. */ +inline fun NBTHolder.mapField(noinline default: () -> Map): PropertyDelegateProvider>> = mapField(serializer(), default) \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt new file mode 100644 index 000000000..5c080277c --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt @@ -0,0 +1,304 @@ +package net.kernelpanicsoft.archie.serialization + +import net.kernelpanicsoft.archie.config.toSnakeCase +import net.kernelpanicsoft.archie.transfer.ArchieEnergyStorage +import net.kernelpanicsoft.archie.transfer.ArchieFluidStorage +import net.kernelpanicsoft.archie.transfer.ArchieItemStorage +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.serializer +import net.benwoodworth.knbt.NbtTag +import net.kernelpanicsoft.archie.gui.blockentity.getStateContainer +import net.minecraft.nbt.CompoundTag +import net.minecraft.world.level.block.entity.BlockEntity +import kotlin.properties.PropertyDelegateProvider +import kotlin.properties.ReadOnlyProperty +import kotlin.properties.ReadWriteProperty +import kotlin.reflect.KProperty +import kotlin.reflect.full.hasAnnotation + +/** + * Default [NBTHolder] implementation backing [NBTHolder.create]. Field values are cached + * in-memory as knbt tags keyed by the delegated property's snake_case name; properties + * annotated [Sync] additionally push updates through [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager]-backed state + * containers when the holder is attached to a [BlockEntity]. + */ +class NBTHolderImpl : NBTHolder +{ + private val data: MutableMap = mutableMapOf() + private val itemStorage: MutableMap = mutableMapOf() + private val fluidStorage: MutableMap = mutableMapOf() + private val energyStorage: MutableMap = mutableMapOf() + private val sync: MutableSet = mutableSetOf() + + override fun field( + serializer: KSerializer, + default: () -> T + ): PropertyDelegateProvider> + { + + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is BlockEntity) + { + thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), serializer) + } + } + + val delegate = object : ReadWriteProperty + { + override fun getValue(thisRef: Any?, property: KProperty<*>): T + { + return runCatching { + NBT.decodeFromNbtTagRootless(serializer, data.getOrPut(property.name.toSnakeCase()) { + NBT.encodeToNbtTagRootless(serializer, default()) + }) + }.recover { + val ret = default() + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(serializer, ret) + ret + }.getOrThrow() + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) + { + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(serializer, value) + if (thisRef is BlockEntity) + { + if (property.name.toSnakeCase() in sync) + thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), value) + thisRef.setChanged() + } + } + } + if (property.name.toSnakeCase() !in data) + delegate.setValue(thisRef, property, default()) + delegate + } + } + + override fun listField( + serializer: KSerializer, + default: () -> List + ): PropertyDelegateProvider>> + { + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is BlockEntity) + { + thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), serializer) + } + } + val delegate = object : ReadWriteProperty> + { + override fun getValue(thisRef: Any?, property: KProperty<*>): MutableList + { + return ObservableList(runCatching { + NBT.decodeFromNbtTagRootless(ListSerializer(serializer), data.getOrPut(property.name.toSnakeCase()) { + NBT.encodeToNbtTagRootless(ListSerializer(serializer), default()) + }) + }.recover { + val ret = default() + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(ListSerializer(serializer), ret) + ret + }.getOrThrow().toMutableList()) { list -> setValue(thisRef, property, list) } + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: MutableList) + { + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(ListSerializer(serializer), value) + if (thisRef is BlockEntity) + { + if (property.name.toSnakeCase() in sync) + thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), value) + thisRef.setChanged() + } + } + } + if (property.name.toSnakeCase() !in data) + delegate.setValue(thisRef, property, default().toMutableList()) + delegate + } + } + + override fun mapField( + serializer: KSerializer, + default: () -> Map + ): PropertyDelegateProvider>> + { + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is BlockEntity) + { + thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), serializer) + } + } + val delegate = object : ReadWriteProperty> + { + override fun getValue(thisRef: Any?, property: KProperty<*>): MutableMap + { + return ObservableMap(runCatching { + NBT.decodeFromNbtTagRootless(MapSerializer(String.serializer(), serializer), data.getOrPut(property.name.toSnakeCase()) { + NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), default()) + }) + }.recover { + val ret = default() + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), ret) + ret + }.getOrThrow().toMutableMap()) { map -> setValue(thisRef, property, map) } + } + + override fun setValue(thisRef: Any?, property: KProperty<*>, value: MutableMap) + { + data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), value) + if (thisRef is BlockEntity) + { + if (property.name.toSnakeCase() in sync) + thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), value) + thisRef.setChanged() + } + } + } + if (property.name.toSnakeCase() !in data) + delegate.setValue(thisRef, property, default().toMutableMap()) + delegate + } + } + + override fun itemField(size: Int): PropertyDelegateProvider> + { + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is BlockEntity) + { + thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), ArchieItemStorage.serializer()) + } + } + val onUpdate = when (thisRef) + { + is BlockEntity -> ({ + if (property.name.toSnakeCase() in sync) + thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), itemStorage[property.name.toSnakeCase()]) + thisRef.setChanged() + }) + else -> ({}) + } + itemStorage[property.name.toSnakeCase()] = ArchieItemStorage(size, onUpdate) + ReadOnlyProperty { _, _ -> itemStorage[property.name.toSnakeCase()]!! } + } + } + + override fun fluidField(limit: Long, size: Int): PropertyDelegateProvider> + { + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is BlockEntity) + { + thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), ArchieFluidStorage.serializer()) + } + } + val onUpdate = when (thisRef) + { + is BlockEntity -> ({ + if (property.name.toSnakeCase() in sync) + thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), fluidStorage[property.name.toSnakeCase()]) + thisRef.setChanged() + }) + else -> ({}) + } + fluidStorage[property.name.toSnakeCase()] = ArchieFluidStorage(limit, size, onUpdate) + ReadOnlyProperty { _, _ -> fluidStorage[property.name.toSnakeCase()]!! } + } + } + + override fun energyField(capacity: Long): PropertyDelegateProvider> + { + return PropertyDelegateProvider { thisRef, property -> + if (property.hasAnnotation()) + { + sync += property.name.toSnakeCase() + if (thisRef is BlockEntity) + { + thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), ArchieEnergyStorage.serializer()) + } + } + val onUpdate = when (thisRef) + { + is BlockEntity -> ({ + if (property.name.toSnakeCase() in sync) + thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), energyStorage[property.name.toSnakeCase()]) + thisRef.setChanged() + }) + else -> ({}) + } + energyStorage[property.name.toSnakeCase()] = ArchieEnergyStorage(capacity, onUpdate) + ReadOnlyProperty { _, _ -> energyStorage[property.name.toSnakeCase()]!! } + } + } + + override fun loadFromTag(compoundTag: CompoundTag) + { + forEachTag(compoundTag) { (key, value) -> + data[key] = value + } + itemStorage.forEach { (key, value) -> + value.readSnapshot(data.getOrPut(key) { + value.createSnapshot() + }) + } + fluidStorage.forEach { (key, value) -> + value.readSnapshot(data.getOrPut(key) { + value.createSnapshot() + }) + } + energyStorage.forEach { (key, value) -> + value.readSnapshot(data.getOrPut(key) { + value.createSnapshot() + }) + } + } + + override fun saveToTag(compoundTag: CompoundTag) + { + mergeToCompoundTag(compoundTag) { + itemStorage.forEach { (key, value) -> + data[key] = value.createSnapshot() + } + fluidStorage.forEach { (key, value) -> + data[key] = value.createSnapshot() + } + energyStorage.forEach { (key, value) -> + data[key] = value.createSnapshot() + } + data.forEach { (key, value) -> + put(key, value) + } + } + } + + override fun getSyncTag(): CompoundTag + { + return buildCompoundTag { + data.filter { (key, _) -> key in sync } + .forEach { (key, value) -> + put(key, value) + } + } + } + + override fun updateProperty(propertyName: String, serializer: KSerializer, value: T) + { + this.data[propertyName] = NBT.encodeToNbtTagRootless(serializer, value) + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableList.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableList.kt new file mode 100644 index 000000000..adc0bf7c4 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableList.kt @@ -0,0 +1,98 @@ +package net.kernelpanicsoft.archie.serialization + +import java.util.function.IntFunction +import java.util.function.Predicate +import java.util.function.UnaryOperator + +/** + * A [MutableList] wrapper that invokes [listener] with the underlying [list] after every + * mutating operation. Used by [listField]-style [NBTHolder] delegates to detect changes and + * persist/sync them. + */ +class ObservableList(private val list: MutableList, private val listener: (MutableList) -> Unit) : MutableList by list +{ + override fun add(element: T): Boolean + { + return list.add(element).also { listener(list) } + } + + override fun add(index: Int, element: T) + { + return list.add(index, element).also { listener(list) } + } + + override fun remove(element: T): Boolean + { + return list.remove(element).also { listener(list) } + } + + override fun removeAt(index: Int): T + { + return list.removeAt(index).also { listener(list) } + } + + override fun addAll(elements: Collection): Boolean + { + return list.addAll(elements).also { listener(list) } + } + + override fun removeAll(elements: Collection): Boolean + { + return list.removeAll(elements).also { listener(list) } + } + + override fun set(index: Int, element: T): T + { + return list.set(index, element).also { listener(list) } + } + + override fun clear() + { + list.clear().also { listener(list) } + } + + override fun addAll(index: Int, elements: Collection): Boolean + { + return list.addAll(index, elements).also { listener(list) } + } + + override fun removeIf(filter: Predicate): Boolean + { + return list.removeIf(filter).also { listener(list) } + } + + override fun retainAll(elements: Collection): Boolean + { + return list.retainAll(elements).also { listener(list) } + } + + override fun replaceAll(operator: UnaryOperator) + { + list.replaceAll(operator).also { listener(list) } + } + + override fun sort(c: Comparator?) + { + list.sortWith(c!!).also { listener(list) } + } + + override fun addFirst(e: T) + { + list.addFirst(e).also { listener(list) } + } + + override fun addLast(e: T) + { + list.addLast(e).also { listener(list) } + } + + override fun removeFirst(): T + { + return list.removeFirst().also { listener(list) } + } + + override fun removeLast(): T + { + return list.removeLast().also { listener(list) } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableMap.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableMap.kt new file mode 100644 index 000000000..ce5e32100 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableMap.kt @@ -0,0 +1,84 @@ +package net.kernelpanicsoft.archie.serialization + +import java.util.function.BiFunction +import java.util.function.Function + +/** + * A [MutableMap] wrapper that invokes [listener] with the underlying [map] after every + * mutating operation. Used by [mapField]-style [NBTHolder] delegates to detect changes and + * persist/sync them. + */ +class ObservableMap(private val map: MutableMap, private val listener: (MutableMap) -> Unit) : MutableMap by map +{ + override fun put(key: K, value: V): V? + { + return map.put(key, value).also { listener(map) } + } + + override fun remove(key: K): V? + { + return map.remove(key).also { listener(map) } + } + + override fun clear() + { + map.clear().also { listener(map) } + } + + override fun putAll(from: Map) + { + map.putAll(from).also { listener(map) } + } + + override fun remove(key: K, value: V): Boolean + { + return map.remove(key, value).also { listener(map) } + } + + override fun replace(key: K, value: V): V? + { + return map.replace(key, value).also { listener(map) } + } + + override fun replace(key: K, oldValue: V, newValue: V): Boolean + { + return map.replace(key, oldValue, newValue).also { listener(map) } + } + + override fun replaceAll(function: BiFunction) + { + map.replaceAll(function).also { listener(map) } + } + + override fun computeIfAbsent(key: K, mappingFunction: Function): V + { + return map.computeIfAbsent(key, mappingFunction).also { listener(map) } + } + + override fun putIfAbsent(key: K, value: V): V? + { + return map.putIfAbsent(key, value).also { listener(map) } + } + + override fun computeIfPresent( + key: K, + remappingFunction: BiFunction + ): V? + { + return map.computeIfPresent(key, remappingFunction).also { listener(map) } + } + + override fun compute(key: K, remappingFunction: BiFunction): V? + { + return map.compute(key, remappingFunction).also { listener(map) } + } + + override fun merge( + key: K, + value: V & Any, + remappingFunction: BiFunction + ): V? + { + return map.merge(key, value, remappingFunction).also { listener(map) } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/SerializationManager.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/SerializationManager.kt new file mode 100644 index 000000000..20771408a --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/SerializationManager.kt @@ -0,0 +1,391 @@ +package net.kernelpanicsoft.archie.serialization + +import com.google.gson.JsonParser +import com.mojang.serialization.Codec +import com.mojang.serialization.DynamicOps +import com.mojang.serialization.JsonOps +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.cbor.Cbor +import kotlinx.serialization.cbor.CborDecoder +import kotlinx.serialization.cbor.CborEncoder +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonDecoder +import kotlinx.serialization.json.JsonElement +import kotlinx.serialization.json.JsonEncoder +import kotlinx.serialization.modules.SerializersModule +import kotlinx.serialization.modules.SerializersModuleBuilder +import kotlinx.serialization.modules.overwriteWith +import net.benwoodworth.knbt.* +import net.kernelpanicsoft.archie.serialization.SerializationManager.SerializationManagerBuilder.DynamicOpRegistryBuilder +import net.kernelpanicsoft.archie.serialization.SerializationManager.SerializationManagerBuilder.SerializerRegistryBuilder +import net.kernelpanicsoft.archie.serialization.serializers.BuiltInSerializersModule +import net.kernelpanicsoft.archie.serialization.serializers.MinecraftSerializersModule +import net.minecraft.nbt.NbtOps +import net.minecraft.nbt.Tag +import net.minecraft.resources.DelegatingOps +import kotlin.reflect.KClass +import com.google.gson.JsonElement as GsonElement + +internal typealias EncodeDynamicOp = (input: Any, strategy: KSerializer) -> T +internal typealias DecodeDynamicOp = (input: T, strategy: KSerializer) -> Any + +internal typealias EncodeSerializer = (T, Codec, Any) -> Unit +internal typealias DecodeSerializer = (T, Codec) -> Any + +/** + * Manages serializers for KLib, allowing customization and extension. + * + * ### Adding Serializers + * KLib respects your serializer annotations, because of this you can use the `@Serializable()` annotation: + * ```kotlin + * data class MyData(@Serializable(MyClassSerializer::class) val field: MyClass) + * ``` + * Otherwise, you can mark it as a `@Contextual` serializer and specify it in the [SerializationManager]: + * ```kotlin + * data class MyData(@Contextual val field: MyClass) + * + * val myModule = SerializersModule { + * contextual(MyClass::class, MyClassSerializer) + * } + * + * SerializationManager { + * module { + * include(myModule) + * } + * } + * ``` + * + * ### Overwriting Existing Serializers + * To overwrite existing serializers: + * ```kotlin + * val myModule = SerializersModule { + * contextual(ResourceLocation::class, CustomResourceLocationSerializer) + * } + * + * SerializationManager overwriteWith myModule + * ``` + */ +@OptIn(ExperimentalSerializationApi::class) +object SerializationManager { + private var sharedModule: SerializersModule = SerializersModule { + include(MinecraftSerializersModule) + include(BuiltInSerializersModule) + } + + private fun createCbor() = Cbor { serializersModule = sharedModule } + private fun createJson() = Json { + ignoreUnknownKeys = true + explicitNulls = false + serializersModule = sharedModule + } + + private fun createNbt() = Nbt { + variant = NbtVariant.Java + compression = NbtCompression.None + serializersModule = sharedModule + } + + /** The shared [Cbor] instance, reconfigured with [sharedModule] whenever [overwriteWith] or [invoke] runs. */ + var cbor: Cbor = createCbor() + private set + /** The shared [Json] instance (unknown keys ignored, nulls omitted), kept in sync with [sharedModule]. */ + var json: Json = createJson() + private set + /** The shared Java-edition, uncompressed [Nbt] instance, kept in sync with [sharedModule]. */ + var nbt: Nbt = createNbt() + private set + + // ConcurrentHashMap/CopyOnWriteArrayList: mods can call registerDynamicOp/registerSerializer + // concurrently during parallel mod init, and lookups happen far more often than registrations. + private val ops: MutableMap, DynamicOpRegistryBuilder.Operation> = + java.util.concurrent.ConcurrentHashMap() + internal val serializers: MutableList = + java.util.concurrent.CopyOnWriteArrayList() + + private fun rebuild() { + cbor = createCbor() + json = createJson() + nbt = createNbt() + } + + /** + * Overwrites existing serializers with the provided module. + * + * @param module The new `SerializersModule` to use. + */ + infix fun overwriteWith(module: SerializersModule) { + sharedModule = sharedModule overwriteWith module + rebuild() + } + + /** + * Retrieves the ops registry for a specific [DynamicOps] instance. + * + * @param op The [DynamicOps] instance. + * @return A registry that can encode/decode the `DynamicOp`, or `null` if not found. + */ + operator fun get(op: DynamicOps<*>): DynamicOpRegistryBuilder.Operation? + { + if (op is DelegatingOps<*>) { + // Handle DelegatingOps by checking the delegate + return get(op.delegate) + } + return ops[op] + } + + /** + * Retrieves the serializer registry for a specific [Encoder]. + * + * @param encoder The [Encoder] interface. + * @return A registry that contains the [Encoder], or `null` if not found. + */ + operator fun get(encoder: Encoder) = serializers.find { it.operation.encoder.isInstance(encoder) } + + /** + * Retrieves the serializer registry for a specific [Decoder]. + * + * @param decoder The [Decoder] interface. + * @return A registry that contains the [Decoder], or `null` if not found. + */ + operator fun get(decoder: Decoder) = serializers.find { it.operation.decoder.isInstance(decoder) } + + /** + * Configures the [SerializationManager] + */ + operator fun invoke(block: SerializationManagerBuilder.() -> Unit) { + SerializationManagerBuilder().block() + rebuild() + } + + class SerializationManagerBuilder { + /** + * Uses the [SerializersModuleBuilder] to configure the shared module between all serializer instances. + * + * @param block A builder function for creating a [SerializersModule] + */ + fun module(block: SerializersModuleBuilder.() -> Unit) { + sharedModule = sharedModule.overwriteWith(SerializersModule { block() }) + } + + /** + * Registers a new [DynamicOps] instance with its associated encode and decode functions. + * This is used when you convert a [KSerializer] into a [Codec] via [SerializerCodec]. + * + * ### Usage + * ```kotlin + * registerDynamicOp(JsonOps.INSTANCE) { + * encode { input, strategy -> json.encodeToJsonElement(strategy, input).toGson } + * decode { input, strategy -> + * require(input is GsonElement) { "Expected input of type JsonElement but received ${input.javaClass.simpleName}." } + * + * json.decodeFromJsonElement(strategy, input.toKson) + * } + * } + * ``` + * + * @param op The `DynamicOps` instance. + * @param block The builder function of the `DynamicOpRegistry` + */ + @Suppress("UNCHECKED_CAST") + fun registerDynamicOp( + op: DynamicOps, + block: DynamicOpRegistryBuilder.() -> Unit + ) { + val builder = DynamicOpRegistryBuilder().apply(block) + + ops[op] = builder.build() as DynamicOpRegistryBuilder.Operation + } + + /** + * Registers a new `Serializer` with its associated encode and decode functions. + * This is used when you convert a [Codec] into [KSerializer]. + * + * **Note:** Because a limitation of the [Codec] structure you need an "intermediary" such as [JsonElement] or [NbtTag] for example. + * + * ### Usage + * ```kotlin + * registerSerializer(JsonElement.serializer().descriptor) { + * encode(JsonEncoder::class) { encoder, codec, input -> + * encoder.encodeJsonElement(codec.encodeStart(KOps.Json, input).orThrow) + * } + * + * decode(JsonDecoder::class) { decoder, codec -> + * codec.parse(KOps.Json, decoder.decodeJsonElement()).orThrow + * } + * } + * ``` + * + * @param descriptor The descriptor of the serializer + * @param name An optional name for the element in the [CodecSerializer] descriptor + * @param block The builder function of the `SerializerRegistry` + */ + fun registerSerializer( + descriptor: SerialDescriptor, + name: String? = null, + block: SerializerRegistryBuilder.() -> Unit + ) { + val builder = SerializerRegistryBuilder().apply(block) + + serializers.add( + SerializerRegistryBuilder.Registry( + descriptor, + name, + builder.build() + ) + ) + } + + class DynamicOpRegistryBuilder { + data class Operation( + val encode: EncodeDynamicOp, + val decode: DecodeDynamicOp + ) + + private var encode: EncodeDynamicOp? = null + private var decode: DecodeDynamicOp? = null + + /** + * Defines the encoder for this `DynamicOp` + * + * @param block The function used for encoding data + */ + fun encode(block: EncodeDynamicOp) { + encode = block + } + + /** + * Defines the decoder for this `DynamicOp` + * + * @param block The function used for decoding data + */ + fun decode(block: DecodeDynamicOp) { + decode = block + } + + internal fun build(): Operation { + requireNotNull(encode) { "Encode function must be provided before building the operation. Call `encode { ... }` to set it." } + requireNotNull(decode) { "Decode function must be provided before building the operation. Call `decode { ... }` to set it." } + + return Operation(encode!!, decode!!) + } + } + + class SerializerRegistryBuilder { + data class Registry( + val descriptor: SerialDescriptor, + val name: String? = null, + val operation: Operation + ) + + data class Operation( + val encoder: KClass, + val decoder: KClass, + val encode: EncodeSerializer, + val decode: DecodeSerializer + ) + + private var encoder: KClass? = null + private var encode: EncodeSerializer? = null + + private var decoder: KClass? = null + private var decode: DecodeSerializer? = null + + /** + * Defines the encoder for this `Serializer` + * + * @param block The function used for encoding data + */ + @Suppress("UNCHECKED_CAST") + fun encode(enc: KClass, block: EncodeSerializer) { + encoder = enc + encode = block as EncodeSerializer + } + + /** + * Defines the decoder for this `Serializer` + * + * @param block The function used for decoding data + */ + @Suppress("UNCHECKED_CAST") + fun decode(dec: KClass, block: DecodeSerializer) { + decoder = dec + decode = block as DecodeSerializer + } + + internal fun build(): Operation { + requireNotNull(encoder) { "Encoder must be provided before building the operation. Call `encode(...) { ... }` to set it." } + requireNotNull(encode) { "Encode function must be provided before building the operation. Call `encode(...) { ... }` to set it." } + requireNotNull(decoder) { "Decoder must be provided before building the operation. Call `decode(...) { ... }` to set it." } + requireNotNull(decode) { "Decode function must be provided before building the operation. Call `decode(...) { ... }` to set it." } + + return Operation( + encoder!!, + decoder!!, + encode!!, + decode!! + ) + } + } + } + + init { + SerializationManager { + registerDynamicOp(JsonOps.INSTANCE) { + encode { input, strategy -> json.encodeToJsonElement(strategy, input).toGson } + decode { input, strategy -> + require(input is GsonElement) { "Expected input of type JsonElement but received ${input.javaClass.simpleName}." } + + json.decodeFromJsonElement(strategy, input.toKson) + } + } + + registerDynamicOp(NbtOps.INSTANCE) { + encode { input, strategy -> nbt.encodeToNbtTag(strategy, input).toMinecraft } + decode { input, strategy -> + require(input is Tag) { "Expected input of type Tag but received ${input.javaClass.simpleName}." } + + nbt.decodeFromNbtTag( + strategy, + input.fromMinecraft ?: throw IllegalStateException("Failed to convert a Minecraft Tag into a KNbtTag.") + ) + } + } + + registerSerializer(JsonElement.serializer().descriptor, name = "JsonElement") { + encode(JsonEncoder::class) { encoder, codec, input -> + encoder.encodeJsonElement(codec.encodeStart(KOps.Json, input).orThrow) + } + + decode(JsonDecoder::class) { decoder, codec -> + codec.parse(KOps.Json, decoder.decodeJsonElement()).orThrow + } + } + + registerSerializer(NbtTag.serializer().descriptor, name = "NbtTag") { + encode(NbtEncoder::class) { encoder, codec, input -> + encoder.encodeNbtTag(codec.encodeStart(KOps.Nbt, input).orThrow) + } + + decode(NbtDecoder::class) { decoder, codec -> + codec.parse(KOps.Nbt, decoder.decodeNbtTag()).orThrow + } + } + + registerSerializer(String.serializer().descriptor, name = "CborElement") { + encode(CborEncoder::class) { encoder, codec, input -> + encoder.encodeString(codec.encodeStart(JsonOps.INSTANCE, input).orThrow.toString()) + } + + decode(CborDecoder::class) { decoder, codec -> + val str = decoder.decodeString() + codec.parse(JsonOps.INSTANCE, JsonParser.parseString(str)).orThrow + } + } + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Sync.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Sync.kt new file mode 100644 index 000000000..df22bbe8e --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Sync.kt @@ -0,0 +1,13 @@ +package net.kernelpanicsoft.archie.serialization + +/** + * Marks an [NBTHolder]-delegated property as one that should be synced from server to client. + * + * Checked by [NBTHolderImpl] (via reflection) when a delegate is created: an annotated property + * is registered with the owning block entity's state container + * ([net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateContainer], obtained through + * [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager]) so later writes push + * updates through that container instead of only being picked up via [NBTHolder.getSyncTag]. + */ +@Target(AnnotationTarget.PROPERTY) +annotation class Sync diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Utils.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Utils.kt new file mode 100644 index 000000000..eaa29989f --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Utils.kt @@ -0,0 +1,292 @@ +@file:Suppress("FunctionName", "unused") +@file:OptIn(ExperimentalSerializationApi::class) + +package net.kernelpanicsoft.archie.serialization + +import com.google.gson.JsonParser +import com.mojang.datafixers.util.Pair +import com.mojang.serialization.Codec +import com.mojang.serialization.DataResult +import com.mojang.serialization.DynamicOps +import kotlinx.serialization.* +import kotlinx.serialization.builtins.ArraySerializer +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.MapSerializer +import kotlinx.serialization.builtins.SetSerializer +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.json.JsonElement +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import kotlin.reflect.KClass +import com.google.gson.JsonElement as GsonElement + +/** + * Gets data from a [RegistryFriendlyByteBuf] using the provided [KSerializer] + */ +fun RegistryFriendlyByteBuf.read(serializer: KSerializer): T = + SerializationManager.cbor.decodeFromByteArray(serializer, readByteArray()) + +/** + * Writes data into a [RegistryFriendlyByteBuf] using the [KSerializer] using the class of the data + */ +@OptIn(InternalSerializationApi::class) +fun RegistryFriendlyByteBuf.write(data: T) = write(data::class.serializer() as KSerializer, data) + +/** + * Writes data into a [RegistryFriendlyByteBuf] using a [KSerializer] + */ +fun RegistryFriendlyByteBuf.write(serializer: KSerializer, data: T) = + writeBytes(SerializationManager.cbor.encodeToByteArray(serializer, data)) + +/** + * Converts a [Codec] into a [KSerializer]. + * + * **Note:** By default `JsonOps` and `NbtOps` are supported. Check [SerializationManager.SerializationManagerBuilder.registerDynamicOp] to register another [DynamicOps]. + * Trying to use unregistered [DynamicOps] implementations will result in an [UnsupportedOperationException]. + */ +val Codec.kSerializer: KSerializer + get() = CodecSerializer(this) + +/** + * Converts a [KSerializer] into a [Codec]. + * + * **Note:** By default `JsonOps` and `NbtOps` are supported. Check [SerializationManager.SerializationManagerBuilder.registerDynamicOp] to register another [DynamicOps]. + * Trying to use unregistered [DynamicOps] implementations will result in an [UnsupportedOperationException]. + * + * ### Example + * ```kotlin + * @Serializable + * data class TestData( + * val str: String, + * val int: Int, + * val float: Float, + * val double: Double, + * val boolean: Boolean + * ) + * + * // Using the toCodec extension: + * val TestCodec = TestData.serializer().toCodec() + * + * // The above is equivalent to manually creating a codec: + * val ManualTestCodec: Codec = RecordCodecBuilder.create { + * it.group( + * Codec.STRING.fieldOf("str").forGetter(TestData::str), + * Codec.INT.fieldOf("int").forGetter(TestData::int), + * Codec.FLOAT.fieldOf("float").forGetter(TestData::float), + * Codec.DOUBLE.fieldOf("double").forGetter(TestData::double), + * Codec.BOOL.fieldOf("boolean").forGetter(TestData::boolean) + * ).apply(it, ::TestData) + * } + * ``` + * + * @return A [Codec] of type `T` defined by the [KSerializer]. + * @throws UnsupportedOperationException If the provided `DynamicOps` type is not supported. + */ +val KSerializer.codec: Codec + get() = SerializerCodec(this) + +/** + * Converts any [KSerializer] into a [StreamCodec] using Cbor + */ +val KSerializer.streamCodec: StreamCodec + get() = StreamCodec.of( + { buffer, value -> buffer.writeByteArray(SerializationManager.cbor.encodeToByteArray(this, value)) }, + { buffer -> SerializationManager.cbor.decodeFromByteArray(this, buffer.readByteArray()) } + ) + +/** + * Returns serial descriptor that delegates all the calls to descriptor returned by [deferred] block. + * Used to resolve cyclic dependencies between recursive serializable structures. + */ +@OptIn(SealedSerializationApi::class) +fun defer(deferred: () -> SerialDescriptor): SerialDescriptor = object : SerialDescriptor { + + private val original: SerialDescriptor by lazy(deferred) + + override val serialName: String + get() = original.serialName + override val kind: SerialKind + get() = original.kind + override val elementsCount: Int + get() = original.elementsCount + override val isInline: Boolean + get() = original.isInline + override val isNullable: Boolean + get() = original.isNullable + override val annotations: List + get() = original.annotations + + override fun getElementName(index: Int): String = original.getElementName(index) + override fun getElementIndex(name: String): Int = original.getElementIndex(name) + override fun getElementAnnotations(index: Int): List = original.getElementAnnotations(index) + override fun getElementDescriptor(index: Int): SerialDescriptor = original.getElementDescriptor(index) + override fun isElementOptional(index: Int): Boolean = original.isElementOptional(index) +} + +/** + * Used for [KSerializer.codec], you could extend this class to make any modifications you like. + * + * **Note:** It is HIGHLY recommended to just use the extension function [KSerializer.codec] instead of manually using this class. + */ +open class SerializerCodec(private val serializer: KSerializer) : Codec { + @Suppress("UNCHECKED_CAST") + override fun encode(input: T, ops: DynamicOps, prefix: V): DataResult + { + return tryOrThrow { + val cod = SerializationManager[ops] + ?: throw UnsupportedOperationException("${ops::class.simpleName} is not a supported DynamicOps instance.") + + cod.encode(input, serializer as KSerializer) as V + } + } + + @Suppress("UNCHECKED_CAST") + override fun decode( + ops: DynamicOps, + input: V + ): DataResult> { + return tryOrThrow { + val cod = SerializationManager[ops] + ?: throw UnsupportedOperationException("${ops::class.simpleName} is not a supported DynamicOps instance.") + + val value = cod.decode(input, serializer as KSerializer) as T + + Pair(value, input) + } + } +} + +internal fun tryOrThrow(action: () -> T): DataResult { + return try { + DataResult.success(action()) + } catch (err: Exception) { + DataResult.error(err::message) + } +} + +/** + * Used for [Codec.kSerializer], you could extend this class to make any modifications you like. + * + * **Note:** It is HIGHLY recommended to just use the extension function [Codec.kSerializer] instead of manually using this class. + */ +@Suppress("UNCHECKED_CAST") +open class CodecSerializer(private val codec: Codec) : KSerializer { + @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) + override val descriptor: SerialDescriptor = defer { + buildSerialDescriptor("CodecSerializer", PolymorphicKind.SEALED) { + SerializationManager.serializers.forEach { + element(it.name ?: it.descriptor.serialName, defer { it.descriptor }) + } + } + } + + override fun serialize(encoder: Encoder, value: T) { + val ser = SerializationManager[encoder] + ?: throw UnsupportedOperationException("${encoder::class.simpleName} is not a supported serializer type.") + + ser.operation.encode(encoder, codec as Codec, value as Any) + } + + override fun deserialize(decoder: Decoder): T { + val ser = SerializationManager[decoder] + ?: throw UnsupportedOperationException("${decoder::class.simpleName} is not a supported serializer type.") + + return ser.operation.decode(decoder, codec as Codec) as T + } +} + +/** + * Convert any [JsonElement] from kotlinx.serialization.json into [GsonElement] from gson + */ +val JsonElement.toGson: GsonElement + get() = JsonParser.parseString(this.toString()) + +/** + * Convert any [GsonElement] from gson into [JsonElement] kotlinx.serialization.json + */ +val GsonElement.toKson: JsonElement + get() = SerializationManager.json.parseToJsonElement(this.toString()) + +/** + * Returns serializer for reference [Array] of type [E] with [descriptor][SerialDescriptor] of [StructureKind.LIST] kind. + * Each element of the array is serialized with the given [elementSerializer]. + * + * [KSerializer.descriptor] is deferred to resolve cyclic dependencies + */ +@ExperimentalSerializationApi +inline fun DeferredArraySerializer(elementSerializer: KSerializer): KSerializer> = + DeferredArraySerializer(T::class, elementSerializer) + +/** + * Returns serializer for reference [Array] of type [E] with [descriptor][SerialDescriptor] of [StructureKind.LIST] kind. + * Each element of the array is serialized with the given [elementSerializer]. + * + * [KSerializer.descriptor] is deferred to resolve cyclic dependencies + */ +@ExperimentalSerializationApi +fun DeferredArraySerializer( + kClass: KClass, + elementSerializer: KSerializer +): KSerializer> = object : KSerializer> +{ + private val surrogate by lazy { ArraySerializer(kClass, elementSerializer) } + + override val descriptor: SerialDescriptor = defer { surrogate.descriptor } + + override fun deserialize(decoder: Decoder): Array = surrogate.deserialize(decoder) + + override fun serialize(encoder: Encoder, value: Array) = surrogate.serialize(encoder, value) +} + +/** + * Creates a serializer for [`List`][List] for the given serializer of type [T]. + * + * [KSerializer.descriptor] is deferred to resolve cyclic dependencies + */ +fun DeferredListSerializer(elementSerializer: KSerializer): KSerializer> = object : KSerializer> +{ + private val surrogate by lazy { ListSerializer(elementSerializer) } + + override val descriptor: SerialDescriptor = defer { surrogate.descriptor } + + override fun deserialize(decoder: Decoder): List = surrogate.deserialize(decoder) + + override fun serialize(encoder: Encoder, value: List) = surrogate.serialize(encoder, value) +} + +/** + * Creates a serializer for [`Set`][Set] for the given serializer of type [T]. + * + * [KSerializer.descriptor] is deferred to resolve cyclic dependencies + */ +fun DeferredSetSerializer(elementSerializer: KSerializer): KSerializer> = object : KSerializer> +{ + private val surrogate by lazy { SetSerializer(elementSerializer) } + + override val descriptor: SerialDescriptor = defer { surrogate.descriptor } + + override fun deserialize(decoder: Decoder): Set = surrogate.deserialize(decoder) + + override fun serialize(encoder: Encoder, value: Set) = surrogate.serialize(encoder, value) +} + +/** + * Creates a serializer for [`Map`][Map] for the given serializers for + * its ket type [K] and value type [V]. + * + * [KSerializer.descriptor] is deferred to resolve cyclic dependencies + */ +fun DeferredMapSerializer( + keySerializer: KSerializer, + valueSerializer: KSerializer +): KSerializer> = object : KSerializer> +{ + private val surrogate by lazy { MapSerializer(keySerializer, valueSerializer) } + override val descriptor: SerialDescriptor = defer { surrogate.descriptor } + + override fun deserialize(decoder: Decoder): Map = surrogate.deserialize(decoder) + + override fun serialize(encoder: Encoder, value: Map) = surrogate.serialize(encoder, value) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/BuiltinSerializers.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/BuiltinSerializers.kt new file mode 100644 index 000000000..4aa240425 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/BuiltinSerializers.kt @@ -0,0 +1,134 @@ +package net.kernelpanicsoft.archie.serialization.serializers + +import com.mojang.blaze3d.platform.InputConstants.Type +import kotlinx.serialization.Contextual +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.modules.SerializersModule +import me.shedaniel.clothconfig2.api.Modifier +import me.shedaniel.clothconfig2.api.ModifierKeyCode +import me.shedaniel.math.Color +import net.kernelpanicsoft.archie.util.onClient + +/* ------------------ TypeAliases ------------------ */ + +/** + * Contextual type-alias for Cloth Config's [ModifierKeyCode] that uses [ModifierKeyCodeSerializer] + * when the field is annotated with `@Contextual`. + */ +typealias SModifierKeyCode = @Contextual ModifierKeyCode +/** + * Contextual type-alias for Cloth Config's [Color] that uses [ColorSerializer] when the field + * is annotated with `@Contextual`. + */ +typealias SColor = @Contextual Color + +/* ------------------ Serializers ------------------ */ + +/** A [KSerializer] for Cloth Config's [ModifierKeyCode] (a keybind plus its held modifier). */ +object ModifierKeyCodeSerializer : KSerializer +{ + @Serializable + enum class KeyType(val type: Type) + { + KEYSYM(Type.KEYSYM), + SCANCODE(Type.SCANCODE), + MOUSE(Type.MOUSE); + + companion object + { + fun forType(type: Type): KeyType + { + return when (type) + { + Type.KEYSYM -> KEYSYM + Type.SCANCODE -> SCANCODE + Type.MOUSE -> MOUSE + } + } + } + } + + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ModifierKeyCode") + { + element("type") + element("key_code") + element("modifier", isOptional = true) + } + + override fun deserialize(decoder: Decoder): ModifierKeyCode + { + return decoder.decodeStructure(descriptor) + { + var type: KeyType = KeyType.KEYSYM + var keyCode = 0 + var modifier: Short = 0 + while (true) + { + when (val index = decodeElementIndex(descriptor)) + { + 0 -> type = decodeSerializableElement(descriptor, index, KeyType.serializer()) + 1 -> keyCode = decodeIntElement(descriptor, index) + 2 -> modifier = decodeShortElement(descriptor, index) + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + if (keyCode == -1) + ModifierKeyCode.unknown() + else + ModifierKeyCode.of(type.type.getOrCreate(keyCode), Modifier.of(modifier)) + } + } + + override fun serialize(encoder: Encoder, value: ModifierKeyCode) + { + encoder.encodeStructure(descriptor) + { + encodeSerializableElement(descriptor, 0, KeyType.serializer(), KeyType.forType(value.type)) + encodeIntElement(descriptor, 1, value.keyCode.value) + if (!value.modifier.isEmpty) + encodeShortElement(descriptor, 2, value.modifier.value) + } + } + +} + +/** A [KSerializer] for Cloth Config's [Color] that encodes/decodes as an `#AARRGGBB` hex string. */ +object ColorSerializer : KSerializer +{ + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Color", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): Color + { + return Color.ofTransparent( + decoder.decodeString() + .trimStart('#') + .toLong(16) + .toInt() + ) + } + + override fun serialize(encoder: Encoder, value: Color) + { + encoder.encodeString("#${Integer.toHexString(value.color).padStart(8, '0')}") + } + +} + +/** + * A [SerializersModule] that registers Archie's Cloth Config-related type serializers + * ([ModifierKeyCodeSerializer], [ColorSerializer]) as contextual serializers. + * + * [ModifierKeyCodeSerializer] is only registered on the physical client (via [onClient]), since + * [ModifierKeyCode] is a client-only Cloth Config type that a dedicated server shouldn't + * class-load; [ColorSerializer] is registered on both sides. + */ +val BuiltInSerializersModule = SerializersModule { + onClient { + contextual(ModifierKeyCode::class, ModifierKeyCodeSerializer) + } + contextual(Color::class, ColorSerializer) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/MinecraftSerializers.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/MinecraftSerializers.kt new file mode 100644 index 000000000..32e56d02d --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/MinecraftSerializers.kt @@ -0,0 +1,363 @@ +package net.kernelpanicsoft.archie.serialization.serializers + +import io.netty.buffer.Unpooled +import kotlinx.serialization.Contextual +import kotlinx.serialization.KSerializer +import kotlinx.serialization.SerializationException +import kotlinx.serialization.builtins.ByteArraySerializer +import kotlinx.serialization.descriptors.* +import kotlinx.serialization.encoding.* +import kotlinx.serialization.modules.SerializersModule +import net.kernelpanicsoft.archie.serialization.CodecSerializer +import net.minecraft.core.* +import net.minecraft.core.registries.Registries +import net.minecraft.network.FriendlyByteBuf +import net.minecraft.resources.ResourceKey +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.ItemStack +import net.minecraft.world.level.ChunkPos +import net.minecraft.world.level.Level +import net.minecraft.world.phys.BlockHitResult +import net.minecraft.world.phys.HitResult +import net.minecraft.world.phys.Vec3 + +/* ─────────────────────── Type aliases ─────────────────────── */ + +/** + * Contextual type-alias for [FriendlyByteBuf] that uses [FriendlyByteBufSerializer] + * when the field is annotated with `@Contextual`. + */ +typealias SFriendlyByteBuf = @Contextual FriendlyByteBuf + +/** + * Contextual type-alias for [ResourceLocation] that uses [ResourceLocationSerializer] when + * the field is annotated with `@Contextual`. + */ +typealias SResourceLocation = @Contextual ResourceLocation + +/** + * Contextual type-alias for [Vec3i] that uses [Vec3iSerializer] when the field is + * annotated with `@Contextual`. + */ +typealias SVec3i = @Contextual Vec3i + +/** + * Contextual type-alias for [Vec3] that uses [Vec3Serializer] when the field is + * annotated with `@Contextual`. + */ +typealias SVec3 = @Contextual Vec3 + +/** + * Contextual type-alias for [BlockPos] that uses [BlockPosSerializer] when the field + * is annotated with `@Contextual`. + */ +typealias SBlockPos = @Contextual BlockPos + +/** + * Contextual type-alias for [ChunkPos] that uses [ChunkPosSerializer] when the field + * is annotated with `@Contextual`. + */ +typealias SChunkPos = @Contextual ChunkPos + +/** + * Contextual type-alias for [GlobalPos] that uses [GlobalPosSerializer] when the field + * is annotated with `@Contextual`. + */ +typealias SGlobalPos = @Contextual GlobalPos + +/** + * Contextual type-alias for [BlockHitResult] that uses [BlockHitResultSerializer] when + * the field is annotated with `@Contextual`. + */ +typealias SBlockHitResult = @Contextual BlockHitResult + +/** + * Contextual type-alias for [ItemStack] that uses a [net.kernelpanicsoft.archie.serialization.CodecSerializer] + * over [ItemStack.CODEC] when the field is annotated with `@Contextual`. + */ +typealias SItemStack = @Contextual ItemStack + +/* ─────────────────────── Serializers ─────────────────────── */ + +/** + * A [KSerializer] for [FriendlyByteBuf] that encodes/decodes the buffer contents as a + * raw byte array. + * + * The reader index is preserved after serialization so the buffer can be reused. + */ +object FriendlyByteBufSerializer : KSerializer { + override val descriptor: SerialDescriptor = + SerialDescriptor("FriendlyByteBuf", ByteArraySerializer().descriptor) + + override fun serialize(encoder: Encoder, value: FriendlyByteBuf) { + val index = value.readerIndex() + val bytes = ByteArray(value.readableBytes()) + value.readBytes(bytes) + value.readerIndex(index) + encoder.encodeSerializableValue(ByteArraySerializer(), bytes) + } + + override fun deserialize(decoder: Decoder): FriendlyByteBuf = + FriendlyByteBuf(Unpooled.buffer()).apply { + writeBytes(decoder.decodeSerializableValue(ByteArraySerializer())) + } +} + +/** + * A [KSerializer] for [ResourceLocation] that encodes/decodes its `namespace:path` string form. + */ +object ResourceLocationSerializer : KSerializer +{ + override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ResourceLocation", PrimitiveKind.STRING) + + override fun deserialize(decoder: Decoder): ResourceLocation + { + return ResourceLocation.parse(decoder.decodeString()) + } + + override fun serialize(encoder: Encoder, value: ResourceLocation) + { + encoder.encodeString(value.toString()) + } + +} + +/** + * A [KSerializer] for [Vec3i] (and its subclass [BlockPos]) that encodes/decodes the + * three integer components. + */ +object Vec3iSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Vec3i") { + element("x") + element("y") + element("z") + } + + override fun serialize(encoder: Encoder, value: Vec3i) { + encoder.encodeStructure(descriptor) { + encodeIntElement(descriptor, 0, value.x) + encodeIntElement(descriptor, 1, value.y) + encodeIntElement(descriptor, 2, value.z) + } + } + + override fun deserialize(decoder: Decoder): Vec3i = + decoder.decodeStructure(descriptor) { + var x: Int? = null; var y: Int? = null; var z: Int? = null + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> x = decodeIntElement(descriptor, 0) + 1 -> y = decodeIntElement(descriptor, 1) + 2 -> z = decodeIntElement(descriptor, 2) + CompositeDecoder.DECODE_DONE -> break + else -> throw SerializationException("Unexpected index: $index") + } + } + Vec3i(x ?: throw SerializationException("Missing x"), y ?: throw SerializationException("Missing y"), z ?: throw SerializationException("Missing z")) + } +} + +/** + * A [KSerializer] for [Vec3] that encodes/decodes the three double-precision components. + */ +object Vec3Serializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Vec3") { + element("x") + element("y") + element("z") + } + + override fun serialize(encoder: Encoder, value: Vec3) { + encoder.encodeStructure(descriptor) { + encodeDoubleElement(descriptor, 0, value.x) + encodeDoubleElement(descriptor, 1, value.y) + encodeDoubleElement(descriptor, 2, value.z) + } + } + + override fun deserialize(decoder: Decoder): Vec3 = + decoder.decodeStructure(descriptor) { + var x: Double? = null; var y: Double? = null; var z: Double? = null + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> x = decodeDoubleElement(descriptor, 0) + 1 -> y = decodeDoubleElement(descriptor, 1) + 2 -> z = decodeDoubleElement(descriptor, 2) + CompositeDecoder.DECODE_DONE -> break + else -> throw SerializationException("Unexpected index: $index") + } + } + Vec3(x ?: throw SerializationException("Missing x"), y ?: throw SerializationException("Missing y"), z ?: throw SerializationException("Missing z")) + } +} + +/** + * A [KSerializer] for [BlockPos] that encodes/decodes the three integer components. + */ +object BlockPosSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("BlockPos") { + element("x") + element("y") + element("z") + } + + override fun serialize(encoder: Encoder, value: BlockPos) { + encoder.encodeStructure(descriptor) { + encodeIntElement(descriptor, 0, value.x) + encodeIntElement(descriptor, 1, value.y) + encodeIntElement(descriptor, 2, value.z) + } + } + + override fun deserialize(decoder: Decoder): BlockPos = + decoder.decodeStructure(descriptor) { + var x: Int? = null; var y: Int? = null; var z: Int? = null + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> x = decodeIntElement(descriptor, 0) + 1 -> y = decodeIntElement(descriptor, 1) + 2 -> z = decodeIntElement(descriptor, 2) + CompositeDecoder.DECODE_DONE -> break + else -> throw SerializationException("Unexpected index: $index") + } + } + BlockPos(x ?: throw SerializationException("Missing x"), y ?: throw SerializationException("Missing y"), z ?: throw SerializationException("Missing z")) + } +} + +/** + * A [KSerializer] for [ChunkPos] that encodes/decodes the value as a single packed [Long]. + */ +object ChunkPosSerializer : KSerializer { + override val descriptor: SerialDescriptor = + PrimitiveSerialDescriptor("ChunkPos", PrimitiveKind.LONG) + + override fun serialize(encoder: Encoder, value: ChunkPos) = encoder.encodeLong(value.toLong()) + override fun deserialize(decoder: Decoder): ChunkPos = ChunkPos(decoder.decodeLong()) +} + +/** + * A [KSerializer] for [ResourceKey] of a specific registry. + * + * The serialized form is a [ResourceLocation] string (the key's location). + * + * ### Example + * ```kotlin + * val DIMENSION_KEY_SERIALIZER = ResourceKeySerializer(Registries.DIMENSION) + * ``` + * + * @param registry The [ResourceKey] of the registry this serializer is scoped to. + */ +class ResourceKeySerializer(val registry: ResourceKey>) : + KSerializer> { + companion object { + /** Pre-built serializer for dimension [ResourceKey]s. */ + val DIMENSION = ResourceKeySerializer(Registries.DIMENSION) + } + + override val descriptor: SerialDescriptor = ResourceLocationSerializer.descriptor + + override fun serialize(encoder: Encoder, value: ResourceKey<*>) = + encoder.encodeSerializableValue(ResourceLocationSerializer, value.location()) + + override fun deserialize(decoder: Decoder): ResourceKey<*> = + ResourceKey.create(registry, decoder.decodeSerializableValue(ResourceLocationSerializer)) +} + +/** + * A [KSerializer] for [GlobalPos] that encodes the dimension [ResourceKey] and [BlockPos]. + */ +object GlobalPosSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("GlobalPos") { + element("dimension", ResourceKeySerializer.DIMENSION.descriptor) + element("pos", BlockPosSerializer.descriptor) + } + + override fun serialize(encoder: Encoder, value: GlobalPos) { + encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, ResourceKeySerializer.DIMENSION, value.dimension()) + encodeSerializableElement(descriptor, 1, BlockPosSerializer, value.pos()) + } + } + + @Suppress("UNCHECKED_CAST") + override fun deserialize(decoder: Decoder): GlobalPos = + decoder.decodeStructure(descriptor) { + var dimension: ResourceKey? = null + var pos: BlockPos? = null + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> dimension = decodeSerializableElement(descriptor, 0, ResourceKeySerializer.DIMENSION) as ResourceKey + 1 -> pos = decodeSerializableElement(descriptor, 1, BlockPosSerializer) + CompositeDecoder.DECODE_DONE -> break + else -> throw SerializationException("Unexpected index: $index") + } + } + GlobalPos(dimension ?: throw SerializationException("Missing dimension"), pos ?: throw SerializationException("Missing pos")) + } +} + +/** + * A [KSerializer] for [BlockHitResult] that encodes the hit location, face direction, + * block position, whether the hit is inside the block, and whether it was a miss. + */ +object BlockHitResultSerializer : KSerializer { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("BlockHitResult") { + element("location", Vec3Serializer.descriptor) + element("side") + element("blockPos", BlockPosSerializer.descriptor) + element("insideBlock") + element("missed") + } + + override fun serialize(encoder: Encoder, value: BlockHitResult) { + encoder.encodeStructure(descriptor) { + encodeSerializableElement(descriptor, 0, Vec3Serializer, value.location) + encodeStringElement(descriptor, 1, value.direction.name) + encodeSerializableElement(descriptor, 2, BlockPosSerializer, value.blockPos) + encodeBooleanElement(descriptor, 3, value.isInside) + encodeBooleanElement(descriptor, 4, value.type == HitResult.Type.MISS) + } + } + + override fun deserialize(decoder: Decoder): BlockHitResult { + var location: Vec3? = null; var side: Direction? = null + var pos: BlockPos? = null; var inside: Boolean? = null; var missed: Boolean? = null + decoder.decodeStructure(descriptor) { + while (true) { + when (val index = decodeElementIndex(descriptor)) { + 0 -> location = decodeSerializableElement(descriptor, 0, Vec3Serializer) + 1 -> side = Direction.byName(decodeStringElement(descriptor, 1)) + 2 -> pos = decodeSerializableElement(descriptor, 2, BlockPosSerializer) + 3 -> inside = decodeBooleanElement(descriptor, 3) + 4 -> missed = decodeBooleanElement(descriptor, 4) + CompositeDecoder.DECODE_DONE -> break + else -> throw SerializationException("Unexpected index: $index") + } + } + } + if (location == null || side == null || pos == null || inside == null || missed == null) + throw SerializationException("Properties missing when decoding BlockHitResult") + return if (missed == true) BlockHitResult.miss(location, side!!, pos) + else BlockHitResult(location, side!!, pos, inside) + } +} + +/** + * A [SerializersModule] that registers all built-in Minecraft type serializers as contextual + * serializers. + * + * Include this module in your serialization format instances to enable `@Contextual` on + * Minecraft types. + */ +val MinecraftSerializersModule = SerializersModule { + contextual(FriendlyByteBuf::class, FriendlyByteBufSerializer) + contextual(ResourceLocation::class, ResourceLocationSerializer) + contextual(Vec3i::class, Vec3iSerializer) + contextual(Vec3::class, Vec3Serializer) + contextual(BlockPos::class, BlockPosSerializer) + contextual(ChunkPos::class, ChunkPosSerializer) + contextual(GlobalPos::class, GlobalPosSerializer) + contextual(BlockHitResult::class, BlockHitResultSerializer) + contextual(ItemStack::class, CodecSerializer(ItemStack.CODEC)) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt new file mode 100644 index 000000000..be325218f --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt @@ -0,0 +1,124 @@ +package net.kernelpanicsoft.archie.transfer + +import dev.architectury.registry.registries.RegistrySupplier +import earth.terrarium.common_storage_lib.context.ItemContext +import earth.terrarium.common_storage_lib.energy.EnergyApi +import earth.terrarium.common_storage_lib.fluid.FluidApi +import earth.terrarium.common_storage_lib.item.ItemApi +import earth.terrarium.common_storage_lib.lookup.BlockLookup +import earth.terrarium.common_storage_lib.lookup.ItemLookup +import net.minecraft.core.Direction +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.world.level.block.entity.BlockEntity +import net.minecraft.world.level.block.entity.BlockEntityType + +/** + * Exposes [ArchieItemStorage]/[ArchieFluidStorage]/[ArchieEnergyStorage] to third-party mods' + * pipes/hoppers/etc, by registering against Common Storage Lib's [ItemApi]/[FluidApi]/[EnergyApi] + * `BLOCK` lookups - which are, unlike a lookup you'd build yourself via [BlockLookup.create], the + * real, already-canonical singletons Common Storage Lib itself wires straight through to each + * platform's native capability system (Fabric Transfer API's `ItemStorage.SIDED`/`FluidStorage.SIDED`, + * NeoForge's `Capabilities.ItemHandler.BLOCK`/`Capabilities.FluidHandler.BLOCK`). Registering here + * makes a block entity's storage visible to *any* mod querying those native systems directly - no + * dependency on Common Storage Lib (or Archie) required on the consuming side. + * + * Deliberately explicit opt-in, not wired into [net.kernelpanicsoft.archie.serialization.NBTHolder.itemField]/ + * `fluidField`/`energyField`: registration must happen exactly once per [BlockEntityType], while + * those field delegates run once per block entity *instance* (inside its constructor) - auto-registering + * from there would either re-register redundantly per instance or need awkward static bookkeeping. + * Call these once, at registration time, next to your `DeferredRegister`/`RegistrySupplier` declarations: + * + * ```kotlin + * object BlockEntities : ADeferredRegistryHolder>(MyMod.MOD, Registries.BLOCK_ENTITY_TYPE) { + * val TANK by register("tank") { BlockEntityType.Builder.of(::TankBlockEntity, MyBlocks.TANK).build(null) } + * } + * + * // In mod init, after BlockEntities.init(): + * BlockEntities.TANK.exposeFluidStorage { tank -> tank.fluid } + * ``` + */ +@Suppress("unused") +object ArchieCapabilityExposure + +/** See [ArchieCapabilityExposure]. Exposes this block entity type's [ArchieItemStorage] to [ItemApi.BLOCK]. */ +fun BlockEntityType.exposeItemStorage(selector: (T, Direction?) -> ArchieItemStorage?) { + exposeToBlockLookup(ItemApi.BLOCK, selector) +} + +/** [exposeItemStorage] overload for a selector that doesn't need the query direction. */ +fun BlockEntityType.exposeItemStorage(selector: (T) -> ArchieItemStorage?) { + exposeItemStorage { be, _ -> selector(be) } +} + +/** See [ArchieCapabilityExposure]. Exposes this block entity type's [ArchieFluidStorage] to [FluidApi.BLOCK]. */ +fun BlockEntityType.exposeFluidStorage(selector: (T, Direction?) -> ArchieFluidStorage?) { + exposeToBlockLookup(FluidApi.BLOCK, selector) +} + +/** [exposeFluidStorage] overload for a selector that doesn't need the query direction. */ +fun BlockEntityType.exposeFluidStorage(selector: (T) -> ArchieFluidStorage?) { + exposeFluidStorage { be, _ -> selector(be) } +} + +/** See [ArchieCapabilityExposure]. Exposes this block entity type's [ArchieEnergyStorage] to [EnergyApi.BLOCK]. */ +fun BlockEntityType.exposeEnergyStorage(selector: (T, Direction?) -> ArchieEnergyStorage?) { + exposeToBlockLookup(EnergyApi.BLOCK, selector) +} + +/** [exposeEnergyStorage] overload for a selector that doesn't need the query direction. */ +fun BlockEntityType.exposeEnergyStorage(selector: (T) -> ArchieEnergyStorage?) { + exposeEnergyStorage { be, _ -> selector(be) } +} + +/** + * Shared implementation: [BlockLookup] only supports registering by [BlockEntityType] via the + * [BlockLookup.BlockRegistrar] callback handed to [BlockLookup.onRegister] - `registerSelf` (the + * more direct-looking method) only accepts a `Block`-keyed getter, not a block-entity-keyed one. + */ +@Suppress("UNCHECKED_CAST") +private fun BlockEntityType.exposeToBlockLookup( + lookup: BlockLookup, + selector: (T, Direction?) -> S?, +) { + lookup.onRegister { registrar -> + registrar.registerBlockEntities( + BlockLookup.BlockEntityGetter { blockEntity, direction -> selector(blockEntity as T, direction) }, + this, + ) + } +} + +// ── RegistrySupplier convenience overloads ────────────────────────────────────────────────── +// So these can be chained right where the type is declared, without waiting for a separate +// registration-time call site. Architectury's RegistrySupplier.listen(...) already guarantees the +// callback runs once the entry is actually registered. + +fun RegistrySupplier>.exposeItemStorage(selector: (T, Direction?) -> ArchieItemStorage?) = + listen { it.exposeItemStorage(selector) } + +fun RegistrySupplier>.exposeItemStorage(selector: (T) -> ArchieItemStorage?) = + listen { it.exposeItemStorage(selector) } + +fun RegistrySupplier>.exposeFluidStorage(selector: (T, Direction?) -> ArchieFluidStorage?) = + listen { it.exposeFluidStorage(selector) } + +fun RegistrySupplier>.exposeFluidStorage(selector: (T) -> ArchieFluidStorage?) = + listen { it.exposeFluidStorage(selector) } + +fun RegistrySupplier>.exposeEnergyStorage(selector: (T, Direction?) -> ArchieEnergyStorage?) = + listen { it.exposeEnergyStorage(selector) } + +fun RegistrySupplier>.exposeEnergyStorage(selector: (T) -> ArchieEnergyStorage?) = + listen { it.exposeEnergyStorage(selector) } + +// ── Item-in-item exposure (stretch) ───────────────────────────────────────────────────────── +// Unlike the BLOCK lookups above, ItemApi.ITEM has no equivalent native-platform bridge - it's +// registered under Common Storage Lib's own mod id, so this is only visible to other mods that +// also depend on Common Storage Lib and query this exact same field. Still useful: it makes a +// backpack/bag's own storage pipe-accessible (by other CSL-aware mods) even while its GUI is closed. + +/** Exposes this item's [ArchieItemStorage] (e.g. a bag/backpack's contents) to [ItemApi.ITEM]. */ +fun Item.exposeItemStorage(selector: (ItemStack, ItemContext) -> ArchieItemStorage?) { + ItemApi.ITEM.registerSelf(ItemLookup.ItemGetter { stack, context -> selector(stack, context) }, this) +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt new file mode 100644 index 000000000..c7139fd71 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt @@ -0,0 +1,153 @@ +package net.kernelpanicsoft.archie.transfer + +import earth.terrarium.common_storage_lib.storage.base.UpdateManager +import earth.terrarium.common_storage_lib.storage.base.ValueStorage +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.encoding.encodeStructure +import net.benwoodworth.knbt.NbtTag +import net.kernelpanicsoft.archie.serialization.NBT +import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless +import net.kernelpanicsoft.archie.serialization.encodeToNbtTagRootless +import kotlin.math.min + +/** + * Archie's platform-agnostic energy buffer: implements Common Storage Lib's [ValueStorage] - + * the energy analogue of the `CommonStorage`/`CommonStorage` + * [ArchieItemStorage]/[ArchieFluidStorage] implement - plus Archie's NBT serialization for + * save/load, mirroring their shape. + * + * Not exposed to third-party mods' pipes/hoppers by default - call + * [BlockEntityType.exposeEnergyStorage][net.kernelpanicsoft.archie.transfer.exposeEnergyStorage] + * once, at registration time, to register it with Common Storage Lib's `EnergyApi.BLOCK`, which + * (unlike a lookup you'd build yourself) is wired straight through to each platform's native + * capability system - no dependency on Common Storage Lib required on the consuming side. Any + * additional platform-specific bridge you still want on top of that (NeoForge's `IEnergyStorage`, + * Fabric's Team Reborn Energy API) is still on you to wire in your own mod. + * + * Usually created through [net.kernelpanicsoft.archie.serialization.NBTHolder.energyField] + * rather than directly. + * + * @param capacity The maximum amount of energy this storage can hold. + * @param onUpdate Invoked whenever this storage's contents change, for persistence/sync. + */ +@Serializable(with = ArchieEnergyStorage.Serializer::class) +class ArchieEnergyStorage( + private var capacity: Long, + private val onUpdate: () -> Unit = {}, +) : ValueStorage, UpdateManager +{ + private var amount: Long = 0 + + /** The amount of energy currently stored. */ + override fun getStoredAmount(): Long = amount + + /** The maximum amount of energy this storage can hold. */ + override fun getCapacity(): Long = capacity + + override fun allowsInsertion(): Boolean = true + + override fun allowsExtraction(): Boolean = true + + /** + * Inserts up to [amount] energy, returning how much was actually accepted. + * When [simulate] is `true`, no state is changed - only the acceptable amount is calculated. + */ + override fun insert(amount: Long, simulate: Boolean): Long + { + val inserted = min(amount, capacity - this.amount) + if (inserted <= 0) return 0 + if (!simulate) + { + this.amount += inserted + update() + } + return inserted + } + + /** + * Extracts up to [amount] energy, returning how much was actually removed. + * When [simulate] is `true`, no state is changed - only the extractable amount is calculated. + */ + override fun extract(amount: Long, simulate: Boolean): Long + { + val extracted = min(amount, this.amount) + if (extracted <= 0) return 0 + if (!simulate) + { + this.amount -= extracted + update() + } + return extracted + } + + /** Directly overwrites the stored amount, clamped to `0..`[getCapacity]. */ + fun set(amount: Long) + { + this.amount = amount.coerceIn(0, capacity) + update() + } + + /** Snapshots this storage's [getCapacity] and stored amount as an [NbtTag], for save/sync. */ + override fun createSnapshot(): NbtTag = NBT.encodeToNbtTagRootless(serializer(), this) + + /** + * Restores this storage's capacity and stored amount from a snapshot produced by + * [createSnapshot]. `capacity` is clamped to non-negative and `amount` to `0..capacity` - a + * malformed or stale snapshot (e.g. from before a capacity change) shouldn't be able to leave + * this storage over-capacity or negative, which would otherwise wedge [insert]/[extract]. + */ + override fun readSnapshot(snapshot: NbtTag) + { + val decoded = NBT.decodeFromNbtTagRootless(serializer(), snapshot) + this.capacity = decoded.capacity.coerceAtLeast(0) + this.amount = decoded.amount.coerceIn(0, this.capacity) + } + + override fun update() = onUpdate() + + /** Serializes an [ArchieEnergyStorage] as its capacity followed by its stored amount. */ + object Serializer : KSerializer + { + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ArchieEnergyStorage") { + element("capacity", Long.serializer().descriptor) + element("amount", Long.serializer().descriptor) + } + + override fun deserialize(decoder: Decoder): ArchieEnergyStorage + { + return decoder.decodeStructure(descriptor) + { + var capacity = 0L + var amount = 0L + while (true) + { + when (val index = decodeElementIndex(descriptor)) + { + 0 -> capacity = decodeLongElement(descriptor, 0).coerceAtLeast(0) + 1 -> amount = decodeLongElement(descriptor, 1) + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + ArchieEnergyStorage(capacity).also { it.amount = amount.coerceIn(0, capacity) } + } + } + + override fun serialize(encoder: Encoder, value: ArchieEnergyStorage) + { + encoder.encodeStructure(descriptor) + { + encodeLongElement(descriptor, 0, value.capacity) + encodeLongElement(descriptor, 1, value.amount) + } + } + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt new file mode 100644 index 000000000..501b2d518 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt @@ -0,0 +1,192 @@ +package net.kernelpanicsoft.archie.transfer + +import dev.architectury.fluid.FluidStack +import earth.terrarium.common_storage_lib.resources.ResourceStack +import earth.terrarium.common_storage_lib.resources.fluid.FluidResource +import earth.terrarium.common_storage_lib.storage.base.StorageSlot +import earth.terrarium.common_storage_lib.storage.base.UpdateManager +import kotlinx.serialization.ExperimentalSerializationApi +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.descriptors.element +import kotlinx.serialization.descriptors.nullable +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.encoding.encodeStructure +import net.benwoodworth.knbt.NbtTag +import net.kernelpanicsoft.archie.serialization.NBT +import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless +import net.kernelpanicsoft.archie.serialization.encodeToNbtTagRootless +import net.kernelpanicsoft.archie.serialization.kSerializer +import net.minecraft.world.item.Item +import kotlin.math.min + +/** + * A single resource-backed slot inside an [ArchieFluidStorage], capped at [limit]. Tracks a + * [FluidResource] + amount internally while exposing plain [FluidStack] access via + * [getFluid]/[set]. + * + * @param onUpdate Invoked by [update] whenever this slot's contents should be persisted/synced. + */ +@Serializable(with = ArchieFluidSlot.Serializer::class) +class ArchieFluidSlot(private val limit: Long, private val onUpdate: () -> Unit = {}) : StorageSlot, UpdateManager +{ + private var resource: FluidResource = FluidResource.BLANK + private var amount: Long = 0 + private var stack: FluidStack + get() + { + if (resource.isBlank) + return FluidStack.empty() + return FluidStack.create(resource.type, amount) + } + set(value) + { + resource = FluidResource.of(value.fluid) + amount = value.amount + } + + private var resourceStack: ResourceStack + get() + { + return ResourceStack(resource, amount) + } + set(value) + { + resource = value.resource + amount = value.amount + } + + constructor(limit: Long, stack: FluidStack = FluidStack.empty(), onUpdate: () -> Unit = {}) : this(limit, onUpdate) + { + this.stack = stack + } + + constructor(limit: Long, resourceStack: ResourceStack, onUpdate: () -> Unit = {}) : this(limit, onUpdate) + { + this.resourceStack = resourceStack + } + + /** The [FluidStack] currently held in this slot (a copy; mutate via [set]). */ + fun getFluid(): FluidStack = stack + /** Replaces this slot's contents with [value]. */ + fun set(value: FluidStack) + { + stack = value + } + + + + override fun insert(unit: FluidResource, amount: Long, simulate: Boolean): Long + { + if (!isResourceValid(unit)) return 0 + if (this.resource.isBlank()) + { + val inserted = min(amount, limit) + if (!simulate) + { + this.resource = unit + this.amount = inserted + } + return inserted + } else if (this.resource == unit) + { + val inserted = min(amount, limit - this.amount) + if (!simulate) + { + this.amount += inserted + } + return inserted + } + return 0 + } + + override fun extract(unit: FluidResource, amount: Long, simulate: Boolean): Long + { + if (this.resource == unit) + { + val extracted = min(amount, this.amount) + if (!simulate) + { + this.amount -= extracted + if (this.amount == 0L) + { + this.resource = FluidResource.BLANK + } + } + return extracted + } + return 0 + } + + override fun getLimit(resource: FluidResource): Long = limit + + override fun isResourceValid(unit: FluidResource): Boolean = true + + override fun getResource(): FluidResource = resource + + override fun getAmount(): Long = amount + + override fun createSnapshot(): NbtTag + { + return NBT.encodeToNbtTagRootless(serializer(), this) + } + + override fun update() + { + onUpdate() + } + + override fun readSnapshot(snapshot: NbtTag) + { + this.stack = NBT.decodeFromNbtTagRootless(serializer(), snapshot).stack + } + + /** Serializes an [ArchieFluidSlot] as its [limit] followed by its [ResourceStack] (or `null` when blank). */ + @OptIn(ExperimentalSerializationApi::class) + object Serializer : KSerializer + { + private val surrogate = ResourceStack.FLUID_CODEC.kSerializer + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ArchieFluidSlot") { + element("limit", Long.serializer().descriptor) + element("resourceStack", surrogate.descriptor.nullable) + } + + override fun deserialize(decoder: Decoder): ArchieFluidSlot + { + return decoder.decodeStructure(descriptor) + { + var limit = 0L + var resourceStack: ResourceStack? = null + while (true) + { + when (val index = decodeElementIndex(descriptor)) + { + 0 -> limit = decodeLongElement(descriptor, 0) + 1 -> resourceStack = decodeNullableSerializableElement(descriptor, 1, surrogate) + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + ArchieFluidSlot(limit, resourceStack ?: ResourceStack(FluidResource.BLANK, 0)) + } + } + + override fun serialize( + encoder: Encoder, + value: ArchieFluidSlot + ) + { + encoder.encodeStructure(descriptor) + { + encodeLongElement(descriptor, 0, value.limit) + encodeNullableSerializableElement(descriptor, 1, surrogate, value.resourceStack) + } + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt new file mode 100644 index 000000000..b90779a55 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt @@ -0,0 +1,126 @@ +package net.kernelpanicsoft.archie.transfer + +import earth.terrarium.common_storage_lib.resources.fluid.FluidResource +import earth.terrarium.common_storage_lib.storage.base.CommonStorage +import earth.terrarium.common_storage_lib.storage.base.UpdateManager +import earth.terrarium.common_storage_lib.storage.util.TransferUtil +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.builtins.serializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.buildClassSerialDescriptor +import kotlinx.serialization.encoding.CompositeDecoder +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import kotlinx.serialization.encoding.decodeStructure +import kotlinx.serialization.encoding.encodeStructure +import net.benwoodworth.knbt.NbtTag +import net.kernelpanicsoft.archie.serialization.NBT +import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless +import net.kernelpanicsoft.archie.serialization.encodeToNbtTagRootless +import net.minecraft.core.NonNullList +import kotlin.math.min + +/** + * The fluid analogue of [ArchieItemStorage]: a fixed-size list of [ArchieFluidSlot]s, each + * capped at [limit], implementing Common Storage Lib's [CommonStorage] and Archie's NBT + * serialization (via [Serializer]) for save/load. + * + * @param onUpdate Invoked by [update] whenever the storage's contents should be persisted/synced. + */ +@Serializable(with = ArchieFluidStorage.Serializer::class) +open class ArchieFluidStorage private constructor( + protected val limit: Long, + protected val slots: NonNullList, + protected val onUpdate: () -> Unit = {} +) : CommonStorage, UpdateManager +{ + /** Creates a storage with [size] empty slots, each capped at [limit]. */ + constructor(limit: Long, size: Int, onUpdate: () -> Unit = {}) : this( + limit, + NonNullList.createWithCapacity(size).apply { + for (i in 0 until size) + { + add(ArchieFluidSlot(limit)) + } + }, onUpdate + ) + + override fun insert(unit: FluidResource, amount: Long, simulate: Boolean): Long + { + return TransferUtil.insertSlots(this, unit, amount, simulate) + } + + override fun extract(unit: FluidResource, amount: Long, simulate: Boolean): Long + { + return TransferUtil.extractSlots(this, unit, amount, simulate) + } + + /** The number of slots in this storage. */ + override fun size(): Int = slots.size + + /** The [ArchieFluidSlot] at [slot]. */ + override fun get(slot: Int): ArchieFluidSlot + { + return slots[slot] + } + + override fun createSnapshot(): NbtTag + { + return NBT.encodeToNbtTagRootless(serializer(), this) + } + + override fun update() + { + onUpdate() + } + + override fun readSnapshot(snapshot: NbtTag) + { + val slots = NBT.decodeFromNbtTagRootless(serializer(), snapshot).slots + for (i in 0 until min(this.slots.size, slots.size)) + { + this.slots[i] = slots[i] + } + } + + /** Serializes an [ArchieFluidStorage] as its [limit] followed by the list of its [ArchieFluidSlot]s. */ + object Serializer : KSerializer + { + private val surrogate = ListSerializer(ArchieFluidSlot.serializer()) + override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ArchieFluidStorage") { + element("limit", Long.serializer().descriptor) + element("slots", surrogate.descriptor) + } + + override fun deserialize(decoder: Decoder): ArchieFluidStorage + { + return decoder.decodeStructure(descriptor) + { + var limit = 0L + var slots: List = emptyList() + while (true) + { + when (val index = decodeElementIndex(descriptor)) + { + 0 -> limit = decodeLongElement(descriptor, 0) + 1 -> slots = decodeSerializableElement(descriptor, 1, surrogate) + CompositeDecoder.DECODE_DONE -> break + else -> error("Unexpected index: $index") + } + } + ArchieFluidStorage(limit, NonNullList.of(ArchieFluidSlot(limit), *slots.toTypedArray())) + } + } + + override fun serialize(encoder: Encoder, value: ArchieFluidStorage) + { + encoder.encodeStructure(descriptor) + { + encodeLongElement(descriptor, 0, value.limit) + encodeSerializableElement(descriptor, 1, surrogate, value.slots) + } + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemMenuSlot.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemMenuSlot.kt new file mode 100644 index 000000000..3dcf42195 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemMenuSlot.kt @@ -0,0 +1,64 @@ +package net.kernelpanicsoft.archie.transfer + +import earth.terrarium.common_storage_lib.storage.base.UpdateManager +import net.kernelpanicsoft.archie.gui.ComposeContainerMenuBase +import net.minecraft.world.SimpleContainer +import net.minecraft.world.inventory.Slot +import net.minecraft.world.item.ItemStack +import java.util.function.Predicate + +/** + * A vanilla [Slot] that bridges one slot of an [ArchieItemStorage] into a [ComposeContainerMenuBase], + * so `net.minecraft.world.inventory` machinery (shift-click, drag, etc.) can operate on it + * directly. Created by [ComposeContainerMenuBase] from a `handler(group, storage, filter)` + * registration; not usually constructed directly. + * + * @param filter Restricts which stacks [mayPlace] into this slot. + */ +class ArchieItemMenuSlot( + private val storage: ArchieItemStorage, + val filter: Predicate = Predicate { true }, + slot: Int, x: Int, y: Int, + private val owningMenu: ComposeContainerMenuBase<*>, +) : Slot(SimpleContainer(0), slot, x, y) +{ + override fun isActive(): Boolean = owningMenu.isSlotVisible(index) + + + override fun getItem(): ItemStack + { + val slot = storage[containerSlot] + return slot.getItem() + } + + override fun set(stack: ItemStack) + { + val slot = storage[containerSlot] + slot.set(stack) + setChanged() + } + + override fun getMaxStackSize(): Int + { + val slot = storage[containerSlot] + return slot.getMaxStackSize() + } + + override fun setChanged() + { + UpdateManager.batch(storage) + } + + override fun remove(amount: Int): ItemStack + { + val slot = storage[containerSlot] + val ret = slot.remove(amount) + setChanged() + return ret + } + + override fun mayPlace(stack: ItemStack): Boolean + { + return filter.test(stack) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemSlot.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemSlot.kt new file mode 100644 index 000000000..2bf6f01d3 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemSlot.kt @@ -0,0 +1,175 @@ +package net.kernelpanicsoft.archie.transfer + +import net.kernelpanicsoft.archie.serialization.NBT +import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless +import net.kernelpanicsoft.archie.serialization.encodeToNbtTagRootless +import earth.terrarium.common_storage_lib.resources.ResourceStack +import earth.terrarium.common_storage_lib.resources.item.ItemResource +import earth.terrarium.common_storage_lib.storage.base.StorageSlot +import earth.terrarium.common_storage_lib.storage.base.UpdateManager +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.descriptors.nullable +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.benwoodworth.knbt.NbtTag +import net.kernelpanicsoft.archie.serialization.kSerializer +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import kotlin.math.min + +/** + * A single resource-backed slot inside an [ArchieItemStorage]. Tracks an [ItemResource] + + * amount internally (for Common Storage Lib's resource-based [insert]/[extract]) while exposing + * plain [ItemStack] access via [getItem]/[set]. + * + * @param onUpdate Invoked by [update] whenever this slot's contents should be persisted/synced. + */ +@Serializable(with = ArchieItemSlot.Serializer::class) +class ArchieItemSlot(private val onUpdate: () -> Unit = {}) : StorageSlot, UpdateManager +{ + private var resource: ItemResource = ItemResource.BLANK + private var amount: Long = 0 + private var stack: ItemStack + get() + { + if (resource.isBlank) + return ItemStack.EMPTY + return resource.toStack(amount.toInt()) + } + set(value) + { + resource = ItemResource.of(value) + amount = value.count.toLong() + } + + private var resourceStack: ResourceStack + get() + { + return ResourceStack(resource, amount) + } + set(value) + { + resource = value.resource + amount = value.amount + } + + constructor(stack: ItemStack = ItemStack.EMPTY, onUpdate: () -> Unit = {}) : this(onUpdate) + { + this.stack = stack + } + + constructor(resourceStack: ResourceStack, onUpdate: () -> Unit = {}) : this(onUpdate) + { + this.resourceStack = resourceStack + } + + /** The [ItemStack] currently held in this slot (a copy; mutate via [set]). */ + fun getItem(): ItemStack = stack + /** Replaces this slot's contents with [value]. */ + fun set(value: ItemStack) + { + stack = value + } + + /** Splits up to [amount] items off this slot's stack and returns them, leaving the rest in place. */ + fun remove(amount: Int): ItemStack + { + return if (!stack.isEmpty && amount > 0) stack.let { + val ret = it.split(amount) + stack = it + ret + } else ItemStack.EMPTY + } + + /** The maximum stack size for the resource currently held (or [Item.ABSOLUTE_MAX_STACK_SIZE] if empty). */ + fun getMaxStackSize(): Int = getLimit(resource).toInt() + + + + override fun insert(unit: ItemResource, amount: Long, simulate: Boolean): Long + { + if (!isResourceValid(unit)) return 0 + if (this.resource.isBlank) + { + val inserted = + min(amount.toDouble(), unit.cachedStack.maxStackSize.toDouble()).toLong() + if (!simulate) + { + this.resource = unit + this.amount = inserted + } + return inserted + } else if (this.resource.test(unit.toStack())) + { + val inserted = + min(amount.toDouble(), (getLimit(resource) - this.amount).toDouble()).toLong() + if (!simulate) + { + this.amount += inserted + } + return inserted + } + return 0 + } + + override fun extract(unit: ItemResource, amount: Long, simulate: Boolean): Long + { + if (this.resource.test(unit.toStack())) + { + val extracted = min(amount.toDouble(), this.amount.toDouble()).toLong() + if (!simulate) + { + this.amount -= extracted + if (this.amount == 0L) + { + this.resource = ItemResource.BLANK + } + } + return extracted + } + return 0 + } + + override fun getLimit(resource: ItemResource): Long = + if (resource.isBlank) Item.ABSOLUTE_MAX_STACK_SIZE.toLong() + else resource.cachedStack.maxStackSize.toLong() + + override fun isResourceValid(unit: ItemResource): Boolean = true + + override fun getResource(): ItemResource = resource + + override fun getAmount(): Long = amount + + override fun createSnapshot(): NbtTag + { + return NBT.encodeToNbtTagRootless(serializer(), this) + } + + override fun update() + { + onUpdate() + } + + override fun readSnapshot(snapshot: NbtTag) + { + this.stack = NBT.decodeFromNbtTagRootless(serializer(), snapshot).stack + } + + /** Serializes an [ArchieItemSlot] as its underlying [ResourceStack], or `null` when blank. */ + object Serializer : KSerializer + { + private val surrogate = ResourceStack.ITEM_CODEC.kSerializer + override val descriptor: SerialDescriptor = surrogate.descriptor.nullable + override fun deserialize(decoder: Decoder): ArchieItemSlot + { + return ArchieItemSlot(surrogate.deserialize(decoder)) + } + + override fun serialize(encoder: Encoder, value: ArchieItemSlot) + { + surrogate.serialize(encoder, value.resourceStack) + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemStorage.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemStorage.kt new file mode 100644 index 000000000..59f68507a --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemStorage.kt @@ -0,0 +1,102 @@ +package net.kernelpanicsoft.archie.transfer + +import net.kernelpanicsoft.archie.serialization.NBT +import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless +import net.kernelpanicsoft.archie.serialization.encodeToNbtTagRootless +import earth.terrarium.common_storage_lib.resources.item.ItemResource +import earth.terrarium.common_storage_lib.storage.base.CommonStorage +import earth.terrarium.common_storage_lib.storage.base.UpdateManager +import earth.terrarium.common_storage_lib.storage.util.TransferUtil +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.builtins.ListSerializer +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder +import net.benwoodworth.knbt.NbtTag +import net.minecraft.core.NonNullList +import kotlin.math.min + +/** + * Archie's platform-agnostic item container: a fixed-size list of [ArchieItemSlot]s that + * implements Common Storage Lib's [CommonStorage] (resource-based [insert]/[extract] for + * capability interop) and Archie's NBT serialization (via [Serializer]) for save/load. + * + * Usually created through [net.kernelpanicsoft.archie.serialization.NBTHolder.itemField] rather + * than directly. Its public surface is deliberately small; read or mutate the [net.minecraft.world.item.ItemStack] in a + * slot through the [ArchieItemSlot] returned by [get], not on the storage itself. + * + * @param onUpdate Invoked by [update] whenever the storage's contents should be persisted/synced. + */ +@Serializable(with = ArchieItemStorage.Serializer::class) +open class ArchieItemStorage private constructor( + protected var slots: NonNullList, + protected val onUpdate: () -> Unit = {} +) : CommonStorage, UpdateManager +{ + /** Creates a storage with [size] empty slots. */ + constructor(size: Int, onUpdate: () -> Unit = {}) : this( + NonNullList.createWithCapacity(size).apply { + for (i in 0 until size) + { + add(ArchieItemSlot()) + } + }, onUpdate + ) + + override fun insert(unit: ItemResource, amount: Long, simulate: Boolean): Long + { + return TransferUtil.insertSlots(this, unit, amount, simulate) + } + + override fun extract(unit: ItemResource, amount: Long, simulate: Boolean): Long + { + return TransferUtil.extractSlots(this, unit, amount, simulate) + } + + /** The number of slots in this storage. */ + override fun size(): Int = slots.size + + /** The [ArchieItemSlot] at [slot], for reading/mutating its [net.minecraft.world.item.ItemStack]. */ + override fun get(slot: Int): ArchieItemSlot + { + return slots[slot] + } + + override fun createSnapshot(): NbtTag + { + return NBT.encodeToNbtTagRootless(serializer(), this) + } + + override fun update() + { + onUpdate() + } + + override fun readSnapshot(snapshot: NbtTag) + { + val slots = NBT.decodeFromNbtTagRootless(serializer(), snapshot).slots + for (i in 0 until min(this.slots.size, slots.size)) + { + this.slots[i] = slots[i] + } + } + + /** Serializes an [ArchieItemStorage] as the plain list of its [ArchieItemSlot]s. */ + object Serializer : KSerializer + { + private val surrogate = ListSerializer(ArchieItemSlot.serializer()) + override val descriptor: SerialDescriptor = surrogate.descriptor + + override fun deserialize(decoder: Decoder): ArchieItemStorage + { + return ArchieItemStorage(NonNullList.of(ArchieItemSlot(), *surrogate.deserialize(decoder).toTypedArray())) + } + + override fun serialize(encoder: Encoder, value: ArchieItemStorage) + { + surrogate.serialize(encoder, value.slots) + } + + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/VanillaMenuSlot.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/VanillaMenuSlot.kt new file mode 100644 index 000000000..6c94705cb --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/VanillaMenuSlot.kt @@ -0,0 +1,69 @@ +package net.kernelpanicsoft.archie.transfer + +import earth.terrarium.common_storage_lib.item.impl.vanilla.AbstractVanillaContainer +import earth.terrarium.common_storage_lib.item.impl.vanilla.VanillaDelegatingSlot +import earth.terrarium.common_storage_lib.storage.base.UpdateManager +import net.kernelpanicsoft.archie.gui.ComposeContainerMenuBase +import net.minecraft.world.SimpleContainer +import net.minecraft.world.inventory.Slot +import net.minecraft.world.item.ItemStack +import java.util.function.Predicate + +/** + * The [ArchieItemMenuSlot] equivalent for adapting an existing vanilla-style + * [AbstractVanillaContainer] (rather than an [ArchieItemStorage]) into a [ComposeContainerMenuBase]. + * Created by [ComposeContainerMenuBase] from a `handler(group, storage, filter)` registration; not + * usually constructed directly. + * + * @param filter Restricts which stacks [mayPlace] into this slot. + */ +class VanillaMenuSlot( + private val storage: AbstractVanillaContainer, + val filter: Predicate = Predicate { true }, + slot: Int, x: Int, y: Int, + private val owningMenu: ComposeContainerMenuBase<*>, +) : Slot(SimpleContainer(0), slot, x, y) +{ + override fun isActive(): Boolean = owningMenu.isSlotVisible(index) + + + override fun getItem(): ItemStack + { + val slot = storage[containerSlot] as VanillaDelegatingSlot + return slot.createSnapshot() + } + + override fun set(stack: ItemStack) + { + val slot = storage[containerSlot] as VanillaDelegatingSlot + slot.readSnapshot(stack) + setChanged() + } + + override fun getMaxStackSize(): Int + { + val slot = storage[containerSlot] as VanillaDelegatingSlot + return slot.getLimit(slot.resource).toInt() + } + + override fun setChanged() + { + UpdateManager.batch(storage) + } + + override fun remove(amount: Int): ItemStack + { + // set(it) already calls setChanged() - an unconditional call here would double up the + // UpdateManager.batch() dispatch, and would also fire when nothing was actually removed. + return if (!item.isEmpty && amount > 0) item.let { + val ret = it.split(amount) + set(it) + ret + } else ItemStack.EMPTY + } + + override fun mayPlace(stack: ItemStack): Boolean + { + return filter.test(stack) + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Array.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Array.kt new file mode 100644 index 000000000..6d5222793 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Array.kt @@ -0,0 +1,22 @@ +package net.kernelpanicsoft.archie.util + +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract +import kotlin.experimental.ExperimentalTypeInference + +/** Builds a reference [Array] of [T] using the [buildList] DSL via [builderAction]. */ +@OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class) +inline fun buildArray(@BuilderInference builderAction: MutableList.() -> Unit): Array +{ + contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } + return buildList(builderAction).toTypedArray() +} + +/** Like [buildArray], but pre-sizes the backing list to [capacity]. */ +@OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class) +inline fun buildArray(capacity: Int, @BuilderInference builderAction: MutableList.() -> Unit): Array +{ + contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } + return buildList(capacity, builderAction).toTypedArray() +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Component.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Component.kt new file mode 100644 index 000000000..7e8427008 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Component.kt @@ -0,0 +1,244 @@ +@file:Suppress("unused") +@file:OptIn(ExperimentalContracts::class) +package net.kernelpanicsoft.archie.util + +import net.minecraft.network.chat.* +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.entity.EntityType +import net.minecraft.world.entity.player.Player +import net.minecraft.world.item.ItemStack +import java.util.* +import kotlin.contracts.ExperimentalContracts +import kotlin.contracts.InvocationKind +import kotlin.contracts.contract + +@DslMarker +annotation class ComponentBuilderDsl + +@ComponentBuilderDsl +class ComponentBuilder @PublishedApi internal constructor(private val component: MutableComponent = Component.empty()) +{ + fun build(): Component + { + return component + } + + fun text(text: String, block: ComponentBuilder.() -> Unit = {}) + { + component.append(ComponentBuilder(Component.literal(text)).apply(block).build()) + } + + fun translate(key: String, vararg args: Any, block: ComponentBuilder.() -> Unit = {}) + { + component.append(ComponentBuilder(Component.translatable(key, *args)).apply(block).build()) + } + + fun style(style: Style) + { + component.withStyle(style) + } + + fun style(block: StyleBuilder.() -> Unit) + { + component.withStyle(StyleBuilder(component.style).apply(block).build()) + } +} + +@ComponentBuilderDsl +class StyleBuilder @PublishedApi internal constructor(private var style: Style = Style.EMPTY) +{ + fun build(): Style + { + return style + } + + var color: TextColor? + get() = style.color + set(color) + { + style = style.withColor(color) + } + + var bold: Boolean? + get() = style.isBold + set(bold) + { + style = style.withBold(bold) + } + + var italic: Boolean? + get() = style.isItalic + set(italic) + { + style = style.withItalic(italic) + } + + var underlined: Boolean? + get() = style.isUnderlined + set(underlined) + { + style = style.withUnderlined(underlined) + } + + var strikethrough: Boolean? + get() = style.isStrikethrough + set(strikethrough) + { + style = style.withStrikethrough(strikethrough) + } + + var obfuscated: Boolean? + get() = style.isObfuscated + set(obfuscated) + { + style = style.withObfuscated(obfuscated) + } + + var clickEvent: ClickEvent? + get() = style.clickEvent + set(clickEvent) + { + style = style.withClickEvent(clickEvent) + } + + fun clickEvent(block: ClickEventBuilder.() -> Unit) + { + clickEvent = ClickEventBuilder().apply(block).build() + } + + var hoverEvent: HoverEvent? + get() = style.hoverEvent + set(hoverEvent) + { + style = style.withHoverEvent(hoverEvent) + } + + fun hoverEvent(block: HoverEventBuilder.() -> Unit) + { + hoverEvent = HoverEventBuilder().apply(block).build() + } + + var insertion: String? + get() = style.insertion + set(insertion) + { + style = style.withInsertion(insertion) + } + + var font: ResourceLocation? + get() = style.font + set(font) + { + style = style.withFont(font) + } +} + +@ComponentBuilderDsl +class ClickEventBuilder @PublishedApi internal constructor() +{ + private lateinit var action: ClickEvent.Action + private lateinit var value: String + + fun build(): ClickEvent + { + return ClickEvent(action, value) + } + + fun openUrl(value: String) + { + this.action = ClickEvent.Action.OPEN_URL + this.value = value + } + + fun openFile(value: String) + { + this.action = ClickEvent.Action.OPEN_FILE + this.value = value + } + + fun runCommand(value: String) + { + this.action = ClickEvent.Action.RUN_COMMAND + this.value = value + } + + fun suggestCommand(value: String) + { + this.action = ClickEvent.Action.SUGGEST_COMMAND + this.value = value + } + + fun changePage(value: String) + { + this.action = ClickEvent.Action.CHANGE_PAGE + this.value = value + } + + fun copyToClipboard(value: String) + { + this.action = ClickEvent.Action.COPY_TO_CLIPBOARD + this.value = value + } +} + +@ComponentBuilderDsl +class HoverEventBuilder @PublishedApi internal constructor() +{ + private lateinit var action: HoverEvent.Action + private lateinit var value: Any + + fun build(): HoverEvent + { + @Suppress("UNCHECKED_CAST") + return HoverEvent(action as HoverEvent.Action, value) + } + + fun text(block: ComponentBuilder.() -> Unit) + { + action = HoverEvent.Action.SHOW_TEXT + value = buildComponent(block) + } + + fun item(stack: ItemStack) + { + action = HoverEvent.Action.SHOW_ITEM + value = HoverEvent.ItemStackInfo(stack) + } + + fun entity(type: EntityType<*>, uuid: UUID, name: Component? = null, block: (ComponentBuilder.() -> Unit)? = null) + { + action = HoverEvent.Action.SHOW_ENTITY + value = HoverEvent.EntityTooltipInfo(type, uuid, name ?: block?.let { ComponentBuilder().apply(it).build() }) + } +} + +inline operator fun Component.invoke( + builderAction: ComponentBuilder.() -> Unit +): Component +{ + return ComponentBuilder(copy()).apply(builderAction).build() +} + +inline fun Player.sendSystemMessage( + builderAction: ComponentBuilder.() -> Unit +) +{ + contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } + sendSystemMessage(buildComponent(builderAction)) +} + +inline fun buildComponent( + builderAction: ComponentBuilder.() -> Unit +): Component +{ + contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } + return ComponentBuilder().apply(builderAction).build() +} + +inline fun buildStyle( + builderAction: StyleBuilder.() -> Unit +): Style +{ + contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } + return StyleBuilder().apply(builderAction).build() +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Env.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Env.kt new file mode 100644 index 000000000..a13bb2155 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Env.kt @@ -0,0 +1,39 @@ +package net.kernelpanicsoft.archie.util + +import dev.architectury.platform.Platform +import dev.architectury.utils.Env +import dev.architectury.utils.EnvExecutor +import dev.architectury.utils.GameInstance +import net.minecraft.client.Minecraft +import net.minecraft.server.MinecraftServer +import java.util.Optional +import java.util.function.Supplier + +/** + * Runs [client] on the physical client and [server] on a dedicated server, returning whichever + * ran. Only the branch matching the current [Env] is ever class-loaded, so [client] can safely + * reference client-only classes even when this is called from common code. + */ +inline fun foldEnv(crossinline client: () -> T, crossinline server: () -> T): T = EnvExecutor.getEnvSpecific({ Supplier { + client() +}}, { Supplier { + server() +}}) + +/** + * Runs [client] and returns its result, only on the physical client; returns [Optional.empty] + * on a dedicated server without ever class-loading [client]. + */ +inline fun onClient(crossinline client: () -> T): Optional = EnvExecutor.getInEnv(Env.CLIENT) { Supplier { client() } } + +/** + * Runs [server] and returns its result, only on a dedicated server; returns [Optional.empty] + * on the physical client without ever class-loading [server]. + */ +inline fun onServer(crossinline server: () -> T): Optional = EnvExecutor.getInEnv(Env.SERVER) { Supplier { server() } } + +inline val isClient: Boolean get() = Platform.getEnvironment() == Env.CLIENT +inline val isServer: Boolean get() = Platform.getEnvironment() == Env.SERVER + +inline val minecraftClient: Minecraft get() = GameInstance.getClient() +inline val minecraftServer: MinecraftServer? get() = GameInstance.getServer() \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/MutableEntry.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/MutableEntry.kt new file mode 100644 index 000000000..a01a31754 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/MutableEntry.kt @@ -0,0 +1,12 @@ +package net.kernelpanicsoft.archie.util + +/** A [Map.Entry] whose [key] and [value] can be reassigned, unlike the standard read-only entry. */ +data class MutableEntry( + override var key: K, + override var value: V +) : Map.Entry + +/** Converts a [Pair] into a [MutableEntry]. */ +fun Pair.toMutableEntry(): MutableEntry = MutableEntry(first, second) +/** Copies a [Map.Entry] into a standalone, mutable [MutableEntry]. */ +fun Map.Entry.toMutableEntry(): MutableEntry = MutableEntry(key, value) diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Properties.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Properties.kt new file mode 100644 index 000000000..e2c07fea1 --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Properties.kt @@ -0,0 +1,35 @@ +package net.kernelpanicsoft.archie.util + +import dev.architectury.extensions.injected.InjectedItemPropertiesExtension +import dev.architectury.registry.registries.DeferredSupplier +import net.minecraft.resources.ResourceKey +import net.minecraft.world.item.CreativeModeTab +import net.minecraft.world.item.Item +import net.minecraft.world.level.block.state.BlockBehaviour + +/** + * Builds a [BlockBehaviour.Properties] via [block], optionally starting from a full copy of + * [parent]'s properties instead of the defaults. + */ +fun blockProperties(parent: BlockBehaviour? = null, block: BlockBehaviour.Properties.() -> Unit): BlockBehaviour.Properties +{ + return (parent?.let { BlockBehaviour.Properties.ofFullCopy(it) } ?: BlockBehaviour.Properties.of()).apply(block) +} + +/** Builds an [Item.Properties] via [block]. */ +fun itemProperties(block: Item.Properties.() -> Unit): Item.Properties +{ + return Item.Properties().apply(block) +} + +/** Assigns [tab] as this item's creative tab, via Architectury's injected item-properties extension. */ +@Suppress("UnstableApiUsage") +fun Item.Properties.tab(tab: CreativeModeTab): Item.Properties = (this as InjectedItemPropertiesExtension).`arch$tab`(tab) + +/** Assigns [tab] as this item's creative tab, via Architectury's injected item-properties extension. */ +@Suppress("UnstableApiUsage") +fun Item.Properties.tab(tab: DeferredSupplier): Item.Properties = (this as InjectedItemPropertiesExtension).`arch$tab`(tab) + +/** Assigns [tab] as this item's creative tab, via Architectury's injected item-properties extension. */ +@Suppress("UnstableApiUsage") +fun Item.Properties.tab(tab: ResourceKey): Item.Properties = (this as InjectedItemPropertiesExtension).`arch$tab`(tab) \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Reflect.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Reflect.kt new file mode 100644 index 000000000..3d2f3601e --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Reflect.kt @@ -0,0 +1,39 @@ +package net.kernelpanicsoft.archie.util + +/** Extension form of [getReflection]; reads [field] (searching up the class hierarchy) from this instance. */ +@JvmName("getReflectionExtension") +inline fun T.getReflection(field: String): R = getReflection(this, field) +/** Extension form of [setReflection]; writes [field] (searching up the class hierarchy) on this instance. */ +@JvmName("setReflectionExtension") +inline fun T.setReflection(field: String, value: R) = setReflection(this, field, value) + +/** + * Reads a private/inaccessible declared field named [field] off [instance] via reflection, + * searching [T] and its superclasses. + * + * @throws NoSuchFieldException if no field named [field] is found anywhere in the hierarchy. + */ +inline fun getReflection(instance: T, field: String): R +{ + val f = generateSequence((instance?.javaClass ?: T::class.java) as Class<*>) { it.superclass } + .firstNotNullOfOrNull { clazz -> runCatching { clazz.getDeclaredField(field) }.getOrNull() } + ?: throw NoSuchFieldException(field) + f.isAccessible = true + @Suppress("UNCHECKED_CAST") + return f.get(instance) as R +} + +/** + * Writes [value] to a private/inaccessible declared field named [field] on [instance] via + * reflection, searching [T] and its superclasses. + * + * @throws NoSuchFieldException if no field named [field] is found anywhere in the hierarchy. + */ +inline fun setReflection(instance: T, field: String, value: R) +{ + val f = generateSequence((instance?.javaClass ?: T::class.java) as Class<*>) { it.superclass } + .firstNotNullOfOrNull { clazz -> runCatching { clazz.getDeclaredField(field) }.getOrNull() } + ?: throw NoSuchFieldException(field) + f.isAccessible = true + f.set(instance, value) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt new file mode 100644 index 000000000..a188af77f --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt @@ -0,0 +1,19 @@ +package net.kernelpanicsoft.archie.util + +import dev.architectury.platform.Mod +import net.kernelpanicsoft.archie.Archie +import net.minecraft.resources.ResourceLocation + +/** Builds a [ResourceLocation] with `this` as the namespace and [other] as the path, e.g. `"mymod" % "my_item"`. */ +operator fun String.rem(other: String): ResourceLocation = ResourceLocation.fromNamespaceAndPath(this, other) +/** Builds a [ResourceLocation] namespaced under this [Mod]'s id, with [other] as the path, e.g. `MyMod.MOD % "my_item"`. */ +operator fun Mod.rem(other: String): ResourceLocation = ResourceLocation.fromNamespaceAndPath(this.modId, other) +/** Builds a [ResourceLocation] namespaced under [Archie.MOD_ID], with [other] as the path, e.g. `Archie % "main"`. */ +operator fun Archie.rem(other: String): ResourceLocation = ResourceLocation.fromNamespaceAndPath(MOD_ID, other) + +/** Appends `/[other]` to this location's path. */ +operator fun ResourceLocation.div(other: String): ResourceLocation = withSuffix("/$other") +/** Appends `/` plus [other]'s path (namespace of [other] is ignored) to this location's path. */ +operator fun ResourceLocation.div(other: ResourceLocation): ResourceLocation = withSuffix("/${other.path}") +/** Prepends `this/` to [other]'s path, keeping [other]'s namespace. */ +operator fun String.div(other: ResourceLocation): ResourceLocation = other.withPrefix("$this/") \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Tile.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Tile.kt new file mode 100644 index 000000000..108bdd20b --- /dev/null +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Tile.kt @@ -0,0 +1,18 @@ +package net.kernelpanicsoft.archie.util + +import com.mojang.datafixers.types.templates.Const +import com.mojang.serialization.Codec +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.entity.BlockEntity +import net.minecraft.world.level.block.entity.BlockEntityType +import net.minecraft.world.level.block.entity.BlockEntityType.BlockEntitySupplier + +/** + * Builds a [BlockEntityType] for [factory], valid for the set of [Block]s declared via + * [builder] (e.g. `{ add(MyBlocks.MY_BLOCK.get()) }`). + */ +fun blockEntityType(factory: BlockEntitySupplier, builder: MutableList.() -> Unit): BlockEntityType +{ + return BlockEntityType.Builder.of(factory, *buildList(builder).toTypedArray()) + .build(Const.PrimitiveType(Codec.unit(Unit))) +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/resources/archie-common.mixins.json b/Archie-Core/core/common/src/main/resources/archie-common.mixins.json new file mode 100644 index 000000000..6c5186a87 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/archie-common.mixins.json @@ -0,0 +1,16 @@ +{ + "required": true, + "package": "net.kernelpanicsoft.archie.mixin", + "compatibilityLevel": "JAVA_17", + "minVersion": "0.8", + "client": [ + "client.gui.AbstractContainerScreenDepthMixin", + "client.gui.AbstractContainerScreenMixin", + "client.gui.GuiGraphicsMixin" + ], + "mixins": [ + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/Archie-Core/core/common/src/main/resources/archie.accesswidener b/Archie-Core/core/common/src/main/resources/archie.accesswidener new file mode 100644 index 000000000..1761f9d0e --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/archie.accesswidener @@ -0,0 +1,336 @@ +accessWidener v2 named +accessible field net/minecraft/data/DataGenerator vanillaPackOutput Lnet/minecraft/data/PackOutput; +mutable field net/minecraft/data/DataGenerator vanillaPackOutput Lnet/minecraft/data/PackOutput; +accessible field net/minecraft/data/recipes/RecipeProvider recipePathProvider Lnet/minecraft/data/PackOutput$PathProvider; +accessible field net/minecraft/data/recipes/RecipeProvider advancementPathProvider Lnet/minecraft/data/PackOutput$PathProvider; +extendable method net/minecraft/data/recipes/RecipeProvider run (Lnet/minecraft/data/CachedOutput;)Ljava/util/concurrent/CompletableFuture; +accessible field net/minecraft/data/tags/TagsProvider$TagAppender builder Lnet/minecraft/tags/TagBuilder; +extendable method net/minecraft/data/tags/TagsProvider$TagAppender add (Lnet/minecraft/resources/ResourceKey;)Lnet/minecraft/data/tags/TagsProvider$TagAppender; +extendable method net/minecraft/data/tags/TagsProvider$TagAppender add ([Lnet/minecraft/resources/ResourceKey;)Lnet/minecraft/data/tags/TagsProvider$TagAppender; +accessible field net/minecraft/data/tags/TagsProvider builders Ljava/util/Map; +accessible field net/minecraft/data/loot/BlockLootSubProvider map Ljava/util/Map; +extendable method net/minecraft/tags/TagEntry (Lnet/minecraft/resources/ResourceLocation;ZZ)V +accessible field net/minecraft/tags/TagEntry id Lnet/minecraft/resources/ResourceLocation; +accessible field net/minecraft/tags/TagEntry tag Z +accessible field net/minecraft/tags/TagEntry required Z +extendable method net/minecraft/data/PackOutput$PathProvider (Lnet/minecraft/data/PackOutput;Lnet/minecraft/data/PackOutput$Target;Ljava/lang/String;)V +accessible field net/minecraft/data/PackOutput$PathProvider root Ljava/nio/file/Path; +accessible field net/minecraft/data/PackOutput$PathProvider kind Ljava/lang/String; +accessible method net/minecraft/data/DataGenerator$PackGenerator (Lnet/minecraft/data/DataGenerator;ZLjava/lang/String;Lnet/minecraft/data/PackOutput;)V +accessible field net/minecraft/data/registries/VanillaRegistries BUILDER Lnet/minecraft/core/RegistrySetBuilder; +accessible method net/minecraft/data/registries/VanillaRegistries validateThatAllBiomeFeaturesHaveBiomeFilter (Lnet/minecraft/core/HolderLookup$Provider;)V +accessible field net/minecraft/core/RegistrySetBuilder entries Ljava/util/List; +accessible class net/minecraft/core/RegistrySetBuilder$RegistryStub +accessible field net/minecraft/world/level/storage/loot/parameters/LootContextParamSets REGISTRY Lcom/google/common/collect/BiMap; +accessible class net/minecraft/client/renderer/block/model/ItemTransform$Deserializer +accessible field net/minecraft/client/renderer/block/model/ItemTransform$Deserializer DEFAULT_ROTATION Lorg/joml/Vector3f; +accessible field net/minecraft/client/renderer/block/model/ItemTransform$Deserializer DEFAULT_TRANSLATION Lorg/joml/Vector3f; +accessible field net/minecraft/client/renderer/block/model/ItemTransform$Deserializer DEFAULT_SCALE Lorg/joml/Vector3f; +accessible class net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager$Source +accessible method net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager$Source (Ljava/util/function/Function;Ljava/util/function/Supplier;)V +transitive-accessible field net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager sources Ljava/util/List; +transitive-mutable field net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager sources Ljava/util/List; +accessible class net/minecraft/world/item/crafting/Ingredient +extendable class net/minecraft/world/item/crafting/Ingredient +extendable method net/minecraft/world/item/crafting/Ingredient (Ljava/util/stream/Stream;)V +accessible method net/minecraft/client/gui/screens/MenuScreens register (Lnet/minecraft/world/inventory/MenuType;Lnet/minecraft/client/gui/screens/MenuScreens$ScreenConstructor;)V +extendable class net/minecraft/client/renderer/block/model/BlockElementFace +mutable field net/minecraft/world/inventory/Slot x I +mutable field net/minecraft/world/inventory/Slot y I +transitive-accessible field net/minecraft/world/inventory/AbstractContainerMenu remoteSlots Lnet/minecraft/core/NonNullList; +transitive-accessible field net/minecraft/world/inventory/AbstractContainerMenu lastSlots Lnet/minecraft/core/NonNullList; +accessible field net/minecraft/resources/DelegatingOps delegate Lcom/mojang/serialization/DynamicOps; +transitive-accessible method net/minecraft/data/BlockFamilies familyBuilder (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/data/BlockFamily$Builder; +transitive-accessible field net/minecraft/data/models/BlockModelGenerators blockStateOutput Ljava/util/function/Consumer; +transitive-accessible field net/minecraft/data/models/BlockModelGenerators modelOutput Ljava/util/function/BiConsumer; +transitive-accessible field net/minecraft/data/models/ItemModelGenerators output Ljava/util/function/BiConsumer; +transitive-accessible method net/minecraft/data/models/model/TextureSlot create (Ljava/lang/String;)Lnet/minecraft/data/models/model/TextureSlot; +transitive-accessible method net/minecraft/data/models/model/TextureSlot create (Ljava/lang/String;Lnet/minecraft/data/models/model/TextureSlot;)Lnet/minecraft/data/models/model/TextureSlot; +transitive-extendable method net/minecraft/data/tags/TagsProvider$TagAppender add ([Lnet/minecraft/resources/ResourceKey;)Lnet/minecraft/data/tags/TagsProvider$TagAppender; +transitive-accessible method net/minecraft/data/models/model/TexturedModel createDefault (Ljava/util/function/Function;Lnet/minecraft/data/models/model/ModelTemplate;)Lnet/minecraft/data/models/model/TexturedModel$Provider; +transitive-accessible class net/minecraft/data/models/BlockModelGenerators$TintState +transitive-accessible class net/minecraft/data/models/BlockModelGenerators$BlockFamilyProvider +transitive-accessible class net/minecraft/data/models/BlockModelGenerators$WoodProvider +transitive-accessible class net/minecraft/data/models/BlockModelGenerators$BlockEntityModelGenerator +#transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider HAS_SILK_TOUCH Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder; +#transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider HAS_NO_SILK_TOUCH Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder; +transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider HAS_SHEARS Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder; +#transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider HAS_SHEARS_OR_SILK_TOUCH Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder; +#transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider HAS_NO_SHEARS_OR_SILK_TOUCH Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder; +transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider NORMAL_LEAVES_SAPLING_CHANCES [F +transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider NORMAL_LEAVES_STICK_CHANCES [F +transitive-accessible method net/minecraft/data/recipes/RecipeProvider buildAdvancement (Lnet/minecraft/data/CachedOutput;Lnet/minecraft/core/HolderLookup$Provider;Lnet/minecraft/advancements/AdvancementHolder;)Ljava/util/concurrent/CompletableFuture; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider buildRecipes (Lnet/minecraft/data/recipes/RecipeOutput;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider generateForEnabledBlockFamilies (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/flag/FeatureFlagSet;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider oneToOneConversionRecipe (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;Ljava/lang/String;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider oneToOneConversionRecipe (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;Ljava/lang/String;I)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider oreSmelting (Lnet/minecraft/data/recipes/RecipeOutput;Ljava/util/List;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;FILjava/lang/String;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider oreBlasting (Lnet/minecraft/data/recipes/RecipeOutput;Ljava/util/List;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;FILjava/lang/String;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider oreCooking (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/item/crafting/RecipeSerializer;Lnet/minecraft/world/item/crafting/AbstractCookingRecipe$Factory;Ljava/util/List;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;FILjava/lang/String;Ljava/lang/String;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider netheriteSmithing (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/item/Item;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/item/Item;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider trimSmithing (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/item/Item;Lnet/minecraft/resources/ResourceLocation;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider twoByTwoPacker (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider threeByThreePacker (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;Ljava/lang/String;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider threeByThreePacker (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider planksFromLog (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/tags/TagKey;I)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider planksFromLogs (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/tags/TagKey;I)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider woodFromLogs (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider woodenBoat (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider chestBoat (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider buttonBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider doorBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider fenceBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider fenceGateBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider pressurePlate (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider pressurePlateBuilder (Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider slab (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider slabBuilder (Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider stairBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider trapdoorBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider signBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider hangingSign (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider colorBlockWithDye (Lnet/minecraft/data/recipes/RecipeOutput;Ljava/util/List;Ljava/util/List;Ljava/lang/String;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider carpet (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider bedFromPlanksAndWool (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider banner (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider stainedGlassFromGlassAndDye (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider stainedGlassPaneFromStainedGlass (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider stainedGlassPaneFromGlassPaneAndDye (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider coloredTerracottaFromTerracottaAndDye (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider concretePowder (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider candle (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider wall (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider wallBuilder (Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider polished (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider polishedBuilder (Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider cut (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider cutBuilder (Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/ShapedRecipeBuilder; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider chiseled (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider mosaicBuilder (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider chiseledBuilder (Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/ShapedRecipeBuilder; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider stonecutterResultFromBase (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider stonecutterResultFromBase (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;I)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider smeltingResultFromBase (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider nineBlockStorageRecipes (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider nineBlockStorageRecipesWithCustomPacking (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Ljava/lang/String;Ljava/lang/String;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider nineBlockStorageRecipesRecipesWithCustomUnpacking (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Ljava/lang/String;Ljava/lang/String;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider nineBlockStorageRecipes (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider copySmithingTemplate (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/tags/TagKey;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider copySmithingTemplate (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider cookRecipes (Lnet/minecraft/data/recipes/RecipeOutput;Ljava/lang/String;Lnet/minecraft/world/item/crafting/RecipeSerializer;Lnet/minecraft/world/item/crafting/AbstractCookingRecipe$Factory;I)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider simpleCookingRecipe (Lnet/minecraft/data/recipes/RecipeOutput;Ljava/lang/String;Lnet/minecraft/world/item/crafting/RecipeSerializer;Lnet/minecraft/world/item/crafting/AbstractCookingRecipe$Factory;ILnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;F)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider waxRecipes (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/flag/FeatureFlagSet;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider grate (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider copperBulb (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider generateRecipes (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/BlockFamily;Lnet/minecraft/world/flag/FeatureFlagSet;)V +transitive-accessible method net/minecraft/data/recipes/RecipeProvider getBaseBlock (Lnet/minecraft/data/BlockFamily;Lnet/minecraft/data/BlockFamily$Variant;)Lnet/minecraft/world/level/block/Block; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider insideOf (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/advancements/Criterion; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider has (Lnet/minecraft/advancements/critereon/MinMaxBounds$Ints;Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/advancements/Criterion; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider has (Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/advancements/Criterion; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider has (Lnet/minecraft/tags/TagKey;)Lnet/minecraft/advancements/Criterion; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider inventoryTrigger ([Lnet/minecraft/advancements/critereon/ItemPredicate$Builder;)Lnet/minecraft/advancements/Criterion; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider inventoryTrigger ([Lnet/minecraft/advancements/critereon/ItemPredicate;)Lnet/minecraft/advancements/Criterion; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider getHasName (Lnet/minecraft/world/level/ItemLike;)Ljava/lang/String; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider getItemName (Lnet/minecraft/world/level/ItemLike;)Ljava/lang/String; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider getSimpleRecipeName (Lnet/minecraft/world/level/ItemLike;)Ljava/lang/String; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider getConversionRecipeName (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)Ljava/lang/String; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider getSmeltingRecipeName (Lnet/minecraft/world/level/ItemLike;)Ljava/lang/String; +transitive-accessible method net/minecraft/data/recipes/RecipeProvider getBlastingRecipeName (Lnet/minecraft/world/level/ItemLike;)Ljava/lang/String; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createMirroredCubeGenerator (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/data/models/model/TextureMapping;Ljava/util/function/BiConsumer;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNorthWestMirroredCubeGenerator (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/data/models/model/TextureMapping;Ljava/util/function/BiConsumer;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createMirroredColumnGenerator (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/data/models/model/TextureMapping;Ljava/util/function/BiConsumer;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators skipAutoItemBlock (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators delegateItemModel (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators delegateItemModel (Lnet/minecraft/world/item/Item;Lnet/minecraft/resources/ResourceLocation;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSimpleFlatItemModel (Lnet/minecraft/world/item/Item;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSimpleFlatItemModel (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSimpleFlatItemModel (Lnet/minecraft/world/level/block/Block;Ljava/lang/String;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createHorizontalFacingDispatch ()Lnet/minecraft/data/models/blockstates/PropertyDispatch; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createHorizontalFacingDispatchAlt ()Lnet/minecraft/data/models/blockstates/PropertyDispatch; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTorchHorizontalDispatch ()Lnet/minecraft/data/models/blockstates/PropertyDispatch; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFacingDispatch ()Lnet/minecraft/data/models/blockstates/PropertyDispatch; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedVariant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/MultiVariantGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedVariants (Lnet/minecraft/resources/ResourceLocation;)[Lnet/minecraft/data/models/blockstates/Variant; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedVariant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/MultiVariantGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createBooleanModelDispatch (Lnet/minecraft/world/level/block/state/properties/BooleanProperty;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/PropertyDispatch; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedMirroredVariantBlock (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedVariantBlock (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createBrushableBlock (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createButton (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators configureDoorHalf (Lnet/minecraft/data/models/blockstates/PropertyDispatch$C4;Lnet/minecraft/world/level/block/state/properties/DoubleBlockHalf;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/PropertyDispatch$C4; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createDoor (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCustomFence (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFence (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createWall (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFenceGate (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Z)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createStairs (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createOrientableTrapdoor (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTrapdoor (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSimpleBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/MultiVariantGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedPillar ()Lnet/minecraft/data/models/blockstates/PropertyDispatch; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPillarBlockUVLocked (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/model/TextureMapping;Ljava/util/function/BiConsumer;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAxisAlignedPillarBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAxisAlignedPillarBlockCustomModel (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createHorizontallyRotatedBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/model/TexturedModel$Provider;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedPillarWithHorizontalVariant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedPillarWithHorizontalVariant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/model/TexturedModel$Provider;Lnet/minecraft/data/models/model/TexturedModel$Provider;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSuffixedVariant (Lnet/minecraft/world/level/block/Block;Ljava/lang/String;Lnet/minecraft/data/models/model/ModelTemplate;Ljava/util/function/Function;)Lnet/minecraft/resources/ResourceLocation; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPressurePlate (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSlab (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTrivialBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/model/TextureMapping;Lnet/minecraft/data/models/model/ModelTemplate;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators family (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/data/models/BlockModelGenerators$BlockFamilyProvider; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createDoor (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators copyDoorModel (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createOrientableTrapdoor (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTrapdoor (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators copyTrapdoorModel (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators woodProvider (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/data/models/BlockModelGenerators$WoodProvider; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNonTemplateModelBlock (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNonTemplateModelBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCrossBlockWithDefaultItem (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCrossBlockWithDefaultItem (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;Lnet/minecraft/data/models/model/TextureMapping;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCrossBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCrossBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;Lnet/minecraft/data/models/model/TextureMapping;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCrossBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;Lnet/minecraft/world/level/block/state/properties/Property;[I)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPlant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCoralFans (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createStems (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCoral (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createDoublePlant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createDoubleBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPassiveRail (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createActiveRail (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators blockEntityModels (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/data/models/BlockModelGenerators$BlockEntityModelGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators blockEntityModels (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/data/models/BlockModelGenerators$BlockEntityModelGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAirLikeBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/item/Item;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAirLikeBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFullAndCarpetBlocks (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFlowerBed (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createColoredBlockWithRandomRotations (Lnet/minecraft/data/models/model/TexturedModel$Provider;[Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createColoredBlockWithStateRotations (Lnet/minecraft/data/models/model/TexturedModel$Provider;[Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createGlassBlocks (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCommandBlock (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAnvil (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createBambooModels (I)Ljava/util/List; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createColumnWithFacing ()Lnet/minecraft/data/models/blockstates/PropertyDispatch; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createEmptyOrFullDispatch (Lnet/minecraft/world/level/block/state/properties/Property;Ljava/lang/Comparable;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/PropertyDispatch; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createBeeNest (Lnet/minecraft/world/level/block/Block;Ljava/util/function/Function;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCropBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/state/properties/Property;[I)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFurnace (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/model/TexturedModel$Provider;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCampfires ([Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAzalea (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPottedAzalea (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createMushroomBlock (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCraftingTableLike (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Ljava/util/function/BiFunction;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPumpkinVariant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/model/TextureMapping;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createDispenserBlock (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCopperBulb (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCopperBulb (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators copyCopperBulbModel (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAmethystCluster (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPointedDripstoneVariant (Lnet/minecraft/core/Direction;Lnet/minecraft/world/level/block/state/properties/DripstoneThickness;)Lnet/minecraft/data/models/blockstates/Variant; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNyliumBlock (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatableColumn (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFloorFireModels (Lnet/minecraft/world/level/block/Block;)Ljava/util/List; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSideFireModels (Lnet/minecraft/world/level/block/Block;)Ljava/util/List; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTopFireModels (Lnet/minecraft/world/level/block/Block;)Ljava/util/List; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators wrapModels (Ljava/util/List;Ljava/util/function/UnaryOperator;)Ljava/util/List; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createLantern (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createGrassLikeBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/data/models/blockstates/Variant;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createWeightedPressurePlate (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators copyModel (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNonTemplateHorizontalBlock (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPistonVariant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/data/models/model/TextureMapping;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNormalTorch (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTurtleEggModel (ILjava/lang/String;Lnet/minecraft/data/models/model/TextureMapping;)Lnet/minecraft/resources/ResourceLocation; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTurtleEggModel (Ljava/lang/Integer;Ljava/lang/Integer;)Lnet/minecraft/resources/ResourceLocation; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createMultiface (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators addSlotStateAndRotationVariants (Lnet/minecraft/data/models/blockstates/MultiPartGenerator;Lnet/minecraft/data/models/blockstates/Condition$TerminalCondition;Lnet/minecraft/data/models/blockstates/VariantProperties$Rotation;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators addBookSlotModel (Lnet/minecraft/data/models/blockstates/MultiPartGenerator;Lnet/minecraft/data/models/blockstates/Condition$TerminalCondition;Lnet/minecraft/data/models/blockstates/VariantProperties$Rotation;Lnet/minecraft/world/level/block/state/properties/BooleanProperty;Lnet/minecraft/data/models/model/ModelTemplate;Z)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createShulkerBox (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createGrowingPlant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createBedItem (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNetherRoots (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/models/BlockModelGenerators applyRotation (Lnet/minecraft/core/FrontAndTop;Lnet/minecraft/data/models/blockstates/Variant;)Lnet/minecraft/data/models/blockstates/Variant; +transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCandleAndCandleCake (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider applyExplosionDecay (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/storage/loot/functions/FunctionUserBuilder;)Lnet/minecraft/world/level/storage/loot/functions/FunctionUserBuilder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider applyExplosionCondition (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/storage/loot/predicates/ConditionUserBuilder;)Lnet/minecraft/world/level/storage/loot/predicates/ConditionUserBuilder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSelfDropDispatchTable (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder;Lnet/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer$Builder;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSilkTouchDispatchTable (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer$Builder;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createShearsDispatchTable (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer$Builder;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSilkTouchOrShearsDispatchTable (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer$Builder;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSingleItemTableWithSilkTouch (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSingleItemTable (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/storage/loot/providers/number/NumberProvider;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSingleItemTableWithSilkTouch (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/storage/loot/providers/number/NumberProvider;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSilkTouchOnlyTable (Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createPotFlowerItemTable (Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSlabItemTable (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSinglePropConditionTable (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/state/properties/Property;Ljava/lang/Comparable;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createNameableBlockEntityTable (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createShulkerBoxDrop (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createCopperOreDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createLapisOreDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createRedstoneOreDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createBannerDrop (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createBeeNestDrop (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createBeeHiveDrop (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createCaveVinesDrop (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createOreDrop (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/item/Item;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createMushroomBlockDrop (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createGrassDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createShearsOnlyDrop (Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createMultifaceBlockDrops (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createLeavesDrops (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;[F)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createOakLeavesDrops (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;[F)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createMangroveLeavesDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createCropDrops (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/item/Item;Lnet/minecraft/world/item/Item;Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createDoublePlantShearsDrop (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createDoublePlantWithSeedDrops (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createCandleDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createPetalsDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createCandleCakeDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider generate ()V +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider addNetherVinesDropTable (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createDoorTable (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider dropPottedContents (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider otherWhenSilkTouch (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider dropOther (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/ItemLike;)V +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider dropWhenSilkTouch (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider dropSelf (Lnet/minecraft/world/level/block/Block;)V +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider add (Lnet/minecraft/world/level/block/Block;Ljava/util/function/Function;)V +transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider add (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/storage/loot/LootTable$Builder;)V +transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateFlatItem (Lnet/minecraft/world/item/Item;Lnet/minecraft/data/models/model/ModelTemplate;)V +transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateFlatItem (Lnet/minecraft/world/item/Item;Ljava/lang/String;Lnet/minecraft/data/models/model/ModelTemplate;)V +transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateFlatItem (Lnet/minecraft/world/item/Item;Lnet/minecraft/world/item/Item;Lnet/minecraft/data/models/model/ModelTemplate;)V +transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateCompassItem (Lnet/minecraft/world/item/Item;)V +transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateClockItem (Lnet/minecraft/world/item/Item;)V +transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateLayeredItem (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)V +transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateLayeredItem (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)V +transitive-accessible method net/minecraft/data/models/ItemModelGenerators getItemModelForTrimMaterial (Lnet/minecraft/resources/ResourceLocation;Ljava/lang/String;)Lnet/minecraft/resources/ResourceLocation; +transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateBaseArmorTrimTemplate (Lnet/minecraft/resources/ResourceLocation;Ljava/util/Map;Lnet/minecraft/core/Holder;)Lcom/google/gson/JsonObject; +transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateArmorTrims (Lnet/minecraft/world/item/ArmorItem;)V +transitive-extendable method net/minecraft/data/metadata/PackMetadataGenerator getName ()Ljava/lang/String; +transitive-extendable method net/minecraft/data/structures/SnbtToNbt getName ()Ljava/lang/String; +transitive-extendable method net/minecraft/data/models/ModelProvider getName ()Ljava/lang/String; +transitive-extendable method net/minecraft/data/structures/NbtToSnbt getName ()Ljava/lang/String; +transitive-extendable method net/minecraft/data/info/BlockListReport getName ()Ljava/lang/String; +transitive-extendable method net/minecraft/data/info/CommandsReport getName ()Ljava/lang/String; +transitive-extendable method net/minecraft/data/registries/RegistriesDatapackGenerator getName ()Ljava/lang/String; +transitive-extendable method net/minecraft/data/info/RegistryDumpReport getName ()Ljava/lang/String; +transitive-extendable method net/minecraft/data/info/BiomeParametersDumpReport getName ()Ljava/lang/String; +transitive-extendable method net/minecraft/data/advancements/AdvancementProvider getName ()Ljava/lang/String; +transitive-extendable method net/minecraft/data/loot/LootTableProvider getName ()Ljava/lang/String; +transitive-extendable method net/minecraft/data/recipes/RecipeProvider getName ()Ljava/lang/String; +transitive-extendable method net/minecraft/data/tags/TagsProvider getName ()Ljava/lang/String; +transitive-accessible field net/minecraft/world/item/CreativeModeTabs BUILDING_BLOCKS Lnet/minecraft/resources/ResourceKey; +transitive-accessible field net/minecraft/world/item/CreativeModeTabs COLORED_BLOCKS Lnet/minecraft/resources/ResourceKey; +transitive-accessible field net/minecraft/world/item/CreativeModeTabs NATURAL_BLOCKS Lnet/minecraft/resources/ResourceKey; +transitive-accessible field net/minecraft/world/item/CreativeModeTabs FUNCTIONAL_BLOCKS Lnet/minecraft/resources/ResourceKey; +transitive-accessible field net/minecraft/world/item/CreativeModeTabs REDSTONE_BLOCKS Lnet/minecraft/resources/ResourceKey; +transitive-accessible field net/minecraft/world/item/CreativeModeTabs TOOLS_AND_UTILITIES Lnet/minecraft/resources/ResourceKey; +transitive-accessible field net/minecraft/world/item/CreativeModeTabs COMBAT Lnet/minecraft/resources/ResourceKey; +transitive-accessible field net/minecraft/world/item/CreativeModeTabs FOOD_AND_DRINKS Lnet/minecraft/resources/ResourceKey; +transitive-accessible field net/minecraft/world/item/CreativeModeTabs INGREDIENTS Lnet/minecraft/resources/ResourceKey; +transitive-accessible field net/minecraft/world/item/CreativeModeTabs SPAWN_EGGS Lnet/minecraft/resources/ResourceKey; +transitive-accessible field net/minecraft/world/item/CreativeModeTabs OP_BLOCKS Lnet/minecraft/resources/ResourceKey; +accessible method net/minecraft/client/gui/screens/Screen removeWidget (Lnet/minecraft/client/gui/components/events/GuiEventListener;)V diff --git a/Archie-Core/core/common/src/main/resources/archie.common.json b/Archie-Core/core/common/src/main/resources/archie.common.json new file mode 100644 index 000000000..be85295c1 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/archie.common.json @@ -0,0 +1,3 @@ +{ + "accessWidener": "archie.accesswidener" +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java.theme.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java.theme.json new file mode 100644 index 000000000..c6f6e3df4 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java.theme.json @@ -0,0 +1,8 @@ +{ + "variants": ["", "dark"], + "default_variant": "", + "aliases": { + "default": "" + } +} + diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/button.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/button.json new file mode 100644 index 000000000..6a445ec51 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/button.json @@ -0,0 +1,19 @@ +{ + "states": { + "default": { + "texture": "archie:java/button", + "texture_size": { + "width": 64, + "height": 64 + }, + "width": 64, + "height": 20 + }, + "hovered": { + "texture": "archie:java/button_highlighted" + }, + "disabled": { + "texture": "archie:java/button_disabled" + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/checkbox.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/checkbox.json new file mode 100644 index 000000000..2cb0de488 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/checkbox.json @@ -0,0 +1,22 @@ +{ + "states": { + "default": { + "texture": "archie:java/checkbox", + "texture_size": { + "width": 20, + "height": 20 + }, + "width": 20, + "height": 20 + }, + "hovered": { + "texture": "archie:java/checkbox_hovered" + }, + "clicked": { + "texture": "archie:java/checkbox_clicked" + }, + "clicked_and_hovered": { + "texture": "archie:java/checkbox_clicked_and_hovered" + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/dark/surface.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/dark/surface.json new file mode 100644 index 000000000..2b79ea1c4 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/dark/surface.json @@ -0,0 +1,21 @@ +{ + "states": { + "default": { + "texture": "archie:java/surface_dark", + "texture_size": { + "width": 16, + "height": 16 + }, + "width": 16, + "height": 16 + } + }, + "variants": { + "inset": { + "default": { + "texture": "archie:java/surface_inset_dark" + } + } + } +} + diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json new file mode 100644 index 000000000..be64d72bc --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json @@ -0,0 +1,13 @@ +{ + "states": { + "default": { + "texture": "archie:java/energy_bar", + "texture_size": { + "width": 32, + "height": 16 + }, + "width": 32, + "height": 16 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json new file mode 100644 index 000000000..931ca1fe0 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json @@ -0,0 +1,13 @@ +{ + "states": { + "default": { + "texture": "archie:java/fluid_tank", + "texture_size": { + "width": 18, + "height": 54 + }, + "width": 18, + "height": 54 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json new file mode 100644 index 000000000..b9ab69729 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json @@ -0,0 +1,13 @@ +{ + "states": { + "default": { + "texture": "archie:java/progress_bar", + "texture_size": { + "width": 32, + "height": 16 + }, + "width": 32, + "height": 16 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/radio.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/radio.json new file mode 100644 index 000000000..7540bd2a6 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/radio.json @@ -0,0 +1,25 @@ +{ + "states": { + "default": { + "texture": "archie:java/radio", + "texture_size": { + "width": 20, + "height": 20 + }, + "width": 20, + "height": 20 + }, + "hovered": { + "texture": "archie:java/radio_hovered" + }, + "clicked": { + "texture": "archie:java/radio_clicked" + }, + "clicked_and_hovered": { + "texture": "archie:java/radio_clicked_and_hovered" + }, + "disabled": { + "texture": "archie:java/radio_disabled" + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slider.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slider.json new file mode 100644 index 000000000..dfd75c3ff --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slider.json @@ -0,0 +1,19 @@ +{ + "states": { + "default": { + "texture": "archie:java/slider", + "texture_size": { + "width": 200, + "height": 20 + }, + "width": 200, + "height": 20 + }, + "hovered": { + "texture": "archie:java/slider_highlighted" + }, + "clicked": { + "texture": "archie:java/slider_highlighted" + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slider_handle.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slider_handle.json new file mode 100644 index 000000000..1b2f9943b --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slider_handle.json @@ -0,0 +1,19 @@ +{ + "states": { + "default": { + "texture": "archie:java/slider_handle", + "texture_size": { + "width": 8, + "height": 20 + }, + "width": 8, + "height": 20 + }, + "hovered": { + "texture": "archie:java/slider_handle_highlighted" + }, + "clicked": { + "texture": "archie:java/slider_handle_highlighted" + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slot.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slot.json new file mode 100644 index 000000000..90c4dfe0f --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slot.json @@ -0,0 +1,15 @@ +{ + "states": { + "default": { + "texture": "archie:java/slot", + "texture_size": { + "width": 18, + "height": 18 + }, + "width": 18, + "height": 18, + "uWidth": 18, + "vHeight": 18 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/small_checkbox.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/small_checkbox.json new file mode 100644 index 000000000..310c2af63 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/small_checkbox.json @@ -0,0 +1,16 @@ +{ + "states": { + "default": { + "texture": "archie:java/small_checkbox", + "texture_size": { + "width": 13, + "height": 13 + }, + "width": 13, + "height": 13 + }, + "clicked": { + "texture": "archie:java/small_checkbox_clicked" + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/surface.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/surface.json new file mode 100644 index 000000000..d9dd37b9e --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/surface.json @@ -0,0 +1,20 @@ +{ + "states": { + "default": { + "texture": "archie:java/surface", + "texture_size": { + "width": 16, + "height": 16 + }, + "width": 16, + "height": 16 + } + }, + "variants": { + "inset": { + "default": { + "texture": "archie:java/surface_inset" + } + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/switch_thumb.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/switch_thumb.json new file mode 100644 index 000000000..035f925c4 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/switch_thumb.json @@ -0,0 +1,16 @@ +{ + "states": { + "default": { + "texture": "archie:java/switch_thumb", + "texture_size": { + "width": 14, + "height": 14 + }, + "width": 14, + "height": 14 + }, + "disabled": { + "texture": "archie:java/switch_thumb_disabled" + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/switch_track.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/switch_track.json new file mode 100644 index 000000000..43d558043 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/switch_track.json @@ -0,0 +1,25 @@ +{ + "states": { + "default": { + "texture": "archie:java/switch_track", + "texture_size": { + "width": 34, + "height": 18 + }, + "width": 34, + "height": 18 + }, + "hovered": { + "texture": "archie:java/switch_track_hovered" + }, + "clicked": { + "texture": "archie:java/switch_track_clicked" + }, + "clicked_and_hovered": { + "texture": "archie:java/switch_track_clicked_and_hovered" + }, + "disabled": { + "texture": "archie:java/switch_track_disabled" + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/tab_game.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/tab_game.json new file mode 100644 index 000000000..5cf2141c8 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/tab_game.json @@ -0,0 +1,25 @@ +{ + "states": { + "default": { + "texture": "archie:java/tab_game", + "texture_size": { + "width": 26, + "height": 32 + }, + "width": 26, + "height": 32 + }, + "hovered": { + "texture": "archie:java/tab_game_hovered" + }, + "clicked": { + "texture": "archie:java/tab_game_selected" + }, + "clicked_and_hovered": { + "texture": "archie:java/tab_game_selected_highlighted" + }, + "disabled": { + "texture": "archie:java/tab_game_disabled" + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/tab_menu.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/tab_menu.json new file mode 100644 index 000000000..f005d6b8c --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/tab_menu.json @@ -0,0 +1,25 @@ +{ + "states": { + "default": { + "texture": "archie:java/tab_menu", + "texture_size": { + "width": 130, + "height": 24 + }, + "width": 130, + "height": 24 + }, + "hovered": { + "texture": "archie:java/tab_menu_hovered" + }, + "clicked": { + "texture": "archie:java/tab_menu_selected" + }, + "clicked_and_hovered": { + "texture": "archie:java/tab_menu_selected_highlighted" + }, + "disabled": { + "texture": "archie:java/tab_menu_disabled" + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/text_field.json b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/text_field.json new file mode 100644 index 000000000..9494a37e4 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/text_field.json @@ -0,0 +1,16 @@ +{ + "states": { + "default": { + "texture": "archie:java/text_field", + "texture_size": { + "width": 16, + "height": 16 + }, + "width": 16, + "height": 16 + }, + "clicked": { + "texture": "archie:java/text_field_highlighted" + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/atlases/java.json b/Archie-Core/core/common/src/main/resources/assets/archie/atlases/java.json new file mode 100644 index 000000000..4f66968a2 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/atlases/java.json @@ -0,0 +1,9 @@ +{ + "sources": [ + { + "type": "directory", + "source": "gui/sprites/java", + "prefix": "java/" + } + ] +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/banner.png b/Archie-Core/core/common/src/main/resources/assets/archie/banner.png new file mode 100644 index 0000000000000000000000000000000000000000..50caab899680004e7c27edf8b5577d6705e562ee GIT binary patch literal 32385 zcmeEt^;4Tq)Ngic0Kl7ym+xmt2rr+QTOK9Z=A$r)RA6>H`13-0DuA@BO$8po_4h2k*25V!g9B^c%MepRJ4Tyg4+anx%($z0^UR_urkVa zpz7liXhyX-FQe$-sCAWmcX~Wtoe4_451lEP2F?I?kDQ8%X1uIQYhft=|M-8H0~Gh! zvrla!J_|i$h*IMg@=B1VhydHtP35TPP8bgxDI`5_6#uGuGtJ)G{?t828$El^wQ^$oc= z{UaDmEG(u5V$2$Y=_l?*YJ5=umdNDJ;BU@{#T)S)M0_vz|A=9+zY?38Uv_P*dBYNn z8;aF|#G#z3pZdtZ>~B=rC;j>!Hk-esgv4$#%Fn^F5g#!*V+MsbB!2`a;BM0iKGW>@ z%YH&bEj>|g_{4vy&4V+xbdwwiy!bN3vYvWwRr7nq-k_$~%hP{s&e@Bc;HZW2cX$kHFA}9NX*4SavzKBkwh-*@7sJiV%8|Wk1 zouTSibqu9KbU?w1+#;4F^%V<-m|C()>(%M&ri`cvAIsO{*`<0F;k2o}<}y^iz~n!a zZxv;D>4LshU&~SqCFE$+`6U(|V!~x!ryLD7wRfA(_Xu$PTVeLae>0XB@+YIIhhOZ!sWDzRd(!as z%7vKtX%f*c^ic6)vbkBV?8`QJ=0nY#m>7^3ra83sn#%K=TM6AqDowLCnwF*RQ+ z&iA*{ML!(PHk+<5#KB@Or7Y??s^b(9-6@$(d%if+WGb@u07?f2zVYjzD9E0r*6Ct1!$+~-Q`z9YvL6x|KQYJI^@pZfchFRoEuf{*&G)`L3qrd!JUWP$UTe z6#}J9is3gyZ(U3tuYQbXjJcmaz-&`t@&{Vf{V{li&vm-3kLPwsEik$ItK-*8E5y5} zKmSvXhOY`tyI^~czZ@+o&j7vHk>YJ!2`Yh~aF{L-)DSb2uGFL-Gh$92xJE)UD4gP# z>G+Aven@`VcRvP30!OHH;f^zkyax?w=hEnvWaz8fKm2i(O7v7PcBdSVO2Ioza!)`zZg(`7aE>!%tRCB)lRo|`gnjT|3uTDYy&VVEPY+1GzvL`0%1c#E5Ig z7@xxp`>~$37<+zS@5_k|s1CWhWXa~EHShjVNAe)@VGVzM%ag)-d-dzpaQeQD_9D<+ zNS^kF7yCwL2b`i{-lK+Df&im89g8{K%*4{jt7+jR{0I)NEd;*Nn9noWr*Y(6b!g@%#WG` z?x^oEj7U(RO(?RNw_#9V>H|o@_wfs;Jn>L)zy&cNL?pgE-VD=N_&jtC`m+D%b=5Tq ziJHGI%ZpA5MYP^w)nv+1#9;BEFl9EFxcgz5+e=fw8q?i0bQ6ALQ+}m3@X28Cea^V( zZ<%YLo_yiJ5!WazoZh0_g4ss%8A1!8TNY1&srCWiPu+_W&g5w*rw|(9zG`Akp-y2a zR&kL5Rw4e?!Fw`THdf3~XsAUy!vO#X29muB+so7NnTlJZ|9;DqwiiJPJySs7p&JtL zu==1v(#ML&^|NAJwE^|G_0TK1zD#E}({AMQeTZv33X!co0TClF+h_2f4j3YsHcVWX zeo`Wj&VDMm81e-V+p!JlKDQ|HKfaO1`|wZlciERkO&R&A@~xMIHWCuytM|qQz7pzv zk!)L{1UQ1-R&0ZZ4J1VKE=IJj>wih2M8^Z`>QMMIjfuY(cr=(%#$m;ZbiZNkEi5Ep zTMdoWvbmNcgOdB5?{O=6KX+I15_JuGD$hB}^lcm4tZ>FcZPtAh{c8e)1JFt;xePFY z>oYl_*)-{M=mE^?HQnf#_Pe#n}ceMPlkcMyS87cyD7jZYlY zuXlZvWAPtGafRn&As7+|6p*+Bae2+nPzw+;ZqeP%ISqJtMOHc4; zO8@O$mZ|sK>sTMn>I*sHza1aQl!AK^uooseetfjenQDgJsA@7BnT$QLZz$S7A4N+a zQ>@KpPQ)CEovA?(%FEKy!i6uPNny$MSLwJxmH&dQJ;$@EeI%q!!f-?|?{mo?C@g$_ zv59SpK~7KR@owp7pDL2Me#jl?*z!$wM)AGh*o>qdI+lPsuqP}OJ2bqbGZbH>6eCKtzmPsS z$Y>67kBISfa}V{#zwja(Su`|g&N}5SUNdj%B37?iz3msn9gPs-c;bBnz$Ad5?2k0f z98^MCTXQ)I=~EounjdekYxlWTIP`6HRNIn6Q7w##L~NnS!72<@DQCzAd2gm`24T^4 zcVnn?>fB)<0~ryNaZNXS1N}p~L-Jv8@-H(9MdD`jAkwRaq6F}!JN{(@ActL+lgFMM zE#y;2N=nbcR}K21)af`}SKh)w{0AL+fIPDhn1K}wY4~@`)kYz<=@9Hay#p< zoacTO)?21hAuzJLBvN&enalmu67#WlO7o~NZ=K&?uf7cSo7%2+_su@q zcJFVP7KRjZ74P;Fxl8pHDvBOc6j~ML^7yJXZ(NV*^HE%=Tm|x^dLqQ4%I|m*gx(AY zg8m{fRQZ1?O~zXya(26RHJoyBgoZtARHnCjNw+yYY-W|8-tNa5uD2sfRk-iUR$VRZ z1iL6`BHF07=5#hhaVX};=ho||ofK|&p{9vjtJ{qy>IB=5Dab`ek4CF^<@avyqb+1) zoqb{cp4ze`oAkk`m7AVaG}(Dx!!|f2o?>x0x2(GILv7R7+s}tuYSi#*45qU(EQ0IC z(;ZSmsZ#SP`@^|Aj(Px!{3(t+&0Nh$H$1*xTcJxC`J$r5CLA*c#Podf`)+u1IblZi z&BN`}+g$JF%R9Q?y>JFbK>rqV1%rbe1RK$p_!jk_Q)oG0?d#~|v@&=^I?6}SEuTC{ zUFA6=3U3s?a~~7^u!&Opwf&g0b?mFwjQdPR2-)3*4sB-NlgIPwUDOqMCF6Htr(T!= z>mPYE=fK2Bh<7QL=JoF1L?zfvu#28WE61eJHoK(Sk7_a@D>HS2W(Y^=vC@@TH$*gm zdVvQ0-LG+Y8mg4vY?i|x$4U3iR&_@In%TF}; zHA`j+HacZi4TX>(ef&BzZSD0PFxzvF1==$c(C|5wpgolxg_2)u4;*&3uFIibr_49OchxX^KN-} z;%$`5jSl??`F(lux#bU~*dP%F_o*p1K8Q7r?ih{&S?^p(Un{Sn;R2Q?r#Blj)D+|6 zODKc%&tt7omvI%pu$PT!RE`%>@et$R4?ynlR7-f@Lab|j7&v{?(mNZBLVSYCcN`8h zfAkZqbJ>ltCP_wrG&!0&E=6nKP925^)i`AGoKNBhVBm-vx=Ro3B(b1Hx2 zN5iG|g!}b3BP@F-$Bk_lw+g9?1_=kB1r>-V*vRn2@g6FTD zM`|)m+xID@9Q|v0{p2M3hX&Y%47oYISr_fy($eHQL5mi)HTI-Y#8GPAk9ic5F3v?o zN3xQ%-W)U^H@Lvd8YceE%URS+Qe_NDElAH(Oa@&BaTPS{+P!+B(pgSC6*M2!=5O`ldu@nyXN95J_+{>$ z=lq~@JD(ZLC3im|HKYidclH8C0yHEBS5dhcv!-{w!0l{)a8vA?TZWj>>d+QPx?p(r zSuM-jL$!Kmp@LnwNN@6TTDiWOLC#`_2x0#UkG^`Oe` zEle$M#m1U2c@3|V(w5~Gxy&`pS2yK+9CC5qOi)wxB%fl_4HrHfXJpzv%S5Fn59jx~ zS+Xy3ECjPNn=Kt9T41je+#GZaRMPgHu*OFAL@hI#hWGt)wuy5GZGUQGuFu~PZ`fEK z^Q}{+$~zIoHbIB*rZ`UWGRAiiW4G^0VrhqX=Nsjq*JnY#=huI1cJ7IqkG%bAsgE)s zNHw?m_>jnQ>S;d4-&MPR80=)a5BY7BcIBVK!&}abQbOYBJ!EuexRcM%pNHD8&lk_X zrGgaxNr&4YMyRpraWQ(S{GryyaOEs5SYy}^Q;woS8I3KgG?`|>6*uppxdA^z=hVkP z-ici(^IEDke;teaX?Afn?*O0XGv!YJa0VOrI9%{d9U6ixk#kFKxCfk8@?{*g-_&%m zb-lAM%7qDqet#VCTZlF1!GMT?*|3?s1{asPhL$h?cr&-XW=;33^r z*MT8sAIT}Ep;Q~bvV!d2Q*HW+xNji-g(*wxk+nz?-GE>31X82B8IeEACxJMv=&ELn zH`GqG02F5_w-}p+g)qv9;3wO3hJFEVa{*TPb=={X+!AC9t7I zi5iUeX<%*=9+OispkL)lg3tGV-y}nKF1-#b?5XzV#8} zKW#rP#(48C*$_6bex5M6XgB*r`L~I}M3Mf@Hs*`HSpAQ0eQ5nNcD6T%bb0wCH!?IL zc_*sdhbEXj_c*)FUZ;XMnH-7d%8 z_mPu6$EmP8dJkkaEQ81H-N zXsUEoluS{t7{4Vxjk3Xs*w6O`>hi>c(4U5JBDc={K(dF)xSmp zwu`+mA-BNgjQw3Rxxz7k6F;=s4c8lnfg;Klue8g4t1hJf86)pvKQK?u zT!8)8NI|dK*1nh)YB)VU&tX(}vQ;xV3A04g+8QwzwcWQ!F1XGh7p!Yod_W!IQ&t%$BHxs}`ir z7Aj_+4w33frKZsGU@O9|q?yhEqIE@N8=>Tn>ori+SMJY7eBE!e&G2lH!P-8URO?+U z3ZeV8^rWU!Kxtt2;hk1a5dC`&Rp%^uVy4ta1NC*YjlsRRL{YOH9`&pAUX&7cAs}jw zGuy}B(7v2X9Cit^{l)D2j2xiZS^sn~m;IB=To|IS&f#^lVT-rZczP2LgF+%3 zujOyR8)j!?SG(&#M#K8^B*DxTc7vh1zII>lv9tUg{;K3TzitJv7C58MLj<*k%%>68 z#h!HjJJe)|UNv!4^+23h`dpFatpV<3uAg$u))LIomV@YOF`R4pxS3&j;tlgc1)yZ2ezoQ*MTIOx{)`Nqa>tH z++EHFUq>t&fDCl~wzO8<^}p!Ng!{*jh9!N14q90O;;j*bi*}E#X2lj z3oX~eFByX79e)j#{TYHGW z*?dSzyN=~0AG#C(5F^UTv8bGd zhlfC6MI>!iX86k6kRY+4I6$BTbt(Fl@(66cs05Mimz?!nUuon~QU@GVv}k_4cKxi0FA16n>HIxp4p0pAm_^Ev#j8RsivcDov?HJ9P=Do^cwx@iaqV z08G*ISV)c94P45Xnqw`p5&^hm9eE!r^7egP+vi3t9ui? z8UV~(3+eMUI20PiL$H>JM=&jTk2&8T8A(F+Jlko1{50JW8{k7A@K9bM*0AClOG=$q9Fdky7C^Zcpb!upeT z86Nwdj*@bIHPFdeg*^YlD{+(BI*L;D6seLmN}Gt4-vh40W+FGM=_a*%+}3B_EYTv^ zEDk~0TsNz->bdo&yzAv|zr9uE{rzd<7huF%u899abh+i)tiemO?Yg@5vd3=9CT&E_ zsAWc-jbQM0c_YA&fXA$<|2+uUGro|)-8sSU~8MtElcSW=W+ERa%Y1I{gshiTro|LsW<(z3``C} z0EX1}??-=RZdph}>!OZHy(rr6yR+`e+xj!cg&t5ie&?*PX zyK{Am1H7d#WifwD3@1GKKZF9ApTnP%zF}fgT3J~i+Z=iw8OXr>x&X|AlP-$Cm?`+` zj_7lv4upnS>pkoE$TEVx2aYAJz`54S@~dnGzUx19IcM{QBptD#V5m~F6MSSi%Eh&V zsXBCgm6m)VrnGPHss^qaweA??eE+v3L*d2zVZWL`NBs%4WZm?~pJR7-s)t{GTQ7bf z%hRq!G79e`YxO%)QZaq2)>q6w`{Qvz_%eV!5uBz`pU0r|`GeICI=o)`5>836AF036 zbW=H%^tCU)TP}onGGaF|;%5;}yXuT{e;1Wy?bvCn^Fz@6NiV+j!qb9S){gVV&H*+9 zL)O)&<_vY+bvILJ*bYAV{lvi${IiamV;3LVL(^yP$t}KT`7JEHTlja&& zj2Mx-Ffdw|B0(F3&C0Z-(eVCq%@k9~kC5J@|A8;D_5<2oIin%Pd|7* zF!m`OJw9Ozqav8qN7<^n6hN$l2P})c(tCV{TSf5%(U{Oie(f?#MeXRN zGEx4KnN_cQ+=w{-Y zX=#_w|HHUDW`QqTzOUvmA!iWC_b{t4+!O1{^G>252+zVOY?$Ff0k5ztgNL5eCbhSl zJXp0&Q#BjwnLRx;)cUpD#srEUA#H4OL36vQA&MeyQP&^-vBc`nH0ZMxLvmlBQ@`eE zb2?>bal=4v2@V)8VOG(&m#=R{tG7WU{5(!!NPY@}>FAQS5@fL5R1`GU8GXbzB8fN` z@j1--Dm@JR@Z*vc(ixAdE`dSL3BZXWm+O2I1ldFzDKu3Thhg-EXZGu}JqzFS9xt-R zt6BY9!VDhWrd@0(+T#O?@RL%r+Gz>#hydn`Q0~ea%}|DZhd2n_Nb(RoQC;6H^^p^` zZgtGA82YZe`992-@$Z%|1Gm+qo(sYRa?@o^u65KG4%I46E2g)uN`I zsMJz-0(4x|ELA;;(~iMN^^3tbhKTd?uA0&T)^pI7Fv-pBqo41yH)qNm?F)(mvV2#@ z!ZLRGLt!L}n-@Q-5xd7e=9v6x_RO^J+IFp~H8jXm7{R}PIDLu%(l;4`pxhyjnPvR7 zw@?z7MS0iTF0c246RAO3z^`xEt!mdDI;aOI)x=6^IJ!u%AD6u%U(ithVb4r{F6Y^u z&d}@!>`iMIThBxSjnhdONu0=6wM|dc|6SQTv__1ZnvnisMq{AGwmgZxfaLU-6dtQ8 zZrrB87oQK@yETOkCAn3%Q98B%g{}wA%OA`;rnzSwaKVtACw?+NUD9|yZfl=!t?u>t zs4beObQ|gX$K?u>ko23l1xSI4fd+?yPT^RA1TXb`vrn)hF1c&i<~ro?Ec5TMsBGF$li^=pjx>4YoFTBP}^1<2<~gg}W*`V>}QP3b0U|v9R{6F8tvw z+Z`$|zBo>eVXuyp15^27Q=zZV38|z^oE(k(z^3;H z`a~44J0Z7Ut;JsFz0HcRtIrl=-i!fKLs819^F-tTH$!Z(I~3w0blHEITeptzz+Y$F zlEjAU>*PS^=CX9Wu+!wngutwiJelaGjr`?8&m1q}G=3`Tv0nbagYWmu5Fx!eMbVe=9HStvwA3Rf6kuj)Rt?KKnmKLQz zyZ688wdcm4Frtmk9rhd=I8PjsFI=yu*ODE?LyzueK_@6}*3HM}hKrB{TlQ{+gBcW^ zW4XI>!}}xJ`s2G89cU%BNB@t!=X@+~G?q+sT!=-QPY{RBpWaIb{Lg9o)bU-3@!yjf z$k0RdutF+J>!35XoX%X+1xr(h`n}xl*af*@lN};ncv$;$zD>VREpC?qnQJbmB^s&x zOW>fwg9Puaf!z{yjiZeoU5?!kP1NZ&%bBNqY(HZE28~kqhXnkgPVAqt(0{)jAx$R9 z(P^yDr8|djkb)7R$-k8p= zT@ShW%vQE4$b+8pL7=*zcH}NwuN*Zkq_S@fI^Idd&0?3#-MTbq69H=Qn&nVS|3*$- zdglt}bT+Fx$^zP?^W-4$9UmXtYe9knn#qKfr!4E<6Y2Ur_;I*8w^saQHqR$_ta8J; zsWJb=cz9yjUi)|tmr2H>z4SgaPo0rgPg%XE0nBaj?hPwx`(nFTlY-lqi|heL-YEe? z9M)AH>;=-CoSgfB+qXuzUB6e&yA;V~_u}cHU`h-r_*r<`O9^Lthiq0Ja#Njy&CBet zpv-tUUm&4tuSEr4eI)_yoa90f-keqixA=5T~^aCeMq?BB~A# zR#j`t1V%n4!pib;;3-Lb0ON6Zz`%AaQ9BhpXhpbN)eQ3J&$`v=uRaJPoZXyG^W{If z9j{?I&pnS#=Pj&df-&OM4XysnG+rH-lXP^f%~U<+_0KGJkkaMN^AHfCAp|ZURM=jW zyFi)R%b<;A3#H+(++C=iV%NB^_Ps|0WCBm?Pg&Y0ZIkN0AYYq2 z$AuPNknjmvo7+BelSdjt%)S8J7m0OHifxab-E<< zem!}xUM@5K_XBcLrlzl*i`Xw6^{iF6zAi;nc*c7TD7@F{%Ft`R-MsP%`vs6TDm)22 zs`aBdcD=GHFoF#QA;CR8dI`7H*WD#EV%F)+TtiHH6|}UO6@6pB-Q&t6@nHk%EPMNW zeK(aZe?H52Fi>NY>t>xvWtF;*3_?rRl(*R6{Ly_5&q%=&XM6kPZ*N3C%%uA&?Vgq% zfO>?I$oxR&Q>p{CYVH`4IJFYBilgw1u~t4kRZ)*)Cmsr}3SC3U`(g$04RbM>%*V_r zIK;SU;hn~Q!*;@#f3rXui2~SUT=B9YghBCP0pkZ%+A?>sa8gnA9?dKST=$iBS+lje zeBFs#vISmWc*(XUH(*1=lxtoRef49M=w7WmyosmZbO$|6X6e>>Lb2}GhOkp<4PA`ByL zu6}xJeKsg=i7YrDut`q#{#D-c)mRic@BYtCEz=LlFk6Fg5XQre>i+#p+0rTOA3VH< zg3n-~lB>Dt#I5y;4j)+-$XJx?Vda@JLM`xZz~f~Yykf%X)XYtFi?sz8HSO zr{(-%y;?mE&M}=@4yU zK2@l8)Cg86(%+&c2xEs$1?|$*(|_EaA;fUUy6MQw%p`xoUcUlxIiA0Yj5}x_Hs@;d zO(&R$4b)JDG0OmJbDj-I+|NH?lR)KEBV20lJpkEv>YF>X`}}#XOk+$9mh*x$@TzQRAxWCQb7MCK#@|&!f&CX1EdBtM0$Pd|=FZt1>Xhvy^8P^1$!b zcG@YP=M}Tc|4`WmT$sBnIgHq?k{O2qwpMocnfcNyRGBG}|2J)Zhli z0;x{H_Er!1a*bgsN%S~6w4@8GP0%fbma}zznut{XK1Z~2r0fdJmqa6~u*dnDo404* zSqrH!g(!n*2^;I?d(0bmt6sutISEwt5^{c5si>ndLUtU#(T;?6L)qUT&+~kgfW)fJ z;qDM1#yZaNAnTHC`7LXz(Mn_4W!AeWFKF@ZLvZLGNe|gSo|WHmLCuI~Iob`KCMn)2 z@yc};PlC>7Rsz2F3;3GYKeR*$&TC|r#pD2M#h8v zEY`UBD_>9iuJ3GYR-WRFSsMf9IR>*3-Qgr=smSrzl15Fb1;b-6k))8ykzQ~f1NG(GMi^EsaSWX?bA zyOI{41y`Egpss?O!e%u%)bS0<%x_2Kk(NdLx%8TMUcA$amrCW;mxDaMpTHG3OKa8m zl%M<52AC3DvN1t1^DvLod!vMa2?C-PjD zy)|PzeQ(vbFU#fLHNRo?;Y!lToL@i}rA?w_?wP?J-HU1l6k{f(9ZVl`Yz%~z8w1VL zOf(=BlXdV82b@)X$L|U+s424>Uo%uAnRS9D$+45y6XVn3?%~iV;7$TV zRvn)|D=f9aq|8R>3TN}8mteF@Lb(S2|FU#8tr(C@Ywde&E zyvs;6JgDV+;x3&WyE}hr;)LiP?t10hMToqYv@zqENzw_2NWTCx!(XdJ94}^D}S2`J0cI0vk|49m0skf_1l$`e0`(hhm(BxZc%at*+*GXMujTS z8-#Dm&$n$cNBk}xn_Lp&P;#$ire9lek%ZfJAbLVFp&&hTyAyi|!jIbM$8`$~p^P>D zr-AV$9(xf+HK==KlRGUaa&7yISy$q5y=j=D!ET29RG+0udkrDDgRdA2SvRt#doZbK zcIaCd@5*e2-n4ot;Wnh%sD8xrS;a+_M?>kOZ*jLIx?0@h^GCt6)Njr+(_jPt-3@j@ zFvhVYfEU7rXKwpZ_U-X9xq{$^71?JUwsy6Ha({yR)-Kd)lMy9lcZ+jWjejUnF-3*K z0m1l7{2Xf}OV3}MFPt9{fXT{TbZ1kBszMH;@2AgXiZ`+Zsa*a6n$rVEi!z8_knFQR zuMfS1*NJQMa0H6(G*l(D==<1LQTHL6A#N*;jD}OQ<;O!i%$5(o<@#Xq2a~)#n)AG5 z=f3wZvo!ZT=O>Mj&qHrUmt%XIZ4J*9EoAfP%_q6yO|byS4d&b73M?yuAy19@5IE1H zHH;_VTorCP>#=zbXz;d>S?$dxPcV_>e7xsjm9X9u2BPqF_cal;vRk2^Pfv^kT-^1e zscfnb8K)QJsF0*a`jTAA4b{i|h+n+9-adWt)HcKFZ+0vlIa&dgicF7WAuhINry5K* zAZVO(acBL{woPz5J*t!@eq{>TDiCxd3v4 zQ1>8Q6}W&vM2Ts-?!v;*@R<7CY?DdJC{MjbyN~%f%x@rrvH1s&js5#dn^9lhs|p_) zM~;s9n~N9YeX5Z0UzD0fG=I*S?d2mxks224scZ^y+=!vH)$fL*q0IK^{MYy~(m1N?RSsTC@ba0!Q<>SPz12aU zp$>SD+fVld?RtiE_|$&&yLFl>=S<(3{7jM`%qiO$pgJyMgo70=O}@}(cJH9TKab44 z%yY&)!Zc(MfN-4Yu~L2WS&y#2Y|uCNwr9jp_$hI#&t{r4xD8wDn05Nlpu&lvIV|HW(n3MRe6n^Q|G2gSjcIl>z28hL zZYn%+hkS9Q4JW`#>{aC@%6*u7gB+xEi?EdDD^-?t714n$t_8CDg(tn;ho}y-1w-Gz zk=2JF%-+WT+>_6`91~9$%^57ji8uc_qBl3vT%09Vi4Z%A5pdHH!D21}$3j3H7 z4_nf1B=^adBK3I}JHH9&0V2rsJT6itg}8?^J+bof(cbD62H$x=V2vJJD&i*PNVZvu zbMs-;FA)nlC@FR_5)vpO|1xVJU7vm~gV~2Um03c13tbpB+GB{_)-X(a{f(n-X{oMq z7bDT)A&cchX!TbyOL>|V2JQ+Nql@eIYSZ(X1(M!?cHuLLlg;bqqoy;s)g~(wEyoV< zvT~D*rt9)Nv~A0f_epVDOY3r;+wH4<`$HV}FCi!WmQq>E@^7PY3WOc5^QoGGc3T%@ zOLu2;=1w2wrn=V?sSRuIMvGYDGe2!{!S{dpgr;9gN_t<_4EIr$W~ws2uXhRYj;wCW zb$rJ*hrO(by7U{xwg5-YSwMWnK$*>feM{Nn?6owakf6gim|%uI@qAM|=_#$wn~koJ z;0lylGqKStk^JNI{Oxa@HyV12v<#~|jvZuEFM2(3oET zx{K3voGpm`(k}qO*&(mBa)rt4y!_U@-~(WN zym^~H0Rd;u)35upR{IvWx*!hEvuCn!(0kEhlJQ`5%~RKA>tl?WXnI*dJd0sb zpcd>_WbkTJhc3P+F??*8m_@ovcJELmX|^0=FFnBig`ba&wIlVHLN+seA7@YbmatN= z??OrAyEGI&oGUX#$$z%;XK>iW%F~l>DnI#fJd~^-Ih8qBP=>)|W7x8vh~M^9rDX6l zTi|2eWy9j(G{vM}Tsgiw(hW3MYx0!a2PqCHbFd=G=lfUc=x%4mnRM@{F%wi*Jo|aM zk!ke-S1=yDPSWeGs;T9YY=`cNmb4YbDBwxzR6lFk&|S{qAyem#S8%|`0bJxh)5+1x z4aL+bC&AjSh`kRI`TZaka5TE<9!A;+VbHDns2x|^0yRImePm-ljpDi=Ncv0tBSCRP4S2mklxKW{ELblo*5sWXn&B6Vh1Zqlp<;yDEyXc9hZud50Mq_rPG~eOpe(6 zuZG6TUR8kL_AoBIz?u-*kubze-F<7~ysUAoRNKHwVdBOSTo~)t61J#g{JVp=>NyEA z@|=2B3D~=T>NM>cvUpJNl$sH~rx%8gl2iY1_xbX$$XV)9GK!nrp#C(fp|~owO5Csg zd9D$@c1>RYmxoPD+@?FP4Ke`D({bw$Z~IzM8B69Un*s{EK=MH@4y$4ss*IQQxR6nL zy&(nd8$NM@Wm{eS6(AX8u91 zH~psw!=|Ibx@0Ezko3%^@!alIVZV*V{GRFzH+PpUH$*7(p~*+mYwl99FRrp3M#`IA zVu<1Iv>V~2f+E>V6B8Vk66)XmTf|ks#>2qdL4~K!QpI21Mi@{1fTvV0aQ*Z>=3gJ0 zyXVvQ7wcg`seQSb7}0&K`Kw}>nbFRG*L1B{D9fIWnt{b zifzJ~a$@7HL70%_8!pzBveuggg}JXF5#J7n=C(?^oWpO>P^3*?v&c4yHx&qVyjS6Q zEQTKYZ<-vW{c4|CCLdQP+&!Ztg7)KgnY{@p(Bd;PpE}Zn4Og9;`Z#7_*K^Mz8ulzGb6~_j274%nh zW}x!+`TEzfHifZPj{~{#OI)4F^Qj>Glpr6rzrUfK$rWb^j9B9Z&=NOqwiP-qe%%Q& zD&@0&)$**3%#TJgusiH$|7;Kc^O2H+PJ@P_SSISIuhoO z97A5G(7=IEc)vB-N}-;>Nw$JNE_ju$1l#^YE#*nYh2!)M=nf05we6lq4Z;<{#ZCb} zoX*|xx4cB4SrH;-3JKhLMrzFIqi-|cflvx+kCa~((YiLWbY~80M9|%||a=vPs)(wsCc% zN!=^he_-6(4sM50@34R=2 zCxCXg=}i1fL~~Ux?8*iiAr2Gx?+lI8?p#;8`^5mV+$Yc9-y~yF#oSA^BDc^ z@{1*Xrin&lN7Ww>=hab>xjQUFs{yet7SVDobAC&P!@0-Z7>FQ;n)w6Qa^_p@o#I~S z@yivP_1-W4%J1t**r53wDaOch-Y?&YLJKIkkyGZbpLT72JXQt}x)7)Qwa-ivmMi@1 zOLH+879Vk7t8Ngn2z2f8VST%A=uc$xQPt+#{s(E{1W$f&D?cx5GjcC9O?Ne52Xdhmmte)bO zaR$po{kP(d+l7M^;d457{EHf%(a7|b4n*ib{psH-^zT-r zzO7uhtq^A3V2mCSbPVb^Rg)yyVn|j4_d;ETF3kc*Wu}cC*J(#hO4&qc4KP6b{O|_d z#!Hb)!Y%*x@CnxEzeJ$hq!BH+8BA1FEh)0)-y-xmZS<)D>xt$OZPB-v^k-Al8got` z1Hexg?8DuxiWIzlw5CW3-j$VCVMUH@)~8()v7k)X`TDulOdA;xDt@K9{X|m{uz88= zqwzKzm9IW#{g@8h)msMl$`iAVtq(vYQGh*aY=AEsbjp8QH`iyaQB><9H3>h-L4+TU z2W-iJr77ZIkURB%95#6sE2FK;v$s6CLom6sF&CNvdUZn;IG>x0>ao|E8;^cL#pP=6 zPfvb5$+hYzGz+NVZ$awDuQe1^f`Wy&+fo*(w3OUydt3^76cz!4PCbv2qQS3PQrIhasFGYq}9-R zuEwGueBc3Kz-Y%vJo?N%oW(o%PD|cm0WpX!d~6Ip1-6Ux=hf{-1dPPFj=UWemXG~4 zQ0ONo@aelN&azO{MMR<=?w@EU=0hV{w!fBNW;&VE42*h1Pd!)VxrcbE9KL{1EloP? z$I|!M)qx-5e-R?Jo(bKT=fZp!8po>rE-n$_);8h)d%h{=^29*b5a?n&yS!Ur+3MU? zp5$#X8_wjWxBA#3UJ(!+AFPPt>vU6IuFdGS*vF)GxfFP2-QjR5G4kz94yL&aPHY?( zn~U5^wTiBIvw-8iU!n`AAmyU2fibf@U4`q*8*s*99{UuKdhASqJqDdP{tFhsp&}%P z4rrrEJUoz$Fl!P$;%0ySC{~36ZOW`Dit*#^I-?fyAW#r`uQJGEBM)y+F5WHB-$*xO zDdmnyhyeGF^S~TTYr8%#_5{;ecoDZPfjGFN15)>TzoawW32iy%Q!x=gb_wXOvpMk& zIC7pmJZ9y(GlbhzT3UGdsY~4q&If-vE$KGabv~KG*YDP9BTA>Ljd__-M&P-zFb|z4 zuAp%k%gQ4e6UX8sY&oscU%T&2Et^>(JJM)ou`rbS$*X4oja43&YNdUnR>x@Zr1ms+ zODcEQOmJ_$7aM?k@-)`HYrN;lP$~msPm4|LE;6t(AKHEAg{cY$DvfBLNl(>VYs9Vx-+rX^%SkY*53HE5nI*KWF=(K4CgtRJz;1l5?E+bGS7iG=In z%ruDw${LDK!q4I3QkUlviG?e??FZua;_D;XSdPb5@wK2gxXRx0(9!+J9vAK_&E`W# zDVcEYnfAdql&U1~iuEJ`R~pxuj(l4ue2S1EZI>|VUS<6;@`bgYcRBuq$YV_r9gqVE z;Qy*zgJ=-r?-y>8JwS@>FI#FY0)W94m;ykj*fn@Gkh0GBjp707+CxIJu>Y|`({pWA z%|#|RBMQw{-1=XiVZ82x-SRJpzjLF@f36X$yXo!Pa#h+K6+K$&!!6!5d7Eei&M0$p ze0{LzM!2`&btP#nyCJ?(|Lt60g~Z4#PUh=}iJB+JQSq#&rDa!<&V=9>QdS(J+LVe6 zp|gEIAZ&(WD^&2_M0_(C$w7R4<|jF9vL1g)Bw+#^cfOQ*CNovFt}oxd3H42%N6r}WyA<3+&YM*Ljr~QD;;@Obqz1WE} zW-Rw|7?e)1%hfhS?=c{UmvadiqOC_G&ngKE_Yb3bRKlzLWZu0_k2sHjVG0XfO#U&@ zK2MJYvnFl}QseL`3@r^%_*@IJ( zwykh6noDQ;eEM{`#gp#T^f08vO@5}FTU(S&UR3UH#-oWRg+KMQ1gRcJr-e}wT`SEX z${4y;!V9i`oN`xVQ$7cYq zZIDUBKK9V{_WAYsq!kIBv_}>8ty}TDw}=W!l-j;ec(fECDd#1@ z#UMhi##T_)7n%vgMcFB43E_Z<8+x@Ba_y*mhZYX6+m7Q@HyD^L^1vBEr9Ne)%mk5B zBchWv{kh`dekIV%O1->jhY}JNSdyrPyVCM!=BHXW;rS`xaRUg8yYx?`BQJ9r*z6w3 zWc4V@vI9eB6cm;ROC=1qrub+IlbPwNy7amtgVQ-^Ioy$F7G|_EH-P_Rl3p2Y|F@cz zcv}=CohSO+Xk>odD5S2fz-d|)$l%^)53Xu!-&^j=4S1+N&5%5&n|j!}&5Rr-eESTd zx=Z%MP3TuXWnvM;XyVnK^O}6VbIE_&P6^xShUKxRmHKTg|3!M@N)A*?(^_z}-1_LD zK7OCAIa-&UY;mr)*Q-`4Re`0L1wz90CjNn-9k9rOY|{3}VkqLt?>3mFeNX!Cb@eu? zMN{pKx6Wd&8v@##ADnXKE5pm7BioZH^`ziaP;w4?HOXqADBFg)c&$1`hm|;&QZ@s7 zv8t7Y=srT8&PNJ2o~}1%_zNMP@OK*xv~>PwpVmVXk%|Z8mfY%d#3Kk&K8#GK_din^ zF(VFs5OAj?5hjl+N2Fw9XF$fK`89&|S2?w@_u4`1zQcJ=GAHl@9BSe9m5bNhhYb6J z+-n2I8o0UVFXSFc8|Y4u>N(?h^AvZ< zC8KuQ3@1!VFU|+?wx`e05~uv5SLG6Cg7-X*veN>4*q>3G$ELmnw6$(M&?bclv1 z?M03;A5;1P(eak@tW@J5vf=iGzrl1LJb~^AeDs~9`5T`Cz>#C4o3(e?`dHwVKU&*x zP@B{^Q(8Xnd|B%FuzDOh5@YH`{aW z)<(bE8W)KC`#&3sQk=$-vR!HmMKGhO*WhoSOBbjZJ9Z$)h$Qmn4>@3b~N1j>AP4wqIwhN@AuW!3P3Q0X3 ztSNsyqJ+LsD)~EKv};_YRqL{r_(O{hPQaTp_x_xV#BF0CK%z6@Q0j$OWdtP!obc^a zanx@V^h+~xON(D?imZ8L6mNVsHC@808*o|K#DjuCJ&%>S)&j1i_bUPc*hH%bY+~5y zgV5!_g`K-Oa^>X}YFDm~otbLxw)Q)emdb}r$@Y;(4 zcFN;KGDab0VB8mV`j-xgie5*6-|2O^5#C>>I)Yc#;c6YYCT*0bZJ2%}WGLr&>n`^Q zNZGype{E0*^`>i!UTgb1m5;>7BqEkXAKdXTLLMvjZ&>sQ8x*daBie@PMn|RiakGQY z-HY2Fqg$(%Yds@+(kB>}vuthNfOZA<^!Yf2(5yxg%OS#*-6OTtKOUbs#0jz`A1|sHMhcG(w|M zxBV9hm75$@9~BA_&}>}+q491#P0-|Q5T(%5%AkjzJ;mXLk-9Mbf|d9tNdO;41&OG4 z+Fhow`_p;{L7`J9WYsz2bPwvy5(RnDO?e~ntdKmHII2s{ zk`iA0i&M1(hxt#d^3u_mTNU;nVW|a{3f)@ zg`i|w*2p81*UGu@z);qRIO~2*y_m_DsGAj0l3kRJsAB9e@bL19fOe2zG{)W5=ux`% z_`2KT9`0(rnMfbCC+?*Hx{Ej2&NA~yma&EHT#t}?I#WIC4*VcV&z@-K1ViJKIpMhlc>VPM#W zv&J0*8uvT~wShm!d+ z{DM-j_k7|K_dG3efv8K7qi1M5w8u@PtKi}j9?u&I+uOdoS{1O~1 zq&3S_Rxf^R-8KCd`mJ*_Ln;K>*68muUh)&9G0$nNp}P8?X$J0I-9ZqL^J04^$a>!kP8Kbk>FnX&X7S}rapp*7*Vi?FQW2?q zq*iF0FeJq`Bp{D5p^o^nx~sQG~RroJkP)?_vbli zIOH1D6tHEO%j*B@7;i9->$*ewc&9Jqao_Ph8_fl`BjrHq>@Uug7*h>hRR#~oR1GCz z1TH1FSe80@>m)bKcw?WF;LGom?)ggvG$8q#*&VSqm(>Z$CDsUv7E12j8=Bv?kMQ!oZwQ@_Oz_7a;_RN6 zYqIh*_la2Y`OzBE5ATN$u3q&)i=v6S6|x=e!{k2Wt*Lur zY6m(Z(jc5~u?)aVHQUXL+co;=^_HdOWn74y`!b_LGo z2o?QA@Uj!(wWG%IoT_?yE$eigNFJMiof|p7NJn2Fj^A6?T;bPm=@f>}!r zs+FMHIq#JB8k216ld7TVcyXD_pg=mO6#?Gb){F{;2tOPT%4Dx2EepRc3Q{1iut#NA z!R)TX*4M9;Bjp5d6Fp%2=kHK-%8Q4orhmaafxJ?%8xqRIBr7D^-9LU)J}loYt|Ik9`Q`ngVdL-LP5Dy)K-bFsS4}` z7UrE7x@?oBi2FrdUh`*ERGNJe9><4$LOxq;u-7*1%~j*lUAj*FSp;D5Cp#f1!!;55 zy{@Tc4|M^P%{MXI>A68cdqdWb!gqFIpVV&*Nd0VCwmW_=#4W?-22K54Trq}w6_rLz zi!xOU7qcYg1A|UV!HdxG8Gmj2(SQl~nH9T0A%!Ksj0Vmo$2T4jSfQVssd1ytfy<@q zloY<+nwh=2xajWlg#Jjv{i@_X_4aGGP*(@M3-Iq8?s6F+XfN7ZxZeTb6HH`_wLW%c5JB?Za~T zZA3KwzjX7@?B?o&n&C z;4s4S-2((!O!cFXKW11RA<{8-m-tSdT_tVE5j`^QEMlf zU9%Xn(2hZJ@N-sF@`K7~Vb-pJVBMa5?nr1mrJ$TJEowO0K_GDg0%-nCvm^hFq|?Y? zefM=VV8KrO1GDM3X^W?dg`TWBxb1Y=l$_)yb^A6Nul#USS%ZxEOuDpKsO@PJ_z3hw z5_FeAZiQ{Q=dBb=)oZ=H3o!R~Mcsh1-;lx_o_s$%V@*wfdZkLS67AQ(uu$?8dr=vK z8L(rKeue<6Di^!?o*7Zv<@EQg1>jze-bPm>og9utsW?KtH1Y?)=2?vBNNeqygpjc+ zG+_$GTw2t%fyX0EEkqyoKQl{}G|!>D*~LJ$PW^fjJj4kaH%z!tSmh|pQsKw{5cqC| zo)rsE8rRnZC$iA1%Hq2s*v1G7MGuJ~YxOW;S*EicG_1b@CVAsd(_3iZOzm8MsF{QnEm+R3Vb&rpSk7>){ zw2?p`nQQ$}X#Aaoh@5`VNK?UW5)63R&VTjx^_ur$$x0?Q8IDZn1Ju>m$&&+gG zACqw=QD)c)B;{JnmaTm(uBPy1!V&B?>UmF|4fjHe)CPQXGlxM%N9~y%v>x(mVw>e@ z&?sU3r6uI3O%-_Cp%z*JV6M1B%d~k)g{>heLF{6&Wm`*9Gl{3$q~ylqO1;HqqNLfI%HA2sNiBb+328OM45jT~VCh&8gg3CV0Qdk}0IGPT4Ej({ zm06S_2NvPD7YcRmmT|PkQ>sKbOx03%LCztepQDASZ@r%hF*}?{qJ5Fzjxw4LJKOA&IZ#HueNlg&mw06>E9-XkE$o|E}-MrsW4E7t5shOjXEFnn@? zFIrOW;_Pt`&S@d8(IcA#4T1%9itI7lbc1|>NHftz%;+94YwE;wR@TEHY+mV35r`9X zYQ(!xgtyr;rmVH(wOS`ry$^Go1EWo(b}9-|1kiFR@k~gSF=}rr;lniHQ#LC9##yih z4jJ+C9zK+Hgopd^X2&#?s!dS~9f4V_dwH=+y0*$zo5`cW0Bjt&H7S*vaf>>eJKTvo z#E$fVJthSG+;sCFeQ|}eUVMvf-ff(vv}{K?!U1eVbdP~-EuIFad<5;Ts-&9Qt5cRQ z&nKdmu5)2l4}@!*zuC1RK+5Sq)gY=NCC9PGNf<+ zQ9Qw~&ECvDCOukcq+_6<c9siMi*c;nWXz-m8Io z{~G%&TtkodS7@{4DwsDauhr;m_p?=RJr+rI4Ees0%g>|GGvl`FsIH%#dp)GkfO{xt z7I%Oc>I^QuMI^Oe4sdRcNU#CaLad*W0k9s(B!*$Yt$*gt93m!i2uRK0E*Rs&0rbKj z&fuw;8i-YUBbK}ku!w+RflLuw>025p3a&@6ks+#gz2h8Ur81pOf*Kuk$@A?QPfiV_C> z;Y>;bMxZUPwM5Msicb}!q;DEAoB&cWDmB_}Ka|L(zXmDk#aunh`j0EP1WXj)2|Vt4 zdOiFqFTqRU_-1!HQgn00`5@Qjuk3ex-}fKxBttTn+3B#5-Lvs~>Wpb|Y{Yd$(00sn z_hpL1+%ZET$1a8ji3Vu_)5LcCrG0$abNcpsVwCOXfJRk$Yu@VsP%Vb>!2nu9=VwObMx_jt9y3yfHCRti( zZT4M0uOF=Eg$uZuB-MzHy12^651S1&T41(exMKVK13e)BBYU(NACQrxM;(zmb2Wh^LDfZ8o z^?okz&_6xPvpPM0FT0i`6nZUYm8Hph;|t&V&A!vTZraz{Yh+xnmcyw-A@mWvJ8z2J zI=uD&CQmyL{@kFWRU3oQ*2;w1_6gViu4qFOMy#<|$U?&$-`K8R?#glHP7zjuBrAbu zjvdBH?zRs7hWG!aPi})c%rl2|Twj)^aMm8)Yc(N?+3VhPaLj8pGP5qX~9$lVsgZ{F#4E>CNN zYsL-iq9?=uMWT3B zi%gj>i{I2fW0J(eY>-Xtr))=WI&#KTZG^1V9<}J^tCPuib|hkXK-!LZ~V_1U%`a&A_I z+Z;-}iN7t4LJ?u%^*ZlH8TihYS=F`qIyZ2lw}FVn%a++~cP%ZMfzL=c>?gq1tye&!$_n+4 zZ;)Mo#;H8}s;*y@epiY$1XJ800#Ou}^W5XI6~VUKI=#rN^p=)fPJ_t}-DG)(nFg+! zGL``@A)3%a)#EyxZ>`yVe28Gx^{DEkE35mBW6aF$^p{(@J*hYVrb)c9>BKdyis8=9 zLg95U%E#fvCOwdr%Mv!ON&Vt`W%t;nUgH)jZreHgIOe;d_@w6lhE?abnx^57Jk?n! zE~BZTj1=@6ve7mYON3iS`YWGEfL_qLhyZlqvhLtyw+yS+Ygm%P7n8S(HV{X7+4>%T zGU9!TVORDPjK5PCpWRmM*s|TBmcGYzRzNnFw@GzxVs{>A+({y|EQzd#{cLvqYQ2|y zpQ!^Te9NietzLDCKOQE0HFL|!{S{d;1PV4sbH157%Jq5w>trE-T9ZPLfsr^tT1r8cC z3@*sJrwZ$%l8CuSmc`E#!4f2PmbCM}so(um>n211CAlg9BGiVefvDI=FrHZ=V|`tl zl>tnIdk&3|=?u;7AxHYU-IJNc*%C zxN;DhpPH!OdH-}UyQsl;6cpLH;*grm^*Yhe<;wpWCD9RTYSZ;E!tf5%ph27#`U;G7 zw_6JqGpeaVvckUqFnpj&bhNT zBDLDJqm#aER6Nw_A0H}V1_65Jw%@3t2gXa1Gn3oRyPj}Xn(xkTNzw-Y{9CL{hK%>X z3Gkh}W6)7-uXt=1`I~6L@kNMAqZt!p0jjn64p*X6$?&T798_s51u~xJJ?j3p|7i~B zKWSFjS<R7nWHj9*)NDieH>mdj(EmLOZ&1zY#rWlvk%^ zx0`QjvBqFt>L0va_^2CRu|D~UPQ5IT0cMfBW}uFmb>I!C;4{5^)Yn&e|{s(bp* z{gS41y8p)asAoIq7~-!gnGxW|PhgyZ?dT`P^mldUJhzXH?qp*^1O4P^-D6AqXxTwM zErMS#l68hir_$2u>EuBlstz)pkZ_F;&N_iKLYmZUAaPlfN%F#W9Jy>l@nFr1Em4Mp zGVu0XIT?QIqTsUP)secgo(D0Mcji4(`>=x) zBYQB~Fa9+0JI&uqxLdqL-;!k8yXO~}7SES9DKqs9D3O%PE$k?IHC5*I&-cvfvR`?mnjdCHpfLtU2Kl9LA_LIYEu5C(4SxBW zw86m+aEIA1tKoNK3PbedpNUBB$5MJy@J?E)_l6y0ZwXU+F;GagyrTS7eO0x0jjlK= zQgqD`bk|Q01^yJ?nZ$Di#zOHbeK`7$8s`^Etza5s>W!3a&Kl%afyD8}_l(G7^RfFN z2fyo_HLyP#DJ!2nQ9e&X=Z9_t7^i&{$Rrq!YM00^WZM;YbT_zZ0JF~0X*!mFLDB$45Dt{O2<8e5+ z5l<~>?9ErP;l*zP6_c^FZ(?B6bNGmw0%q@AUH5pt?e2KKgQZGK!UJivH0*2#s~O-h z@|}hC&&&2-ypI_}4*UIVjTy@LxQ=RDr#DjRKv(kz%6;4;6@#0}WgfdX6w zw+#?5Y=MLCPw91Lrx=R~94@KU%6hU}ep)7=zTp1X$Ak*|9hB{rq}^1Gq9)J28E-rY zC)8tA!{Swi;8uy_37T)9E=qW05j-0fo{V<_FA|%L7w2I)5sUjjpQZj;p{gxLxmL5 z)@&3(H;>}G|FWLDjD(u)E{t8%`qtFsv3v<}c;lwrKb5FShPy9@HD79i@|Wt=o7|c1>#4R232c-3 z-o22=kJNUVr#;cYo3(KRBjXzol@)^n?#lVfTT&fI+Lc|UuinHF*%j9BZB@>FpOUp(0T(AbkAZ&xnY(zTY9M{TX`=8c4vY+Fw zx6XHvJiN;4Fk#1mr=TzMibhLE=uuVPd3iuHL};hiimtE%ke%cpdyn(Ha_@JnQ{#Xf zXuegwR)S(W6Cgtf?(mO9RFua~TN$82aawDBQ<&~cpeL?l*`alwbc;Z`Zd!pj#Yxa> zn_sXKKVM^}BeeuHwwtI`# zBD3m3?Zvt%Fz_(k0%z8uf}>acf0573*h#$IEl{@$o*5A7(CNYih;}i8@yX6Jh8TxC zY%dt-lhsywbkAE!_C0*)Yy&QD#GJf)N`LQE!2%=aOmK{a=5AjbR1?>q{VH{*SyCw8 zu2ly@E8BezcoaD3$f}hLtDC@{2fc)3CC-t~xSY?loT{%+p6f<@r=w@1Z)A)<*tn${ zP%{Zn$a^ZhkP{>IB37rzIu!ix062;NI{>bd!^ni|Rwkb1vrTqzykr${*v90E8l@0% zkuTay@7R&6i~7vttYevppzSVoy`H6_)-~hUS7}CnTsdMa@RfD65A)lmE}_ef{OITC zxeJiD0;~C9X(5PL5TLvvE+I=VBc68b>;60;(Auf@4kzK*s^l4@JIQJ`4+Ae?C`b{X zK5@hd-q}V!AGP1fPkxS;@H{=ZM9DCQFRp)n~OdoY;!-pOY?A)$~P1Yn|i;erRVOus&Cd-!AsQE(p> zviS(DCh0+!f23{XdCn~yM@1PCxpRGIV!BHCPva*rU5Q(JK&^bJC{ukiBCdnhVECpw z)-=dSR_EOV=50Np@LeKQ3N|Z@_%INtAv=X7g$TXHAT4 zt*i1T+T1Rij%V8gx8+2%^8L@X$|@6dLKXA%d7#19%hb_ccVJWMCt>O>a6e4Is&=dG zC4Bq#(ve{M0ct0&7w16T+pey{&K(-C&vEC9vXJ0vb9PeM&l`?lr*@a)sya;=x9VyD zp<|Qudb!6#`yxG8CtFAfAK8LvnXI&V~bdwe8T9s#Eb0;vFOXRKnjOTy$w&a4qR}d&b5~>7#`8;zHN#5hqB(U8lruI zA^H?3AD;{2Xx*#xELxr|Q@@ppnW!=`XC{frk-3s44_a=T!X{-&z@GU-OK)_-_ZSDe z(#ySz?rhzcmCJLkhqCa{*x0z)ht9g^@{hDGXz(?%zxyttAuc+^H z19@EhQ+Eu1^*$+^PZvAbgY6GYWuq=N<^W;gKz~p~RyRvp=17 zLa2;`Eeo6Reb|{;9e8`GK;6ba-k*;q5d`||nezLj$*5C>UCTU^Le0<=sOf1si*29D zpZUIza$jeF{8UtQ9OuYzuifo|Ar-`$o{()*B!(C>V zy~@HX9vz|D?e?{MXSCU`2Qbcsk+So literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/icon.png b/Archie-Core/core/common/src/main/resources/assets/archie/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..cc7c01afdbaf4991edb19b7bb81f6cc3b9a3fee2 GIT binary patch literal 68643 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 literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png.mcmeta new file mode 100644 index 000000000..6d45dddfb --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 64, + "height": 64, + "border": 3 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..3a59250c0ef543bf15fec8452388a79cb2b50059 GIT binary patch literal 1223 zcmV;&1UUPNP)6F^SiMui>>Ruqt=R23YKNDcY5!3-+RY-o+zbYjDclY z>{&|5_8a5hzREeHl!Eg-?d&mz>B6hEX8XRszkeBk%H*7FArIs4l#&_9kCf6RT&;Cd za$Q$bY>Z(>G%u852Ijo~J;pF2sDac;tu@ny@9+DL)|$OTJ!y!-m${KD*n3AQWjfez z(t9^W`J8KV&bBuV2w>Vr6;RaiOd5|D?{z3za?Yr=+B?tZ(;P%JQF$|fBB1wfaN+%( zu(WV%&ET!Z0KSm0EQ^JehK3)iU~6qcqZ*Za`VeK9Q7+0sC(sD$5Ho-d#u0_q4Rg+u z_ttgAx~^t456x}cOi0~J1?QO>{~GTqGzBM`s5DWa@2VsCOmmx^tycDNu0qftIv4^9 zbrqWPgdIK0OV42ZYB+Em4Z=ApweP#lbB>|8Z5x(l!Fit3ek$Vy0TfUzMMdta0K>!& zh7LVWPZ1G3>wi^%M%lLQwhE|&)^#=X{!1UN0+z3IBaLJ4aWAmu3XcpQ=c<6lq9(=1 z<6#P@kjyi@_nDsWyk0My=Q$-r%92vD9O8{v)EZoPr<5wjMCHAqhSS^uz`V<_;Cdp6 zNJ%MS-*=>x?4EzMC*fJ!wpo~HxntZjM=>XOxi1JPrPzTv=Lu5WM7>{YMa~)Px?1_s z{kT6bzC#16*XRJ>M`)N4#+2dteA+V~jv7kGaa@`)Gk`)$6=qkZaY;`xFYo{=LIkMi zHDrLVkVq-n8X8kWL=C8ep-4peXF60LCQLuBCjb{AXmxs~lwwu|UDcauqRPt*Tv4FLU_w0UCv^vi1s; zV_jD~pHGWtDyTJ{YkXHqeSG(M9LH2qCIb-8cm25Uy)}7xsOrHwM?+9auDi~fC+DrKFihbX0Ui%I@#y4Dk zz3QPkNE@-qb*daUe7#;%tE-tbrG&@hG4zbEC<3 z-}i|R&~m9d@coMcer+eHFEc*=#|YikXb%6`O9XIFUrFx11yp!-j2c^ehkf5CMo1H_ z=7L)U1S2Hh(7_1xV1$AZ3PvaxpoJF1Ze2r#V=+5{V{~|BTg|Tp~1XxvQuq=D0){*AX^i3b8BgjD*oe z<2pm`!w97YslUQbG;ZUvLr65TI=koWhduk@ec$K9^Zq_OAD$aHCtF!5q!a)EvUVq} zTz2ly-$zn>XFm?Q!UX`aFgq(tHzH-x$1Z|r26tG{p%ttXHkJQSNV;xhu{i)?1140> zy$o@4{P`{!_(TpY<>BKeN99ewwh6xcXe1AOXeW_~1Kzs;PXg&{ElS*0__XKrx4nUr zU~;J7eOT*i;^r-qH{@MoIocIN`X;;)_Mt6N6abDV3BS;nAS70@7i5F$w&$5=HdeV= zpSYw<442nxOOOTeA}x^AjIV?zU73EAD8TeEpveaDHYoF=7tUT`i;P<0^48YYTw_8m z>rIKEW>hJWpQr_C@X423Ndj0|{y2qg;#duJBD6L5@x&sjV4e@*0IgU3=0~cngz8!V zBOxK-+^Fd!#oCGEMVW}bW8``uH%`TdoCzVJC~JE=nX(yRJi569Ooxj;VTN<=LnAcV z_90fjqP<@2>5bLR`VhYhxO*;ASjiF}Juwd0g=kjN8i+8;9|F-0m z+UACNzkD(3g-JEarVD||CM2(Vq0@_GG+7Akc(mA4L`OH`iW7NR8dy}ORv>}tTK}B@ zrf1!`C%Oo9&tYW_NE9?HjjXGIGja$U2TtR`GFY#7R%{Wb5gMsHg9`ZKklKfyx&c47 zQNn43r&!t0-CI~DprID$c$_{BHhxeL#i^RlM3knPvELw4FM7c;N&|@>OdOZESsg6} z&ZnqRal3&QGPWTkZ{RhzyvBswtFF!SJmzArcu~eZu^UWZ*)Y&Bc4Szs zQ(7#3%--_}>yjmJk^_HffGpCY^sFBX73`+Goio&wSdbtznVV533Eak|C}Sj<302QdWk1>0jj!^$kYEjl_+ z4rt9lDhFzoTI%gXnbFdM?v`m^=50{j3zJd7h(pNPQi%$9CwqW~$@vq>0Vz(p@A0ds zE-zz7TbvSJ;Xwja%){yWC8ykyjDTifJ;tzeUMjv-9*vY;j%4S#luLPTHp40@)dEZa z*>jkwjT^zKpzB*hMsr*`&HL4)-!4)Xzj`S!WWdn5u?Ws{^Dz3erpk{&$ff1wK|f@m z6vU}nArqH}!aUbkjW8d;`b)EGgiV7$jNbr`N5Gtgb4bv6sV&crF^=ig>{#8Cz1`Aw!9^TS5YX59z)#T{mbj@gAP tfKIUFZKeu{--*}meBJh=g>OB>XZ;F|#;aa!neH?&U}x=QRf#>9@)z5a#)SX? literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png.mcmeta new file mode 100644 index 000000000..6d45dddfb --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 64, + "height": 64, + "border": 3 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox.png new file mode 100644 index 0000000000000000000000000000000000000000..3aa7ee3bbe4511399249f2935b38e860069d4505 GIT binary patch literal 408 zcmV;J0cZY+P))l4>UGF8n(RmbBvk|i}^K9+tY)2aLRAyP!<3~}*rJaCcW>iUlTjyPsCC6D1JS;LXw-JC^)V4&$A&^v__fy8 zfMmytL-Tt;xDD*)b}9q8D0S=#4GqgBo0vCf<$X)4=bCbpp|yDKDCWK_DnJ<0m%5r zhyc*GEdW4O5kdeFfr$JY0E7@AA`lTUv#*yi48u2|Ert*P06AymoU3Od@`m#uBG2ct zNJOec17K$4oWab#X27b7aU7iqTL&WYVgrsbLRF!vn5N060N~VPj9_MO7U$!av`X5h z?L1dRsBBNNw?REnIbY17)Vu@~3mB(gIu>_if zIb{=-h|n~Rzc)?u254X@C9KzLZ}Rcj5D_ky3-2>Yj>jWJ1kdNw^?f`Z0007zSF6<(y-jM-&|j&sb62EXa5pq-J4U4{!iAkoKLPI{PQp1 Y3(fyZy5B`4cK`qY07*qoM6N<$g8y98zW@LL literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked_and_hovered.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked_and_hovered.png new file mode 100644 index 0000000000000000000000000000000000000000..eb01e5b864e163297c2d3173766f87e07b509b0a GIT binary patch literal 478 zcmV<40U`d0P)-Tt;x16J>V>!rr+^KJQ*eVyEGV1>iPrX8eAy)B=}YhFdDS-d*fYtbWM+mx zj0^zl^%?-6sz@n;h(JW~4FFO~5D|z7m^s$V7>3~=pf9GB003qNAKagbC>qX#h$5fM zA`z()9e|l(W?<%6b6{1)IF7-Dy#oa=wQ3u4x7y;ntXd~hzQr~75n`jW){c6b$dJ>0d_ndp{g;+VzGDw zIz=->+qUSs4!7G4hr=NX?)Q6uolYl+2wtyO==*#=0RUuXHrGK_W17D2(RE!sZ@1gZ z8Nl0?!!XnnhOo>b_8iO`mWO#9$1uZ(Q~Z6tgO|%Cz(fSoG`;P{9JTC8o~Wv^Nvev~ zYK6Y71J^Py|>pq;~^#5l)&-u<(^w+Q1M~^OLGau3r0?bHeCVprJ0Jqx>01y#SRfq^gq}~C5 zs>0pj?igc~zuWzOe+Q&fRRI8*2@>bMh*ZL}iAdo`KjiKkkO9UR$V`ke%E!PFfwfju z^1(17s$@Q&laNG2z};bHm2BW-W>$x~j>mBnOKLdh#PxdJG~G(>W%50*Y*sgS$69N{ zjx&JFdRA41nH7eIDa%&j7JUpv#5O)X8FeCtT6df=5bYa?Ms4Su9~1F)Y$!B^pL2c< zNOr79L~yxW{uz=@W|qhRYQB1R*>P2^8STya7#k+&x{3(g9mjFJS(w4y@p`>V0ygb~ zAsdjHFtcq#y1rfawN{k~^?}AiA#fF~+802NU#sKDVy+ zy%mvKJu=<~yRX=})NkLz%*5mI*sA;arpT{<0e|rjLx8Pe;lTg^002ovPDHLkV1gK7 Bw9Ehi literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png new file mode 100644 index 0000000000000000000000000000000000000000..eca5f3cf91876a83f7abd4799ebd03323f077ad1 GIT binary patch literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Et!3HGD8EPYel)tBoV@SoEWD!BZpXUu48af*r z8xynFiQLs~yLpm>o6(R_ZkN@AtL^$cJUkZ!Bs>gWH3YKvSS4IxT+Ek{n9vZyb8qte ji&v9o%kii%A68RMSgV%3}$u25n UZ{IoLKgd)DPgg&ebxsLQ00)aIl>h($ literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta new file mode 100644 index 000000000..ea42a1b16 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 18, + "height": 54, + "border": 2 + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png new file mode 100644 index 0000000000000000000000000000000000000000..98e37d595bde493d362f6c34553f9933d4a6b226 GIT binary patch literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Et!3HGD8EPYel$EE8V@SoEWD!BZpXUu48Zs4h z(-VN8(eTQZD=)G>u8rOFyX~z};H~fWHi@3K4;|7S1_XYc)f%5Rqn)nN2`MFy};O{xBpfGUIdt za(c`9=Ur7b5TEw&o=jQvjcjheGPA?G6(|6>dl$Mp=A7MJRWLJ*F}jAq-Qhh9tE$bv zeNxr7gYVjW)#vwz;heJz2|VZQ>GwLZj^0HC%qpH|@0|3@q{{zx^CdYe9i5ztQ@lpkE6yFPe`4jL5{m-lG4BOIU00000 LNkvXXu0mjf{KVDW literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked.png new file mode 100644 index 0000000000000000000000000000000000000000..59a40bda9e8fa429ddf32cec6ac0a1ef8864a289 GIT binary patch literal 468 zcmV;_0W1EAP)D`rNm zH9vB9+?}-+s)~rP*2-^s@3hv~_ZPeWEACFM^^`#c7-OVAV+^)!1K{y^d?~i? zd*07EDW$x680P7o6K{dueM7Ufo068_$^vCIl zVJM}XCneI2v5Sai#uy`Mj_JiA676$pY!S)X?7e5Bx_grM-kEbg&Hp4E*9z~w6dOw^ zPd`4$Lntb$s@VyxwO7G7=c2dFET5w%<9!q^vWRms-V6Wy3wQ%Nn`29Ocw7(w0000< KMNUMnLSTZ;rqjp( literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked_and_hovered.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked_and_hovered.png new file mode 100644 index 0000000000000000000000000000000000000000..ab9ab302eedd8e2149bd2c449dace6d9f5b7d5d0 GIT binary patch literal 463 zcmV;=0WkiFP)fGJ063ja0D!d?YOOFc z)LL^>L?9yI?od^T2;4pQj4{xA$Mt%J0)U7hBA}{p_XHOam>JeuFf*ts%q+pyT6xLF z(B1QJDP=oQYfXl#n%_AmYOR=aW)}cH8Ab%wTIjvQ-O+nbhGZck$->N%fQX>A1~Ypw zBr7uG?F9Vp8?}pw1ln5W?PiA78X^Ms`~9Vxm&*kafm-X{yhB zl^Rm~cS}{FswvmTkZl5Z8<^UywNOgg0VzHWIp*rO5|IRoh#ioW$!v`AC!o7yjPcY_ zQaQTyjwvFN(PH3%!=%s{08iQasR-ciIbjJV0FYC|&>yFR!%#}uCxz{D z?0BP@Va}Osj_vUgVSH#xP6}spjDb>0J_uw!#=u(ZA^taDek<^qB_1rLJoNZtP9Qp} zs+ok|`!gWlIcCeu@}HUHI*nr&d?&ds5fQiB?N3+Vtq&|0T|=7ks(1hZ002ovPDHLk FV1g+v%n1Mh literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_disabled.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..133e677cf0d39885b131e9acf8aaccf6b34b36c0 GIT binary patch literal 424 zcmV;Z0ayNsP)8{Lhnf~rD9P*tcZ zhzL{_-JiOAGX?<644H}N^9g0$IS3=#&22)Y}p zip&J#_-$qok=e(hPWYs}6LC&7?mjdbvjI#(GaLBSi^*7!gW3@_*G87D-%iG=RfX<` zna!Mq31rKn8*)S(8!~e=%1o&0ulQH-ooo``8xs-JfUQ+#qPyYl2W593w7#_?MRt&A zxtY>rCft42D90q^M*buXcOPr8*f#l&C5u5w;n@_<~{`iqp@2Cgi?iiC3<=u1J5&JLkr}5G z@Za3%T~##@pZ4&cOj-1eY;M0Yv%|X;C;+&77rHy7`_d2nT-bDn=>;FK@#k_DYwM6)>Fg*43FUSjqCG-A|ibo_TDhF4q?vOK}E!q`VGu;aF^I< zX6*-=6Nqc0s@Qu20M=Un1JZaV$9qbN9CZKjQUz}m-^*`*0{#Hb@Ui9Poj3RZ0000< KMNUMnLSTaKaMY~; literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png new file mode 100644 index 0000000000000000000000000000000000000000..782246740e548fd2896ca82a75ece382e4092664 GIT binary patch literal 1158 zcmV;11bO?3P)s}(9y>C;>*wzFq6 z$B^wCau*IwuMY(cT7fqR1OnJO7$DnPqtH$p+Bz$aklh^FwclHYO3Daym6KF`s8Wdi z10Xo(ik8U|uv-KHeUOq{)~ia0R%1y`*lS^7;Gi@Q*1##8yNWg2FL-%e+XZ;A8SdxxujA=`bvQ}#e#gg(%#Rlc4^yVLV&xI%m6{|>*9z7Zx$-=4d7oBe;c*?$cF Y1&$A31Mhs9r~m)}07*qoM6N<$g5O~jE&u=k literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png.mcmeta new file mode 100644 index 000000000..6e42b4335 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 200, + "height": 20, + "border": 1 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png new file mode 100644 index 0000000000000000000000000000000000000000..844cc35f74d6e78152d5dddba96d19b9137623b0 GIT binary patch literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^96&6>!3-qZkM=|YDenNE5LY0*YSpUz{QR7poQ#Z& zprD|1k90*~|7%PV!3-qZkM=|YDenNE5ZC|z|8Ll^Vg35`>(;GX zvt~_ob#+x$Rb^#md3kwRSy^dmX-P>*adB}`QBh%GVSavoUS3{&e0*G7Tx@J?OiWC4 zbaZ57WO#UZNJz+1c9XL}V`DvC978H@xgK=vb28*%HF&;tzT=($Ym^nenj#W^pS*tf zS>D?H|9#gkc0H@DdHaFOlB}%GZL2slJ1YI-qeFauaRk3l=f3h*ZA*6F**#K;u~(J7 k{BCWNf7j8yjo}A__cF!QfUu}dK#LeWUHx3vIVCg!0Qn4H<^TWy literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png.mcmeta new file mode 100644 index 000000000..ac6b6f93d --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 8, + "height": 20, + "border": { + "left": 2, + "top": 2, + "right": 2, + "bottom": 3 + } + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png new file mode 100644 index 0000000000000000000000000000000000000000..3a41aca2a1855be0f1a61167ce6047425def6f13 GIT binary patch literal 1165 zcmV;81akX{P)Dfo;g$M1Ba(}@N>2fi! zn}f5LA^2}03{w7%CoQFF@067p2^s6uW-{KMd{VVH=cgs!jTE3=KNKMbf?ByhP{gi2 zCgDLyG`g6OlVmy{=kAx*DIVfr2L-0sPS1z&3TB6XD;Z9*k zW>qPF*{!vvkP7N>TVG-bOKUBBnC*<6{gjYvE7TZ62OugQGKX1@`91?(^JOG*pCkmUAe3 zlP1XhffPCW#Foh*of4H2UnuaF_3lQo)mX3z`&e2Uc{%dOrEywlH*@ZAf6}cb<>}Z$ zp=2PmRTI969j_A7xywSiD@!GFp=pVM8AzJbLXo;+?I>+YXf4S_N|c*}S62D))xpxa zG-J~GxRkz!b*JB~@9Q@i^i;T|R{7n~ZTo%OA*ra}cO#KK)TFdc=5zeUbLz-i3}Q)$ z>ivMX!JpoTO7cTKJp62<(cR_SY)nahZu)2c_1f0*?YgJET{;gH-UaH1`pz&M8}Zd0 zC!SK=egE#+ILP2_+=a;L(8`sybL~Rm94f6go>FE!=|j49iJ{FdM-%10xfX_J4!G#ma^ZSOK`CUHVnAw$dz+p{ritu+N_B+?nt z9a( zO|xqLAMSO=nm)c${u(qt)M}(?gBBB_?D412#ob52Js+W94u~XlLP6ZxYty2mAk% f{eN(>{~`DfKBN+D-E%m;00000NkvXXu0mjfIt)Z~ literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png.mcmeta new file mode 100644 index 000000000..6e42b4335 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 200, + "height": 20, + "border": 1 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slot.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slot.png new file mode 100644 index 0000000000000000000000000000000000000000..406db70fe396a8c409bed04ab731fd4c25119fc7 GIT binary patch literal 507 zcmV4Tx0C=2zkv&MmKp2MKrbfqw6tAnc`2>yULJ2)x2NQvJig%&X$+}*=_-}`d+9UwF+OtZS;fTr7K zI++l&xm7XriZBKcq;*tgmN6$uDfo`Bdj$A?7vov}b$^aNHE%H>AQH!!VcNtS#50?= z!FiuJ!b-AAd`>)J(glehxvqHp#<}RSz%wIeCOuCaAr^}rtaLCdnHuplaa7fG$``U8 ztDLtuYn2*n-IKpCoYz;DxlVHgNi1Rs5=1Ddp^OS_#Aw$^v5=Klt5St1va`C500}_lx6vi~*rtpjmgE?_`CI2m^;tnJohCA7hL|N_Lh+L?G#1lycmiIp^we`&`fSEV4u1 zE-B-Ara@nwawaoF(qT|3<*jCCl*F9#uzn{@BT6PG(v-bmck^o|Go#juBqUL5{aJJ@ pHzMf0ucY4lT0s&4&{~Us@d7C{tq82R{}=!O002ovPDHLkV1j3BW5WOd literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox_clicked.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox_clicked.png new file mode 100644 index 0000000000000000000000000000000000000000..03ec5a166659fa835073f61e962f60bed1717d0c GIT binary patch literal 320 zcmV-G0l)rD91h(r&-%vhF%*XuNg_e6FOFQg}Qbs4DJ`s{S3V za&pd$;}|T@=M#XoZ7VPV{BSr#<955T-EO1qx(Ese#9qa8xm?(6HgsJVEaNx^U}c1=qN<1p!!Q7lbLMzFRz|Fr zSI<>dcDo%ZB}4=>W1eT)wk79GDTUAHQ$N?;nWibU&+|+vg_KgL0if@D|24jzQo-Kk So=Bzu0000c8lw+*ZtlX@;M+|_#f@`o2n@f(>Oe<{mr&Mcn} Pw3xxu)z4*}Q$iB}D6Kzx literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png.mcmeta new file mode 100644 index 000000000..85fa1f8a0 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 16, + "height": 16, + "border": 5 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..aef13cda62f5abbdfbf26a250e3b00c1ea5b7fff GIT binary patch literal 173 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`C7v#hAr*7pPBs)etiZz}er&GX zV)GA0c8km}tPHbazkQ=IM01nunY-siCsYWZndsK8vD9a4_F8t&I8EjiS|!=rix+JD zreR?0!cxw!@O0{@jz5;4#Q#h?a7M5EcW~I%#0Pxk>>huQa|lLiedjJa`J|!veRbR) Weq}XlLHk~y{S2P2elF{r5}E*;5J0K` literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png.mcmeta new file mode 100644 index 000000000..85fa1f8a0 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 16, + "height": 16, + "border": 5 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png new file mode 100644 index 0000000000000000000000000000000000000000..eddefa2ed76b1631a0acf5b73f79232a9e5f4218 GIT binary patch literal 366 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf66p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>dx@v7EBh-JCLs&uz8h{p zg$$}Ct`Q|Ei6yC4$wjF^iowXh$V}J3MAyJ5#L(Qz*u=`zT-(6F%D~|5bXFe}4Y~O# znQ4`{HOx7+_XALa2Hb{{%-q!ClEmBs6g?JJre;>grVvX4<-Jb<^#pmkIEHAPzdCUz zZ-WC5OS7i}_dyk7G3T*LJv?-e?{oY@Z>@!eLx xU{DnDOKrx7_YT#$@6H|ck93W`^kb4b-;xM5lXJoEih!mwc)I$ztaD0e0ssfja}odm literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png.mcmeta new file mode 100644 index 000000000..85fa1f8a0 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 16, + "height": 16, + "border": 5 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png new file mode 100644 index 0000000000000000000000000000000000000000..f9b2ba1bfeb4a1a472bdbe94bce08bd773173d2c GIT binary patch literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf66p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>dx@v7EBh-JW+okd-YCh> zKq1u<*NBpo#FA92xq2lo{*R;|A@8H>4sy<@+b54i*E7+6$$@S2in2l>FVdQ&MBb@ E0K4OFeEkwg6}#gQu&X%Q~loCIB21CpG{8 literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb_disabled.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..96fb12a78393693d6afac81cf6fedaff0b19c688 GIT binary patch literal 127 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xaH%}MGkcv6UJzZVD&O5LrBqk&z zoH%t#$@B7-%*zT=k`fXU_x4mePg!PN{>}glCZ>oANnEwCF*y{>r{gNXEpgKK2?NI+ We)WeguJ?c@GI+ZBxvXs4C7m0D$Lt zkeP^x-irvB8ScHP`ifz#1pv7BLPStiFf&vY?!7NNA_6l508|ws0-v9sPykTXHg?W| zhyVaG6RO(ts(O8!S?{m4;O?&|MFj4Sa}F}|1)9NfC_nDK004K#S_^BfW`JzacxFOG zpsFu$5zzqYm}hA-Gn+}W+dwA8_ud9|?`>8hqQR+ZM=v6sCel9BJi*XzRl(ii?udvV zAot!j003sz5i`Gxo(>}dd+!$;1kCw4r@^0d;O^~XGWAMLMD%7=wYWG4l!gd&YKRfj z4x{HUGvV%?UZxWGuA1KhBLg$j&vu;h6^Gm)7;Yc%fey$GN+F;&hpBGM^*o~Qc(0a6>h%d}=@YmPqt z(@0fy1av}1m{-nc=@JB~ex3*J-utW!I;9QXd!>eD%P5GPosgm>nAstx7P!teR*Q(> zobv+j?$~=b+ud2|Xm&>eoa;0OpgX3SH6u({H>z(2sS)-Q0;C+2l|g~dwoIe*!eoaG z?!9r&fthu$pd(hsWSPxu4y9362`T2m*+ctsv=iI}#OUV($Y5E4Gu;3n_#=Oe Z{sGxxb-v1k;r9Rl002ovPDHLkV1hIn8`l5; literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked.png new file mode 100644 index 0000000000000000000000000000000000000000..59118c6bdfee3e4176b02604d03c3566759e7e18 GIT binary patch literal 965 zcmV;$13LVPP)H^!Yx+D*^C0tuzScJNbnXq3ksUW4R|Qbo5S#lC)A)&7cU7WBTs0IOq$T6}o!`Eq$UiLlcEK=eeK(rDAkpe*j}Un%nXDzW zT!MCk&6|cvngeY-g$PWtVa9{9I2B}?T;P2`X72+nc8o1-<0wYS=%N-rznL{H-Z0sq z2+$H$utxEDqXvcmnz+d&2+Y%kYq)E|#l)fjZV@%KL2d|$NUVV_ilQCp{f)@Fd2Wyn z9O8^vU=g~@MsYDFmfyF`h*=24P1UZEOH(l#oCl4;TTHChgO&7?R7q-7;6K_7O9*1`H#9h%JL z&HYUXrjD?JD3LT~SS`$OwCo13bZS*CwuMFZ&+&HBGF!E2JQ_AJz~XV(tZk7^6)%!gYX4?tXVYn%oy9WEJxU z#?%YaPlA7DwZTQ4!L!6;(!(n;dB|L{4HD{0RzMuC?l(#Y#7kTVzEg2AwS(78fjfx! zC?|y_QibaM5mL2u2Q!d}y~83Rfmyuz-W$N>-9kC&3bVLC1DnZMD8+d%)B&1VAr{}H n&Ue2$4R?R74!=GHe?a{URH}P#{?aaR00000NkvXXu0mjfUka)@ literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked_and_hovered.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked_and_hovered.png new file mode 100644 index 0000000000000000000000000000000000000000..c0b6c2659a00d7f42b0db7e62d7ada6f2e823498 GIT binary patch literal 964 zcmV;#13UbQP)71j6fai zV?zdyLu8n*xSB&^ zo7_c}m?bA+783fJ(8UScfoc#yh#VseGklc>>1E%8&LSnw0Yob?5Lxi`3skbP42NC_ z4@dy$2AdGi(n@s9oIop1EfEJ6^nzEk0&2+9WDJ=Q4^)dCQ42>RR^Y+?{s@tWlF3>^ z%Oz+x*t}_&q&d*WQ;5JM8)iHxi&H_S$pzjAWcEJLV#nCRHjZMHj4o>7^P5@I;ti7x ziU2K91#1+aH)>!ApoyDYg1|goxQ4qXTudwq;1*Fs8{~$7h{PJ`qA1#d-rtC)JxNUCIaG==)EFVIK2sZz`XjbMSWJjZkbO}tHv=V*~ zJya&VLL{MsPa%uY;dw5^5+tf-87P=66ixaNWg-@yAu%p^&q4j*ToeyBRGOHVV4(Hm z5oa?7FVtz)rZL$E=S3^L!P?|Nhe-l%p=>Y`a9f(lVuw8`B4{6rC9vV>QJE`%8usPMp- zn*Yxci(W$wzDtz2YaS(O;}xpGByFQID47_jj}UaBbq6_OkDRU){u-yFv;=`(X{SA?98nh%wrPBU}f#MOY6q{G0(TJc zQBDd=qzcvhBcy8S4rU+`dxu3t0<(Da{WpNiyM=Pl6=rdP1~!wiP>S!LM*;X moo|128t(oY9nhb082t;bcY$ee-=(Mk0000psu)JQc!hbLf0N(F6stT$KRmI*L z0C3+oG7}Nedl3OM!#M|4Uol+Q1pqkbKtxbgFf&vY&N&Y|A_6l508|ws0-v9sPykTX zHn#VMhyVaG6RO(ts(SpJS?^!h1$Tc$DI#!p?7fkh56}#jL-}*g0RXr=uIs{eUCjX5 zpz+Lvh(J{z;3A>{(lO7{W@a{%WVeA#iqAO>>YUT8L_~vA)s9|7I!&Z~rg?&)-Kv7S z!`%@PKS0hoZ2$nwtRrTA89g0F1lC#)HVByWy?2A(d&Aw^$7Je}nuzGls%mj@5GV~1 z=+qD+rX5DlFEioponEFA_^z7Y0wV)6?~`iIr@p8G4l*}w@hs1)YHR5MgypBIop)B$ zRJob8W|$HY!CDKzSJ`u)Ot9p6KQob;KWjAZ?!5@0H8EArGa}L{yzjgF0Rd7Qyvwv^ zW^0Z<{?bTQbp&)mMwnO5XXz3IslM+U?%w;X3_7I^-g~5mWy>gtoSl%OC79VErxv)* zHCBs=VDJ3^@9tP@HQU`;>1cLG0-WnK2B15pnKdI!S2wC}2B{JD69S|hl$Ak&&bCaW z^TK3@46e1X_lB8uub?AV#$=hzY!0PSRtYKQ!P#FJJ=ZB(zB98AO@`=Wig>Nn&q)KB zG)@o%H#;i-&8$m$I%K8L!7Ovyq(C`uAitXq0IanhFYE6Yn6wkz1jOj)1IS=mfivA* fH6I@z_}AkP0`_+!$2k2u00000NkvXXu0mjfI|&-5 literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_hovered.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_hovered.png new file mode 100644 index 0000000000000000000000000000000000000000..e0410aae6be380722840e7154d67cd5b1ab55b03 GIT binary patch literal 637 zcmV-@0)qXCP)Od!dj~tAR9EEnGg}E z>I+;%G(bA$S=!9ZW|HhSkV)~qw?W-|o0W)YaH`tTi%6%5w9hn8Ftl4$aCf*nBH{E9bLx34&BV&jWYweO3mY(gyFnQp2)k6hzKWNYN6^?2uCnT<03AMMQAU zd4YF#?7f@q?yPh)yCVV4bs7WE9n;L35vHpf)i;CG2>S^EQVz<>pg?C^rqOv}vO@;< z-Z-mjPS_X|wg32p*n^z#8^u<DZUB(K^2g{4 XGd+8_Q~lvh00000NkvXXu0mjfTN*8K literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png new file mode 100644 index 0000000000000000000000000000000000000000..da18e3a9739434e449853f09a30881d064e97082 GIT binary patch literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsVGku$B>FSZ!hfRWiaG$xyb+4 zS^eD+-;F2brU|Q;ZJO2T#`IC;LH@>t*H8ZTU%BdU-^{&j^PL!!nBMu{8BHA@$-Tq|$-1!xO{r>mdKI;Vst00r_fxBvhE literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png.mcmeta new file mode 100644 index 000000000..49e89779d --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 26, + "height": 32, + "border": { + "left": 7, + "top": 8, + "right": 7, + "bottom": 8 + } + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png new file mode 100644 index 0000000000000000000000000000000000000000..c7f9552aa298a3c2b389ba7c1a6f2ccbf667fe78 GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsVq+y$B>FSZ?7409WdZ%eR%(l zd{O@sxn~h&nz<(~xwapwp1k10!pX;GE7+Hb2~K=J<7V;}EtUg!AMTqn_d=95@2kDc z2G5tg?e%%$^IJ|(#bbhkV-pLf&=>XL literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png.mcmeta new file mode 100644 index 000000000..49e89779d --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 26, + "height": 32, + "border": { + "left": 7, + "top": 8, + "right": 7, + "bottom": 8 + } + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png new file mode 100644 index 0000000000000000000000000000000000000000..9c5c6eaae327db015a9a316d3e80b089a09f9c39 GIT binary patch literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsZ387$B>FSZ?7409WdZ%eRyBS zPTjLFWnJ?0OKm!_B^rCoIl1JmGWT$PN=_FP{Mk3t)@`*D!yVao-ZwedZoRbNo;5?U z->rKmT+VHIFX-6B!YQQUF+stxzG2n9hqi)&`!7l`GZcUSHMM~|sY+#zp}zMoprs6+ Lu6{1-oD!M<)>}BR literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png.mcmeta new file mode 100644 index 000000000..49e89779d --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 26, + "height": 32, + "border": { + "left": 7, + "top": 8, + "right": 7, + "bottom": 8 + } + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..0c23cf347a3794813e8d2db15186e12e6f6c5f7e GIT binary patch literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsWeX)$B>FSZ!c}+J)pqja*_X3 z!W;J41};I>n#+}xR`R-*s7Dq(P51UYaI)=*EXM)Euc-(BWScx*^yr<-^*uFmC%Udg z$?GuMIj`MnaN-QZi@p2jD+>KgyM3KgNX278+R03^e@O1Ta JS?83{1OUu8IX?gZ literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png.mcmeta new file mode 100644 index 000000000..49e89779d --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 26, + "height": 32, + "border": { + "left": 7, + "top": 8, + "right": 7, + "bottom": 8 + } + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png new file mode 100644 index 0000000000000000000000000000000000000000..b136a0bb2b1c5f08506b468b373e0183b44fede7 GIT binary patch literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsc26Z$B>FSZ!hfRWiaG$xyb+b zctE3fVQkK(_NPZW=dMXxG||C^^G9^fi_%a3f>&kjPflC^aJfK3N5k)6Gv}E*d0*{i xz7Tcz_(8vvzV+cOoI)xd6BHbqSU7(bG4j3=-+lbtF)N@o44$rjF6*2Ung9*EHpBn` literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png.mcmeta new file mode 100644 index 000000000..49e89779d --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 26, + "height": 32, + "border": { + "left": 7, + "top": 8, + "right": 7, + "bottom": 8 + } + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..c7f9552aa298a3c2b389ba7c1a6f2ccbf667fe78 GIT binary patch literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsVq+y$B>FSZ?7409WdZ%eR%(l zd{O@sxn~h&nz<(~xwapwp1k10!pX;GE7+Hb2~K=J<7V;}EtUg!AMTqn_d=95@2kDc z2G5tg?e%%$^IJ|(#bbhkV-pLf&=>XL literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png.mcmeta new file mode 100644 index 000000000..49e89779d --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 26, + "height": 32, + "border": { + "left": 7, + "top": 8, + "right": 7, + "bottom": 8 + } + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png new file mode 100644 index 0000000000000000000000000000000000000000..9c5c6eaae327db015a9a316d3e80b089a09f9c39 GIT binary patch literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsZ387$B>FSZ?7409WdZ%eRyBS zPTjLFWnJ?0OKm!_B^rCoIl1JmGWT$PN=_FP{Mk3t)@`*D!yVao-ZwedZoRbNo;5?U z->rKmT+VHIFX-6B!YQQUF+stxzG2n9hqi)&`!7l`GZcUSHMM~|sY+#zp}zMoprs6+ Lu6{1-oD!M<)>}BR literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png.mcmeta new file mode 100644 index 000000000..49e89779d --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png.mcmeta @@ -0,0 +1,15 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 26, + "height": 32, + "border": { + "left": 7, + "top": 8, + "right": 7, + "bottom": 8 + } + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png new file mode 100644 index 0000000000000000000000000000000000000000..e171115189a94067f99885bb4badfd3593e28971 GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^O+YNc!3HGF{;A#oQk9-Ajv*Cu-rm|Mc!+_A<)HB6 zzB@dv0d3q1xxK2@xn?YEP~UNON^QYHhe;|guQSa5#_?5_!87iy;;Wf6U;nVWch>Ng z$0wGGE$uJtC7KwVFBq{fwk=5IVB&E+Ex;ntG)l>JD?{QtY`}>Q)*_Erl|N3m9emujiJfGq8 z7lFU*&H@P>TtpIGyYjB>iL6=n`~S~*oA*C^mT`gc17l^o-~-9*3lcylF?hQAxvX literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png.mcmeta new file mode 100644 index 000000000..12147d0c5 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 130, + "height": 24, + "border": 2 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png new file mode 100644 index 0000000000000000000000000000000000000000..e171115189a94067f99885bb4badfd3593e28971 GIT binary patch literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^O+YNc!3HGF{;A#oQk9-Ajv*Cu-rm|Mc!+_A<)HB6 zzB@dv0d3q1xxK2@xn?YEP~UNON^QYHhe;|guQSa5#_?5_!87iy;;Wf6U;nVWch>Ng z$0wGGE$uJtC7KwVFBq{fwk=5IVB&E+Ex;ntG)F=6MhPF?BqG;r671f;o{S|=5E#76+9<>$!*wd>r~6`FsZt-y({fmVU5kavxcuc zidc7SX@6lZ(Zt|MEV|AD2^>*v3JLE8AN-YF`QOAd i@2%jg&mq1Hdl+P*_l>JD?{QtY`}>Q)*_Erl|N3m9emujiJfGq8 z7lFU*&H@P>TtpIGyYjB>iL6=n`~S~*oA*C^mT`gc17l^o-~-9*3lcylF?hQAxvX literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png.mcmeta new file mode 100644 index 000000000..12147d0c5 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 130, + "height": 24, + "border": 2 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png new file mode 100644 index 0000000000000000000000000000000000000000..86ec7d4209eb03cb8074b4060b05e20a424b0ebe GIT binary patch literal 111 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`W}YsNAr*6yV>ToltYN3kd1|bL0N*6spo=pB+vu~Pgg&ebxsLQ E0Af8F*Z=?k literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png.mcmeta new file mode 100644 index 000000000..85fa1f8a0 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 16, + "height": 16, + "border": 5 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png new file mode 100644 index 0000000000000000000000000000000000000000..486af70956ae482d5368fc46fb9a0821634d1d04 GIT binary patch literal 104 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%``kpS1Ar*6y|NQ^|zn;yAQ5guh zr#Nm(o3YpD$t^zS#cVxN20lCqi3tuVJ&cSD+jzL-ySF&b0BU9MboFyt=akR{090ok A3IG5A literal 0 HcmV?d00001 diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png.mcmeta b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png.mcmeta new file mode 100644 index 000000000..85fa1f8a0 --- /dev/null +++ b/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png.mcmeta @@ -0,0 +1,10 @@ +{ + "gui": { + "scaling": { + "type": "nine_slice", + "width": 16, + "height": 16, + "border": 5 + } + } +} diff --git a/Archie-Core/core/common/src/main/resources/data/archie/structure/gametest/empty.nbt b/Archie-Core/core/common/src/main/resources/data/archie/structure/gametest/empty.nbt new file mode 100644 index 0000000000000000000000000000000000000000..c5562f150067080e54d49b0d5a62698fe269dd36 GIT binary patch literal 123 zcmb2|=3oGW|F)+$@-;aKv^-R7Kb6??J~`JnbN?j6n+)Yo%CzUn1r|=)KdVMpAaJ%G z55u-qdpp`rUY**OD|r0*jNb=uy_bHLWBp72L4w}7`4(r5F3pv{wBTKelHO#sZ?185 b!n*lI`?^mzA2m^bkQKq`>znU54QM9-1z myTargets, Set otherTargets) + { + + } + + @Override + public List getMixins() + { + return null; + } + + @Override + public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) + { + + } + + @Override + public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) + { + + } +} diff --git a/Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java b/Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java new file mode 100644 index 000000000..ba85c6110 --- /dev/null +++ b/Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java @@ -0,0 +1,44 @@ +package net.kernelpanicsoft.archie.mixin.fabric.threading; + +import net.kernelpanicsoft.archie.gametest.ThreadingImpl; +import net.minecraft.client.Minecraft; +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(Minecraft.class) +public class MinecraftClientMixin { + @Inject(method = "run", at = @At("HEAD")) + private void archie$onRunStart(CallbackInfo ci) { + ThreadingImpl.onClientRunStart(); + } + + @Inject(method = "run", at = @At("RETURN")) + private void archie$onRunStop(CallbackInfo ci) { + ThreadingImpl.onClientRunStop(); + } + + @Inject(method = "runTick(Z)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;runAllTasks()V")) + private void archie$preRunTasks(CallbackInfo ci) { + ThreadingImpl.preRunTasks(); + } + + @Inject(method = "runTick(Z)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;runAllTasks()V", shift = At.Shift.AFTER)) + private void archie$postRunTasks(CallbackInfo ci) { + ThreadingImpl.postRunTasks(); + } + + @Inject(method = "delayCrashRaw", at = @At("HEAD")) + private void archie$onDelayCrashRaw(CallbackInfo ci) { + ThreadingImpl.setGameCrashed(); + } + + @Inject(method = "emergencySaveAndCrash", at = @At("HEAD")) + private void archie$onEmergencySaveAndCrash(CallbackInfo ci) { + ThreadingImpl.setGameCrashed(); + } +} + + + diff --git a/Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java b/Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java new file mode 100644 index 000000000..b1c4ed829 --- /dev/null +++ b/Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java @@ -0,0 +1,33 @@ +package net.kernelpanicsoft.archie.mixin.fabric.threading; + +import net.kernelpanicsoft.archie.gametest.ThreadingImpl; +import net.kernelpanicsoft.archie.gametest.ADedicatedServerPlatformInternal; +import net.minecraft.server.MinecraftServer; +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; + +/** + * Injects ThreadingImpl.onServerTick() into MinecraftServer tick cycle for GameTest coordination. + * Allows ThreadingImpl to coordinate server-side task execution with client-side gametest thread. + */ +@Mixin(MinecraftServer.class) +public class ServerMixin { + @Inject(method = "runServer", at = @At("HEAD")) + private void archie$onRunServerStart(CallbackInfo ci) { + ADedicatedServerPlatformInternal.captureRunningServer((MinecraftServer) (Object) this); + ThreadingImpl.onServerRunStart(); + } + + @Inject(method = "runServer", at = @At("RETURN")) + private void archie$onRunServerStop(CallbackInfo ci) { + ThreadingImpl.onServerRunStop(); + } + + @Inject(method = "tickServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;tickChildren(Ljava/util/function/BooleanSupplier;)V", shift = At.Shift.BEFORE)) + private void archie$onServerTick(CallbackInfo ci) { + ThreadingImpl.onServerTick(); + } +} + diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt new file mode 100644 index 000000000..eb8de4a8a --- /dev/null +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt @@ -0,0 +1,11 @@ +package net.kernelpanicsoft.archie + +import dev.architectury.platform.Mod +import dev.architectury.platform.Platform + +/** Fabric implementation of [APlatform]. */ +actual object APlatform +{ + actual val platform: String = "fabric" + +} \ No newline at end of file diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt new file mode 100644 index 000000000..640a5648d --- /dev/null +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt @@ -0,0 +1,25 @@ +package net.kernelpanicsoft.archie + +import net.fabricmc.api.ClientModInitializer +import net.fabricmc.api.ModInitializer +import net.kernelpanicsoft.archie.gametest.ThreadingImpl + +/** + * Fabric entrypoint for the mod (`fabric.mod.json` `main`/`client` entrypoints). + * + * Delegates all real initialization to [Archie]; this object only wires that shared logic into + * Fabric's initializer callbacks and registers the Fabric-specific client tick pump used by + * [ThreadingImpl]. + */ +object ArchieFabric : ModInitializer, ClientModInitializer { + override fun onInitialize() + { + Archie.init() + Archie.initCommon() + } + + override fun onInitializeClient() + { + Archie.initClient() + } +} diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.kt new file mode 100644 index 000000000..771eb2f1f --- /dev/null +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.kt @@ -0,0 +1,10 @@ +package net.kernelpanicsoft.archie.data + +/** Fabric implementation of [ADataGeneratorPlatform]. */ +@Suppress("unused") +actual object ADataGeneratorPlatform +{ + /** True when launched via `fabric:runDatagen`, which sets the `archie.datagen` system property. */ + actual val isDataGen: Boolean + get() = System.getProperty("archie.datagen").toBoolean() +} diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt new file mode 100644 index 000000000..94a42dcdf --- /dev/null +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt @@ -0,0 +1,93 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.mojang.serialization.Codec +import com.mojang.serialization.MapCodec +import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition +import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditionType +import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditions +import net.minecraft.core.Holder +import net.minecraft.core.HolderLookup +import net.minecraft.core.Registry +import net.minecraft.core.RegistryAccess +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.resources.ResourceKey +import net.minecraft.resources.ResourceLocation + +/** + * Fabric implementation of [AConditionsPlatform], adapting Archie's platform-neutral [IACondition] + * onto Fabric's `ResourceCondition` API (`fabric-resource-conditions-api-v1`). + * + * Every [IACondition] is wrapped as a [FabricCondition] to cross into Fabric's condition system, + * and unwrapped again via [ResourceCondition.archie] when Archie code needs the original back. + */ +actual object AConditionsPlatform +{ + /** Fabric condition types registered via [register], keyed by their [IACondition.identifier]. */ + private val registry: MutableMap> = mutableMapOf() + + /** Registers [identifier] as a Fabric [ResourceConditionType], backed by [codec] via the [FabricCondition] wrapper. */ + actual fun register(identifier: ResourceLocation, codec: MapCodec) + { + @Suppress("UNCHECKED_CAST") + registry[identifier] = ResourceConditionType.create(identifier, (codec as MapCodec).xmap({ + it.fabric + }, { + it.condition + })) + ResourceConditions.register(registry[identifier]) + } + + /** Codec for [IACondition] backed by [ResourceCondition.CODEC], round-tripping through [fabric]/[archie]. */ + actual fun codec(): Codec + { + return ResourceCondition.CODEC.xmap( + { resourceCondition -> + resourceCondition.archie + }, { iCondition -> + iCondition.fabric + } + ) + } + + /** Wraps this condition as a Fabric [ResourceCondition]. */ + val IACondition.fabric + get() = FabricCondition(this) + + /** Unwraps a Fabric [ResourceCondition] back to its originating [IACondition]. Throws if it wasn't created via [fabric]. */ + val ResourceCondition.archie + get() = ((this as? FabricCondition) ?: throw AssertionError()).condition + + /** Adapts an [IACondition] to Fabric's [ResourceCondition] interface, delegating [getType] and [test] to it. */ + class FabricCondition( + val condition: IACondition + ) : ResourceCondition + { + override fun getType(): ResourceConditionType<*> + { + return registry[condition.identifier]!! + } + + override fun test(registryLookup: HolderLookup.Provider?): Boolean + { + return registryLookup?.let { + condition.test(ConditionContext(it)) + } ?: false + } + } + + /** [IACondition.IContext] backed directly by a Fabric registry lookup, used when Fabric evaluates a condition. */ + class ConditionContext(private val registryLookup: HolderLookup.Provider) : + IACondition.IContext + { + override fun getAllTags(registry: ResourceKey>): Map>> + { + return registryLookup.lookupOrThrow(registry).listTags().toList() + .associateBy({ it.key().location }, { it.toList() }) + } + + override fun getRegistry(registry: ResourceKey>): Registry + { + return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY).registryOrThrow(registry) + } + } +} diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.kt new file mode 100644 index 000000000..927dc88a9 --- /dev/null +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.kt @@ -0,0 +1,48 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +import net.kernelpanicsoft.archie.data.common.crafting.ingredients.ACustomIngredientSerializerPlatform.fabric +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.crafting.Ingredient + +/** Fabric implementation of [ACustomIngredientPlatform], adapting [IACustomIngredient] onto Fabric's `CustomIngredient` API. */ +actual object ACustomIngredientPlatform +{ + /** Wraps [custom] as a Fabric [CustomIngredient] and converts it to a vanilla [Ingredient]. */ + actual fun vanillaOf(custom: IACustomIngredient): Ingredient + { + return custom.fabric.toVanilla() + } + + /** Wraps this ingredient as a Fabric [CustomIngredient]. */ + val T.fabric: CustomIngredient + get() = FabricCustomIngredient(this) + + /** Adapts an [IACustomIngredient] to Fabric's [CustomIngredient] interface, delegating all matching logic to it. */ + class FabricCustomIngredient + ( + override val custom: T + ) : CustomIngredient, IACustomIngredientHolder + { + override fun test(stack: ItemStack): Boolean + { + return custom.test(stack) + } + + override fun getMatchingStacks(): MutableList + { + return custom.matchingStacks + } + + override fun requiresTesting(): Boolean + { + return custom.requiresTesting + } + + override fun getSerializer(): CustomIngredientSerializer<*> + { + return custom.serializer.fabric + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.kt new file mode 100644 index 000000000..57da49326 --- /dev/null +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.kt @@ -0,0 +1,69 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +import com.mojang.serialization.* +import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer +import net.minecraft.network.RegistryFriendlyByteBuf +import net.minecraft.network.codec.StreamCodec +import net.minecraft.resources.ResourceLocation +import java.util.stream.Stream + +/** Fabric implementation of [ACustomIngredientSerializerPlatform], adapting [IACustomIngredientSerializer] onto Fabric's `CustomIngredientSerializer` API. */ +actual object ACustomIngredientSerializerPlatform +{ + /** Registers [serializer] with Fabric's [CustomIngredientSerializer] registry via [fabric]. */ + actual fun register(serializer: IACustomIngredientSerializer) + { + CustomIngredientSerializer.register(serializer.fabric) + } + + /** Wraps this serializer as a Fabric [CustomIngredientSerializer]. */ + val IACustomIngredientSerializer.fabric: CustomIngredientSerializer> + get() = FabricCustomIngredientSerializer(this) + + /** Adapts an [IACustomIngredientSerializer] to Fabric's [CustomIngredientSerializer] interface, wrapping/unwrapping [ACustomIngredientPlatform.FabricCustomIngredient] around the shared codec/packet codec. */ + class FabricCustomIngredientSerializer( + private val custom: IACustomIngredientSerializer + ) : CustomIngredientSerializer> + { + override fun getIdentifier(): ResourceLocation + { + return custom.identifier + } + + override fun getCodec(allowEmpty: Boolean): MapCodec> + { + val codec = custom.getCodec(allowEmpty) + return object : MapCodec>() + { + override fun encode( + input: ACustomIngredientPlatform.FabricCustomIngredient, + ops: DynamicOps, + prefix: RecordBuilder + ): RecordBuilder + { + return codec.encode(input.custom, ops, prefix) + } + + override fun keys(ops: DynamicOps): Stream + { + return codec.keys(ops) + } + + override fun decode( + ops: DynamicOps, + input: MapLike + ): DataResult> + { + return codec.decode(ops, input).map { + ACustomIngredientPlatform.FabricCustomIngredient(it) + } + } + } + } + + override fun getPacketCodec(): StreamCodec> + { + return custom.packetCodec.map({ ACustomIngredientPlatform.FabricCustomIngredient(it)}, {it.custom}) + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt new file mode 100644 index 000000000..8426bd1db --- /dev/null +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt @@ -0,0 +1,105 @@ +package net.kernelpanicsoft.archie.gametest + +import net.minecraft.Util +import net.minecraft.server.Main +import net.minecraft.server.MinecraftServer +import net.minecraft.server.dedicated.DedicatedServer +import java.nio.file.Files +import java.nio.file.Path +import java.util.Properties +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +/** + * Fabric implementation of [ADedicatedServerPlatform]. + * + * Boots a real dedicated server ([Main.main]) on a daemon thread and hands the resulting + * [DedicatedServer] back through [ADedicatedServerPlatformInternal], which `ServerMixin` feeds + * via [ADedicatedServerPlatformInternal.captureRunningServer] once the server instance exists. + */ +actual object ADedicatedServerPlatform { + /** Baseline `server.properties` for a headless, single-player-only GameTest server; overridden by caller-supplied properties. */ + private val defaultProperties: Properties = Util.make(Properties()) { props -> + props.setProperty("online-mode", "false") + props.setProperty("sync-chunk-writes", (Util.getPlatform() == Util.OS.WINDOWS).toString()) + props.setProperty("spawn-protection", "0") + props.setProperty("max-players", "1") + } + + /** + * Writes `server.properties`/`eula.txt` into [serverDirectory], launches vanilla's dedicated + * server entrypoint on a background thread, and blocks up to [timeoutSeconds] for it to report + * back via [ADedicatedServerPlatformInternal]. Falls back to the last captured server instance + * if the wait times out but a server is already up and listening. + */ + actual fun start(serverDirectory: Path, serverProperties: Properties, timeoutSeconds: Long): Any { + Files.createDirectories(serverDirectory) + writeServerFiles(serverDirectory, serverProperties) + + val future = ADedicatedServerPlatformInternal.beginBootstrap() + + Thread({ + try { + Main.main(arrayOf("--nogui", "--universe", serverDirectory.toAbsolutePath().toString(), "--world", "world")) + } catch (t: Throwable) { + ADedicatedServerPlatformInternal.failBootstrap(t) + } + }, "Archie Dedicated GameTest Server Bootstrap").apply { + isDaemon = true + start() + } + + val server = try { + future.get(timeoutSeconds, TimeUnit.SECONDS) + } catch (e: TimeoutException) { + ADedicatedServerPlatformInternal.clearBootstrap() + val fallbackServer = ADedicatedServerPlatformInternal.latestCapturedServer() + if (fallbackServer != null && fallbackServer.isRunning && fallbackServer.serverPort > 0) { + fallbackServer + } else { + throw IllegalStateException("Timed out waiting for dedicated server bootstrap", e) + } + } + + val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds) + while (System.nanoTime() < deadline) { + if (server.isRunning && server.serverPort > 0) break + Thread.sleep(50L) + } + + return server + } + + actual fun stop(serverInstance: Any) { + (serverInstance as? DedicatedServer)?.stopServer() + } + + actual fun port(serverInstance: Any): Int { + return (serverInstance as? DedicatedServer)?.serverPort ?: 25565 + } + + actual fun isAlive(serverInstance: Any): Boolean { + val threadMethod = serverInstance.javaClass.methods.firstOrNull { + (it.name == "getRunningThread" || it.name == "getThread") && it.parameterCount == 0 + } ?: return true + + val thread = runCatching { threadMethod.invoke(serverInstance) as? Thread }.getOrNull() + return thread?.isAlive ?: true + } + + private fun writeServerFiles(serverDirectory: Path, customProperties: Properties) { + val merged = Properties() + merged.putAll(defaultProperties) + merged.putAll(customProperties) + + Files.newBufferedWriter(serverDirectory.resolve("server.properties")).use { writer -> + merged.store(writer, "Archie GameTest dedicated server properties") + } + + Files.newBufferedWriter(serverDirectory.resolve("eula.txt")).use { writer -> + writer.write("eula=true") + writer.newLine() + } + } +} + diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt new file mode 100644 index 000000000..0f5163f50 --- /dev/null +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt @@ -0,0 +1,46 @@ +package net.kernelpanicsoft.archie.gametest + +import net.minecraft.server.MinecraftServer +import net.minecraft.server.dedicated.DedicatedServer +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicReference + +/** + * Bridges [ADedicatedServerPlatform.start] on the bootstrap thread with `ServerMixin`, which calls + * [captureRunningServer] from the constructed [MinecraftServer] once it exists. + */ +object ADedicatedServerPlatformInternal { + private val bootstrapFutureRef = AtomicReference?>(null) + private val latestCapturedServerRef = AtomicReference(null) + + /** Starts a new bootstrap wait, clearing any previously captured server. Throws if a bootstrap is already in progress. */ + fun beginBootstrap(): CompletableFuture { + val future = CompletableFuture() + latestCapturedServerRef.set(null) + check(bootstrapFutureRef.compareAndSet(null, future)) { "Dedicated server bootstrap already in progress" } + return future + } + + /** Fails the in-progress bootstrap future with [error]. */ + fun failBootstrap(error: Throwable) { + bootstrapFutureRef.getAndSet(null)?.completeExceptionally(error) + } + + /** Clears the in-progress bootstrap future without resolving it, used after a timeout falls back to [latestCapturedServer]. */ + fun clearBootstrap() { + bootstrapFutureRef.set(null) + } + + /** The most recently captured [DedicatedServer], if any; used as a fallback when the bootstrap future times out. */ + fun latestCapturedServer(): DedicatedServer? = latestCapturedServerRef.get() + + /** Called by `ServerMixin` when a [MinecraftServer] instance is constructed; completes the bootstrap future if [server] is a [DedicatedServer]. */ + @JvmStatic + fun captureRunningServer(server: MinecraftServer) { + if (server is DedicatedServer) { + latestCapturedServerRef.set(server) + bootstrapFutureRef.getAndSet(null)?.complete(server) + } + } +} + diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt new file mode 100644 index 000000000..8d4dfcdbb --- /dev/null +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt @@ -0,0 +1,43 @@ +package net.kernelpanicsoft.archie.gametest + +import dev.architectury.platform.Mod +import dev.architectury.platform.Platform + +/** Fabric implementation of [AGameTestPlatform]. */ +@Suppress("unused") +actual object AGameTestPlatform +{ + private const val SIDE_OVERRIDE_PROP = "archie.gametest.side" + private const val GAMETEST_PROP = "archie.gametest" + + /** + * True when running under one of Archie's own GameTest run configs. + * + * Deliberately reads [GAMETEST_PROP] instead of `FabricGameTestHelper.ENABLED` - Loom's + * generated dev-launch config shares a single property bucket per environment, so a flag set + * on the `gametestClient` run can leak into the plain `client` run's bucket too (observed on + * the NeoForge side; kept consistent here). [GAMETEST_PROP] is a property Archie's own build + * sets exclusively on its `gametest`/`gametestClient` runs, so it isn't affected by that leak. + */ + actual val isGameTest: Boolean + get() = System.getProperty(GAMETEST_PROP)?.toBoolean() == true + + actual val side: AGameTestSide? + get() { + val override = System.getProperty(SIDE_OVERRIDE_PROP)?.trim()?.lowercase() + return when (override) { + "client" -> AGameTestSide.CLIENT + "server" -> AGameTestSide.SERVER + else -> null + } + } + + val testClasses: MutableMap>> + get() = AGameTestPlatformInternal.testClasses + + actual fun register(clazz: Class<*>, mod: Mod) + { + testClasses.getOrPut(mod, ::mutableSetOf).add(clazz) + } + +} diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt new file mode 100644 index 000000000..5da3f7e18 --- /dev/null +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt @@ -0,0 +1,18 @@ +package net.kernelpanicsoft.archie.gametest + +import dev.architectury.platform.Mod + +/** + * Backs [AGameTestPlatform] on Fabric: holds registered test classes. + * + * Trimmed to the [testClasses] map [AGameTestPlatform.register] needs - the actual + * `registerGameTests()` driving logic (firing [net.kernelpanicsoft.archie.events.AEvents + * .REGISTER_GAME_TEST], wiring `GameTestRegistry`/`FabricGameTestModInitializerMixin`) is + * gametest-run-only and lives in `archie-gametest` instead. + */ +internal object AGameTestPlatformInternal +{ + /** Test classes registered via [AGameTestPlatform.register], keyed by owning mod. */ + @JvmField + internal val testClasses: MutableMap>> = mutableMapOf() +} diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt new file mode 100644 index 000000000..27f01b376 --- /dev/null +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt @@ -0,0 +1,21 @@ +package net.kernelpanicsoft.archie.gui.render + +import net.fabricmc.fabric.api.client.render.fluid.v1.FluidRenderHandlerRegistry +import net.minecraft.client.renderer.texture.TextureAtlasSprite +import net.minecraft.world.level.material.Fluid + +/** Fabric implementation of [AFluidRenderPlatform], backed by `fabric-rendering-fluids-v1`. */ +actual object AFluidRenderPlatform +{ + actual fun getStillSprite(fluid: Fluid): TextureAtlasSprite? + { + val handler = FluidRenderHandlerRegistry.INSTANCE.get(fluid) ?: return null + return handler.getFluidSprites(null, null, fluid.defaultFluidState()).getOrNull(0) + } + + actual fun getTintColor(fluid: Fluid): Int + { + val handler = FluidRenderHandlerRegistry.INSTANCE.get(fluid) ?: return -1 + return handler.getFluidColor(null, null, fluid.defaultFluidState()) + } +} diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt new file mode 100644 index 000000000..693faf7c6 --- /dev/null +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt @@ -0,0 +1,6 @@ +package net.kernelpanicsoft.archie.registries + +import dev.architectury.platform.Mod + +/** Fabric has no staged registry-event model to race, so [block] just runs immediately. */ +actual fun scheduleEarlyClientRegistration(mod: Mod, block: () -> Unit) = block() diff --git a/Archie-Core/core/fabric/src/main/resources/archie.mixins.json b/Archie-Core/core/fabric/src/main/resources/archie.mixins.json new file mode 100644 index 000000000..a4f977bee --- /dev/null +++ b/Archie-Core/core/fabric/src/main/resources/archie.mixins.json @@ -0,0 +1,16 @@ +{ + "required": true, + "package": "net.kernelpanicsoft.archie.mixin.fabric", + "plugin": "net.kernelpanicsoft.archie.mixin.fabric.ArchieMixinPlugin", + "compatibilityLevel": "JAVA_17", + "minVersion": "0.8", + "client": [ + "threading.MinecraftClientMixin" + ], + "mixins": [ + "threading.ServerMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/Archie-Core/core/fabric/src/main/resources/fabric.mod.json b/Archie-Core/core/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 000000000..706814a99 --- /dev/null +++ b/Archie-Core/core/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,55 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}", + "version": "${mod_version}", + "name": "${mod_display_name}", + "description": "${mod_description}", + "authors": [ + "${mod_authors}" + ], + "contributors": [ + "${mod_credits}" + ], + "contact": { + "homepage": "${mod_url}", + "sources": "${mod_source}" + }, + "custom": { + "catalogue": { + "banner": "assets/${mod_id}/banner.png" + } + }, + "license": "${mod_license}", + "icon": "assets/${mod_id}/icon.png", + "environment": "*", + "entrypoints": { + "main": [ + { + "adapter": "kotlin", + "value": "net.kernelpanicsoft.archie.ArchieFabric" + } + ], + "client": [ + { + "adapter": "kotlin", + "value": "net.kernelpanicsoft.archie.ArchieFabric" + } + ] + }, + "mixins": [ + "${mod_id}.mixins.json", + "${mod_id}-common.mixins.json" + ], + "depends": { + "minecraft": "${versions.minecraft}", + "fabricloader": ">=${versions.fabric_loader}", + "fabric-api": ">=${versions.fabric_api}", + "fabric-language-kotlin": ">=${versions.kotlin_fabric}", + "architectury": ">=${versions.architectury}" + }, + "suggests": { + "cloth-config": ">=${versions.cloth_config_range}", + "modmenu": "*", + "catalogue": "*" + } +} diff --git a/Archie-Core/core/neoforge/build.gradle.kts b/Archie-Core/core/neoforge/build.gradle.kts new file mode 100644 index 000000000..f02041c19 --- /dev/null +++ b/Archie-Core/core/neoforge/build.gradle.kts @@ -0,0 +1,150 @@ +import net.kernelpanicsoft.archie.plugin.bundleMod +import net.kernelpanicsoft.archie.plugin.bundleRuntimeLibrary +import net.kernelpanicsoft.archie.plugin.runtimeLibrary + +plugins { + alias(libs.plugins.shadow) + alias(libs.plugins.archie) +} + +architectury { + platformSetupLoomIde() + neoForge() +} + +actualizer { + actualizes(project(":archie-core-common")) +} + +configurations { + create("common") + create("shadowCommon") + configureEach { + // Keep NeoForge Kotlin runtime provided by KotlinLangForge only. + exclude(group = "thedarkcolour", module = "kotlinforforge-neoforge") + exclude(group = "remapped.thedarkcolour", module = "kotlinforforge-neoforge-1d1bcbf2") + } + compileClasspath.get().extendsFrom(configurations["common"]) + runtimeClasspath.get().extendsFrom(configurations["common"]) + testCompileClasspath.get().extendsFrom(compileClasspath.get()) + testRuntimeClasspath.get().extendsFrom(runtimeClasspath.get()) +} + +loom { + accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) + + mods { + maybeCreate("main").apply { + sourceSet(sourceSets.main.get()) + } + } + + runs { + getByName("client") { + name = "Minecraft Client" + source(sourceSets.main.get()) + vmArgs("-XX:+AllowEnhancedClassRedefinition") + property("kotlinx.coroutines.debug", "off") + } + getByName("server") { + name = "Minecraft Server" + source(sourceSets.main.get()) + vmArgs("-XX:+AllowEnhancedClassRedefinition") + property("kotlinx.coroutines.debug", "off") + } + } +} + +dependencies { + neoForge(libs.neoforge) + modApi(libs.architectury.neoforge) + implementation(libs.kotlin.neoforge) + compileOnly(libs.kotlinx.serialization) + bundleRuntimeLibrary(libs.kotlinx.serialization) + bundleRuntimeLibrary(libs.kotlinx.serialization.json) + bundleRuntimeLibrary(libs.kotlinx.serialization.nbt) + bundleRuntimeLibrary(libs.kotlinx.serialization.toml) + bundleRuntimeLibrary(libs.kotlinx.serialization.json5) + bundleRuntimeLibrary(libs.kotlinx.serialization.cbor) + bundleRuntimeLibrary(compose.runtime) + modRuntimeOnly(libs.rei.neoforge) + modCompileOnlyApi(libs.catalogue.neoforge) + modRuntimeOnly(libs.catalogue.neoforge) + modCompileOnlyApi(libs.clothConfig.neoforge) + modRuntimeOnly(libs.clothConfig.neoforge) + bundleMod(libs.storage.neoforge) { + exclude(group = "curse.maven") + } + + implementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.api) + testRuntimeOnly(libs.junit.jupiter.engine) + runtimeLibrary(libs.kotlinx.coroutines.test) + + "common"(project(":archie-core-common", "namedElements")) { isTransitive = false } + "shadowCommon"(project(":archie-core-common", "transformProductionNeoForge")) { isTransitive = false } +} + +modResources { + filesMatching.add("META-INF/neoforge.mods.toml") +} + +tasks { + base.archivesName.set(base.archivesName.get() + "-neoforge") + + test { + useJUnitPlatform() + } + + processResources { + from(project(":archie-core-common").sourceSets.main.get().resources) { + include("assets/archie/**") + include("data/archie/**") + include("archie-common.mixins.json") + include("archie.common.json") + include("archie.accesswidener") + } + dependsOn(processTestResources) + } + + processTestResources { + } + + classes { + finalizedBy(testClasses) + } + + shadowJar { + exclude("fabric.mod.json") + configurations = + listOf(project.configurations.getByName("shadowCommon"), project.configurations.getByName("shadow")) + archiveClassifier.set("dev-shadow") + } + + remapJar { + inputFile.set(shadowJar.get().archiveFile) + atAccessWideners.set(setOf(loom.accessWidenerPath.get().asFile.name)) + dependsOn(shadowJar) + } + + jar.get().archiveClassifier.set("dev") + + jar { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(project(":archie-core-common").sourceSets.main.get().output) { + // That output is common's own independently-compiled (stub-linked) classes - this + // module's own sourceSets.main.output already has a correctly-actualized copy of all + // of them via actualizes(project(":archie-core-common")) above. Exclude so the + // stub-linked copy can't win the duplicatesStrategy race - it did once, and threw at + // runtime (see today's Archie/neoforge/build.gradle.kts's matching comment). + exclude("net/kernelpanicsoft/archie/**") + } + } + + sourcesJar { + val commonSources = project(":archie-core-common").tasks.sourcesJar + dependsOn(commonSources) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(commonSources.get().archiveFile.map { zipTree(it) }) + } +} diff --git a/Archie-Core/core/neoforge/gradle.properties b/Archie-Core/core/neoforge/gradle.properties new file mode 100644 index 000000000..2914393db --- /dev/null +++ b/Archie-Core/core/neoforge/gradle.properties @@ -0,0 +1 @@ +loom.platform=neoforge \ No newline at end of file diff --git a/Archie-Core/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java b/Archie-Core/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java new file mode 100644 index 000000000..262da08ba --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java @@ -0,0 +1,44 @@ +package net.kernelpanicsoft.archie.mixin.neoforge.threading; + +import net.kernelpanicsoft.archie.gametest.ThreadingImpl; +import net.minecraft.client.Minecraft; +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(Minecraft.class) +public class MinecraftClientMixin { + @Inject(method = "run", at = @At("HEAD")) + private void archie$onRunStart(CallbackInfo ci) { + ThreadingImpl.onClientRunStart(); + } + + @Inject(method = "run", at = @At("RETURN")) + private void archie$onRunStop(CallbackInfo ci) { + ThreadingImpl.onClientRunStop(); + } + + @Inject(method = "runTick(Z)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;runAllTasks()V")) + private void archie$preRunTasks(CallbackInfo ci) { + ThreadingImpl.preRunTasks(); + } + + @Inject(method = "runTick(Z)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;runAllTasks()V", shift = At.Shift.AFTER)) + private void archie$postRunTasks(CallbackInfo ci) { + ThreadingImpl.postRunTasks(); + } + + @Inject(method = "delayCrashRaw", at = @At("HEAD")) + private void archie$onDelayCrashRaw(CallbackInfo ci) { + ThreadingImpl.setGameCrashed(); + } + + @Inject(method = "emergencySaveAndCrash", at = @At("HEAD")) + private void archie$onEmergencySaveAndCrash(CallbackInfo ci) { + ThreadingImpl.setGameCrashed(); + } +} + + + diff --git a/Archie-Core/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java b/Archie-Core/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java new file mode 100644 index 000000000..15636d026 --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java @@ -0,0 +1,33 @@ +package net.kernelpanicsoft.archie.mixin.neoforge.threading; + +import net.kernelpanicsoft.archie.gametest.ThreadingImpl; +import net.kernelpanicsoft.archie.gametest.ADedicatedServerPlatformInternal; +import net.minecraft.server.MinecraftServer; +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; + +/** + * Injects ThreadingImpl.onServerTick() into MinecraftServer tick cycle for GameTest coordination. + * Allows ThreadingImpl to coordinate server-side task execution with client-side gametest thread. + */ +@Mixin(MinecraftServer.class) +public class ServerMixin { + @Inject(method = "runServer", at = @At("HEAD")) + private void archie$onRunServerStart(CallbackInfo ci) { + ADedicatedServerPlatformInternal.captureRunningServer((MinecraftServer) (Object) this); + ThreadingImpl.onServerRunStart(); + } + + @Inject(method = "runServer", at = @At("RETURN")) + private void archie$onRunServerStop(CallbackInfo ci) { + ThreadingImpl.onServerRunStop(); + } + + @Inject(method = "tickServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;tickChildren(Ljava/util/function/BooleanSupplier;)V", shift = At.Shift.BEFORE)) + private void archie$onServerTick(CallbackInfo ci) { + ThreadingImpl.onServerTick(); + } +} + diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt new file mode 100644 index 000000000..de6f98342 --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt @@ -0,0 +1,10 @@ +package net.kernelpanicsoft.archie + +import dev.architectury.platform.Mod +import dev.architectury.platform.Platform + +/** NeoForge implementation of [APlatform]. */ +actual object APlatform +{ + actual val platform: String = "neoforge" +} \ No newline at end of file diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt new file mode 100644 index 000000000..a84760b99 --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt @@ -0,0 +1,31 @@ +package net.kernelpanicsoft.archie + +import dev.architectury.event.events.client.ClientTickEvent +import dev.nyon.klf.MOD_BUS +import net.kernelpanicsoft.archie.gametest.ThreadingImpl +import net.neoforged.fml.common.Mod +import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent +import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent +import net.neoforged.fml.event.lifecycle.FMLConstructModEvent + +/** + * NeoForge entrypoint for the mod, registered via the `@Mod` annotation. + * + * Delegates all real initialization to [Archie], wiring its lifecycle calls into the NeoForge + * mod-bus events ([FMLConstructModEvent], [FMLClientSetupEvent], [FMLCommonSetupEvent]) and + * registering the client tick pump used by [ThreadingImpl]. + */ +@Mod(Archie.MOD_ID) +object ArchieNeoForge { + init { + MOD_BUS.addListener { + Archie.init() + } + MOD_BUS.addListener { + Archie.initClient() + } + MOD_BUS.addListener { + Archie.initCommon() + } + } +} diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.kt new file mode 100644 index 000000000..a447ec655 --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.kt @@ -0,0 +1,10 @@ +package net.kernelpanicsoft.archie.data + + +/** NeoForge implementation of [ADataGeneratorPlatform]. */ +actual object ADataGeneratorPlatform +{ + /** True when launched via `neoforge:runDatagen`, which sets the `archie.datagen` system property. */ + actual val isDataGen: Boolean + get() = System.getProperty("archie.datagen").toBoolean() +} \ No newline at end of file diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt new file mode 100644 index 000000000..0419713ee --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt @@ -0,0 +1,113 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.mojang.serialization.* +import dev.nyon.klf.MOD_BUS +import net.minecraft.core.Holder +import net.minecraft.core.HolderLookup +import net.minecraft.core.Registry +import net.minecraft.core.RegistryAccess +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.resources.ResourceKey +import net.minecraft.resources.ResourceLocation +import net.neoforged.neoforge.common.conditions.ICondition +import net.neoforged.neoforge.registries.DeferredRegister +import net.neoforged.neoforge.registries.NeoForgeRegistries +import java.util.stream.Stream +import net.kernelpanicsoft.archie.data.common.conditions.IACondition as ArchieCondition + +/** + * NeoForge implementation of [AConditionsPlatform], adapting Archie's platform-neutral + * [ArchieCondition] onto NeoForge's `ICondition` API. + * + * Every [ArchieCondition] is wrapped as a [NeoForgeCondition] to cross into NeoForge's condition + * system, and unwrapped again via [ICondition.archie] when Archie code needs the original back. + */ +actual object AConditionsPlatform +{ + /** Registers [identifier] as a NeoForge condition serializer, backed by [codec] via the [NeoForgeConditionCodec] wrapper. */ + actual fun register(identifier: ResourceLocation, codec: MapCodec) + { + val registry = DeferredRegister.create(NeoForgeRegistries.CONDITION_SERIALIZERS, identifier.namespace) + registry.register(identifier.path) { _ -> + codec.neoforge + } + registry.register(MOD_BUS) + } + + /** Codec for [ArchieCondition] backed by `ICondition.CODEC`, round-tripping through [neoforge]/[archie]. */ + actual fun codec(): Codec + { + return ICondition.CODEC.xmap({ + it.archie + }, { + it.neoforge + }) + } + + /** Unwraps a NeoForge [ICondition] back to its originating [ArchieCondition]. Throws if it wasn't created via [neoforge]. */ + val ICondition.archie + get() = ((this as? NeoForgeCondition) ?: throw AssertionError()).condition + + /** Wraps this condition as a NeoForge [ICondition]. */ + val ArchieCondition.neoforge + get() = NeoForgeCondition(this) + + /** Wraps this codec as a NeoForge condition-serializer codec. */ + val MapCodec.neoforge + get() = NeoForgeConditionCodec(this) + + /** Adapts an [ArchieCondition] to NeoForge's [ICondition] interface, delegating [test] to it and [codec] to the registered serializer. */ + class NeoForgeCondition( + val condition: ArchieCondition + ) : ICondition + { + override fun test(iContext: ICondition.IContext): Boolean + { + return condition.test(object : ArchieCondition.IContext + { + override fun getAllTags(registry: ResourceKey>): Map>> + { + return iContext.getAllTags(registry) + } + + override fun getRegistry(registry: ResourceKey>): Registry + { + return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY).registryOrThrow(registry) + } + }) + } + + override fun codec(): MapCodec + { + return NeoForgeRegistries.CONDITION_SERIALIZERS[condition.identifier]!! + } + } + + /** Adapts a [MapCodec] of [ArchieCondition] to one producing/consuming [NeoForgeCondition] wrappers. */ + class NeoForgeConditionCodec( + private val codec: MapCodec + ) : MapCodec() + { + override fun encode( + input: NeoForgeCondition, + ops: DynamicOps, + prefix: RecordBuilder + ): RecordBuilder + { + @Suppress("UNCHECKED_CAST") + return (codec as MapCodec).encode(input.condition, ops, prefix) + } + + override fun keys(ops: DynamicOps): Stream + { + return codec.keys(ops) + } + + override fun decode(ops: DynamicOps, input: MapLike): DataResult + { + return codec.decode(ops, input).map { result -> + result.neoforge + } + } + } +} diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.kt new file mode 100644 index 000000000..bb15eb2ed --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.kt @@ -0,0 +1,37 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +import net.kernelpanicsoft.archie.data.common.crafting.ingredients.IACustomIngredient as ArchieIngredient +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.crafting.Ingredient +import net.neoforged.neoforge.common.crafting.ICustomIngredient +import net.neoforged.neoforge.common.crafting.IngredientType +import net.neoforged.neoforge.registries.NeoForgeRegistries +import java.util.stream.Stream + +/** NeoForge implementation of [ACustomIngredientPlatform], adapting [ArchieIngredient] onto NeoForge's `ICustomIngredient` API. */ +actual object ACustomIngredientPlatform +{ + /** Wraps [custom] as a NeoForge [ICustomIngredient] and converts it to a vanilla [Ingredient]. */ + actual fun vanillaOf(custom: ArchieIngredient): Ingredient + { + return custom.neoforge.toVanilla() + } + + /** Wraps this ingredient as a NeoForge [ICustomIngredient]. */ + val T.neoforge: NeoForgeCustomIngredient + get() = NeoForgeCustomIngredient(this) + + /** Adapts an [ArchieIngredient] to NeoForge's [ICustomIngredient] interface, delegating all matching logic to it; never treated as [isSimple]. */ + class NeoForgeCustomIngredient( + override val custom: T + ) : ICustomIngredient, IACustomIngredientHolder + { + override fun test(arg: ItemStack): Boolean = custom.test(arg) + + override fun getItems(): Stream = custom.matchingStacks.stream() + + override fun isSimple(): Boolean = false + + override fun getType(): IngredientType<*> = NeoForgeRegistries.INGREDIENT_TYPES[custom.serializer.identifier]!! + } +} \ No newline at end of file diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.kt new file mode 100644 index 000000000..1a13eda1d --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.kt @@ -0,0 +1,57 @@ +package net.kernelpanicsoft.archie.data.common.crafting.ingredients + +import com.mojang.serialization.* +import dev.nyon.klf.MOD_BUS +import net.kernelpanicsoft.archie.data.common.crafting.ingredients.ACustomIngredientPlatform.neoforge +import net.neoforged.neoforge.common.crafting.IngredientType +import net.neoforged.neoforge.registries.DeferredRegister +import net.neoforged.neoforge.registries.NeoForgeRegistries +import java.util.stream.Stream + +/** NeoForge implementation of [ACustomIngredientSerializerPlatform], adapting [IACustomIngredientSerializer] onto NeoForge's `IngredientType` registry. */ +actual object ACustomIngredientSerializerPlatform +{ + /** Registers [serializer] as a NeoForge [IngredientType] via [neoforge]. */ + actual fun register(serializer: IACustomIngredientSerializer) + { + val registry = DeferredRegister.create(NeoForgeRegistries.INGREDIENT_TYPES, serializer.identifier.namespace) + registry.register(serializer.identifier.path) { _ -> serializer.neoforge } + registry.register(MOD_BUS) + } + + /** Wraps this serializer as a NeoForge [IngredientType], backed by [NeoForgeCustomIngredientCodec]. */ + val IACustomIngredientSerializer.neoforge: IngredientType> + get() = IngredientType(NeoForgeCustomIngredientCodec(this)) + + /** Adapts an [IACustomIngredientSerializer]'s codec to one producing/consuming [ACustomIngredientPlatform.NeoForgeCustomIngredient] wrappers; no packet codec since NeoForge derives sync from the data codec. */ + class NeoForgeCustomIngredientCodec( + custom: IACustomIngredientSerializer + ) : MapCodec>() + { + private val codec = custom.getCodec(false) + + override fun keys(ops: DynamicOps): Stream + { + return codec.keys(ops) + } + + override fun encode( + input: ACustomIngredientPlatform.NeoForgeCustomIngredient, + ops: DynamicOps, + prefix: RecordBuilder + ): RecordBuilder + { + return codec.encode(input.custom, ops, prefix) + } + + override fun decode( + ops: DynamicOps, + input: MapLike + ): DataResult> + { + return codec.decode(ops, input).map { + it.neoforge + } + } + } +} \ No newline at end of file diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt new file mode 100644 index 000000000..8d3defcc2 --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt @@ -0,0 +1,105 @@ +package net.kernelpanicsoft.archie.gametest + +import net.minecraft.Util +import net.minecraft.server.Main +import net.minecraft.server.MinecraftServer +import net.minecraft.server.dedicated.DedicatedServer +import java.nio.file.Files +import java.nio.file.Path +import java.util.Properties +import java.util.concurrent.TimeUnit +import java.util.concurrent.TimeoutException + +/** + * NeoForge implementation of [ADedicatedServerPlatform]. + * + * Boots a real dedicated server ([Main.main]) on a daemon thread and hands the resulting + * [DedicatedServer] back through [ADedicatedServerPlatformInternal], which `ServerMixin` feeds + * via [ADedicatedServerPlatformInternal.captureRunningServer] once the server instance exists. + */ +actual object ADedicatedServerPlatform { + /** Baseline `server.properties` for a headless, single-player-only GameTest server; overridden by caller-supplied properties. */ + private val defaultProperties: Properties = Util.make(Properties()) { props -> + props.setProperty("online-mode", "false") + props.setProperty("sync-chunk-writes", (Util.getPlatform() == Util.OS.WINDOWS).toString()) + props.setProperty("spawn-protection", "0") + props.setProperty("max-players", "1") + } + + /** + * Writes `server.properties`/`eula.txt` into [serverDirectory], launches vanilla's dedicated + * server entrypoint on a background thread, and blocks up to [timeoutSeconds] for it to report + * back via [ADedicatedServerPlatformInternal]. Falls back to the last captured server instance + * if the wait times out but a server is already up and listening. + */ + actual fun start(serverDirectory: Path, serverProperties: Properties, timeoutSeconds: Long): Any { + Files.createDirectories(serverDirectory) + writeServerFiles(serverDirectory, serverProperties) + + val future = ADedicatedServerPlatformInternal.beginBootstrap() + + Thread({ + try { + Main.main(arrayOf("--nogui", "--universe", serverDirectory.toAbsolutePath().toString(), "--world", "world")) + } catch (t: Throwable) { + ADedicatedServerPlatformInternal.failBootstrap(t) + } + }, "Archie Dedicated GameTest Server Bootstrap").apply { + isDaemon = true + start() + } + + val server = try { + future.get(timeoutSeconds, TimeUnit.SECONDS) + } catch (e: TimeoutException) { + ADedicatedServerPlatformInternal.clearBootstrap() + val fallbackServer = ADedicatedServerPlatformInternal.latestCapturedServer() + if (fallbackServer != null && fallbackServer.isRunning && fallbackServer.serverPort > 0) { + fallbackServer + } else { + throw IllegalStateException("Timed out waiting for dedicated server bootstrap", e) + } + } + + val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds) + while (System.nanoTime() < deadline) { + if (server.isRunning && server.serverPort > 0) break + Thread.sleep(50L) + } + + return server + } + + actual fun stop(serverInstance: Any) { + (serverInstance as? DedicatedServer)?.stopServer() + } + + actual fun port(serverInstance: Any): Int { + return (serverInstance as? DedicatedServer)?.serverPort ?: 25565 + } + + actual fun isAlive(serverInstance: Any): Boolean { + val threadMethod = serverInstance.javaClass.methods.firstOrNull { + (it.name == "getRunningThread" || it.name == "getThread") && it.parameterCount == 0 + } ?: return true + + val thread = runCatching { threadMethod.invoke(serverInstance) as? Thread }.getOrNull() + return thread?.isAlive ?: true + } + + private fun writeServerFiles(serverDirectory: Path, customProperties: Properties) { + val merged = Properties() + merged.putAll(defaultProperties) + merged.putAll(customProperties) + + Files.newBufferedWriter(serverDirectory.resolve("server.properties")).use { writer -> + merged.store(writer, "Archie GameTest dedicated server properties") + } + + Files.newBufferedWriter(serverDirectory.resolve("eula.txt")).use { writer -> + writer.write("eula=true") + writer.newLine() + } + } +} + diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt new file mode 100644 index 000000000..0f5163f50 --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt @@ -0,0 +1,46 @@ +package net.kernelpanicsoft.archie.gametest + +import net.minecraft.server.MinecraftServer +import net.minecraft.server.dedicated.DedicatedServer +import java.util.concurrent.CompletableFuture +import java.util.concurrent.atomic.AtomicReference + +/** + * Bridges [ADedicatedServerPlatform.start] on the bootstrap thread with `ServerMixin`, which calls + * [captureRunningServer] from the constructed [MinecraftServer] once it exists. + */ +object ADedicatedServerPlatformInternal { + private val bootstrapFutureRef = AtomicReference?>(null) + private val latestCapturedServerRef = AtomicReference(null) + + /** Starts a new bootstrap wait, clearing any previously captured server. Throws if a bootstrap is already in progress. */ + fun beginBootstrap(): CompletableFuture { + val future = CompletableFuture() + latestCapturedServerRef.set(null) + check(bootstrapFutureRef.compareAndSet(null, future)) { "Dedicated server bootstrap already in progress" } + return future + } + + /** Fails the in-progress bootstrap future with [error]. */ + fun failBootstrap(error: Throwable) { + bootstrapFutureRef.getAndSet(null)?.completeExceptionally(error) + } + + /** Clears the in-progress bootstrap future without resolving it, used after a timeout falls back to [latestCapturedServer]. */ + fun clearBootstrap() { + bootstrapFutureRef.set(null) + } + + /** The most recently captured [DedicatedServer], if any; used as a fallback when the bootstrap future times out. */ + fun latestCapturedServer(): DedicatedServer? = latestCapturedServerRef.get() + + /** Called by `ServerMixin` when a [MinecraftServer] instance is constructed; completes the bootstrap future if [server] is a [DedicatedServer]. */ + @JvmStatic + fun captureRunningServer(server: MinecraftServer) { + if (server is DedicatedServer) { + latestCapturedServerRef.set(server) + bootstrapFutureRef.getAndSet(null)?.complete(server) + } + } +} + diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt new file mode 100644 index 000000000..2716dbe42 --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt @@ -0,0 +1,42 @@ +package net.kernelpanicsoft.archie.gametest + +import dev.architectury.platform.Mod +import dev.architectury.platform.Platform + +/** NeoForge implementation of [AGameTestPlatform]. */ +@Suppress("unused") +actual object AGameTestPlatform +{ + private const val SIDE_OVERRIDE_PROP = "archie.gametest.side" + private const val GAMETEST_PROP = "archie.gametest" + + /** + * True when running under one of Archie's own GameTest run configs. + * + * Deliberately reads [GAMETEST_PROP] instead of `GameTestHooks.isGametestEnabled()` - Loom's + * generated dev-launch config shares a single property bucket per environment, so + * `neoforge.enableGameTest` set on the `gametestClient` run leaks into the plain `client` + * run's bucket too. [GAMETEST_PROP] is a property Archie's own build sets exclusively on its + * `gametest`/`gametestClient` runs, so it isn't affected by that leak. + */ + actual val isGameTest: Boolean + get() = System.getProperty(GAMETEST_PROP)?.toBoolean() == true + + actual val side: AGameTestSide? + get() { + val override = System.getProperty(SIDE_OVERRIDE_PROP)?.trim()?.lowercase() + return when (override) { + "client" -> AGameTestSide.CLIENT + "server" -> AGameTestSide.SERVER + else -> null + } + } + + val testClasses: MutableMap>> + get() = AGameTestPlatformInternal.testClasses + + actual fun register(clazz: Class<*>, mod: Mod) + { + testClasses.getOrPut(mod, ::mutableSetOf).add(clazz) + } +} diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt new file mode 100644 index 000000000..ae4cb5b02 --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt @@ -0,0 +1,16 @@ +package net.kernelpanicsoft.archie.gametest + +import dev.architectury.platform.Mod + +/** + * Backs [AGameTestPlatform] on NeoForge: holds registered test classes. + * + * Trimmed to the [testClasses] map [AGameTestPlatform.register] needs - the actual + * `registerGameTests()` driving logic is gametest-run-only and lives in `archie-gametest` instead. + */ +internal object AGameTestPlatformInternal +{ + /** Test classes registered via [AGameTestPlatform.register], keyed by owning mod. */ + @JvmField + internal val testClasses: MutableMap>> = mutableMapOf() +} diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt new file mode 100644 index 000000000..0e973b4a0 --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt @@ -0,0 +1,19 @@ +package net.kernelpanicsoft.archie.gui.render + +import net.minecraft.client.Minecraft +import net.minecraft.client.renderer.texture.TextureAtlas +import net.minecraft.client.renderer.texture.TextureAtlasSprite +import net.minecraft.world.level.material.Fluid +import net.neoforged.neoforge.client.extensions.common.IClientFluidTypeExtensions + +/** NeoForge implementation of [AFluidRenderPlatform], backed by [IClientFluidTypeExtensions]. */ +actual object AFluidRenderPlatform +{ + actual fun getStillSprite(fluid: Fluid): TextureAtlasSprite? + { + val loc = IClientFluidTypeExtensions.of(fluid).stillTexture ?: return null + return Minecraft.getInstance().modelManager.getAtlas(TextureAtlas.LOCATION_BLOCKS).getSprite(loc) + } + + actual fun getTintColor(fluid: Fluid): Int = IClientFluidTypeExtensions.of(fluid).tintColor +} diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt new file mode 100644 index 000000000..c3c77f5df --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt @@ -0,0 +1,18 @@ +package net.kernelpanicsoft.archie.registries + +import dev.architectury.platform.Mod +import dev.architectury.platform.hooks.EventBusesHooks +import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent + +/** + * [RegisterMenuScreensEvent] is the earliest point NeoForge's own client registration-stage + * events fire, and specifically the one `MenuRegistry.registerScreenFactory` itself listens + * for internally - hooking the exact same event here (on [mod]'s own bus, since it's a per-mod + * event) guarantees this fires before that internal listener would otherwise miss it. + */ +actual fun scheduleEarlyClientRegistration(mod: Mod, block: () -> Unit) +{ + EventBusesHooks.whenAvailable(mod.modId) { bus -> + bus.addListener(RegisterMenuScreensEvent::class.java) { block() } + } +} diff --git a/Archie-Core/core/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/Archie-Core/core/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 000000000..ef7a416e1 --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,46 @@ +modLoader = "klf" +loaderVersion = "[${versions.kotlin_neoforge_range},)" +issueTrackerURL = "" +license = "${mod_license}" + +[[mods]] +modId = "${mod_id}" +version = "${mod_version}" +displayName = "${mod_display_name}" +authors = "${mod_authors}" +credits = "${mod_credits}" +description = ''' +${mod_description} +''' +logoFile = "assets/${mod_id}/banner.png" +displayURL = "${mod_url}" + +[[mixins]] +config = "${mod_id}.mixins.json" + +[[mixins]] +config = "${mod_id}-common.mixins.json" + +[[dependencies."${mod_id}"]] +modId = "neoforge" +type = "required" +versionRange = "[${versions.neoforge_range},)" +ordering = "NONE" +side = "BOTH" + +[[dependencies."${mod_id}"]] +modId = "minecraft" +type = "required" +versionRange = "[${versions.minecraft}]" +ordering = "NONE" +side = "BOTH" + +[[dependencies."${mod_id}"]] +modId = "cloth_config" +type = "optional" +versionRange = "[${versions.cloth_config_range},)" +ordering = "NONE" +side = "BOTH" + +[modproperties."${mod_id}"] +catalogueImageIcon = "assets/${mod_id}/icon.png" diff --git a/Archie-Core/core/neoforge/src/main/resources/archie.mixins.json b/Archie-Core/core/neoforge/src/main/resources/archie.mixins.json new file mode 100644 index 000000000..03445fbc6 --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/resources/archie.mixins.json @@ -0,0 +1,15 @@ +{ + "required": true, + "package": "net.kernelpanicsoft.archie.mixin.neoforge", + "compatibilityLevel": "JAVA_17", + "minVersion": "0.8", + "client": [ + "threading.MinecraftClientMixin" + ], + "mixins": [ + "threading.ServerMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/Archie-Core/gradle.properties b/Archie-Core/gradle.properties new file mode 100644 index 000000000..c842d3012 --- /dev/null +++ b/Archie-Core/gradle.properties @@ -0,0 +1,12 @@ +org.gradle.jvmargs=-Xmx8G +kotlin.incremental=false +mod_id=archie +mod_group=net.kernelpanicsoft +mod_version=1.0.0 +mod_display_name=Archie +mod_description=A library mod for Kernel Panic's mods +mod_authors=Kernel Panic +mod_credits=Both the NeoForge and fabric teams for the code I ported to Architectury and Kotlin +mod_url=https://github.com/kernel-panic-codecave/Archie +mod_source=https://github.com/kernel-panic-codecave/Archie +mod_license=GPL-3.0-or-later diff --git a/Archie-Core/gradle/wrapper/gradle-wrapper.jar b/Archie-Core/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..a4b76b9530d66f5e68d973ea569d8e19de379189 GIT binary patch literal 43583 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 GZ~q4|mN%#X literal 0 HcmV?d00001 diff --git a/Archie-Core/gradle/wrapper/gradle-wrapper.properties b/Archie-Core/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 000000000..cea7a793a --- /dev/null +++ b/Archie-Core/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/Archie-Core/gradlew b/Archie-Core/gradlew new file mode 100755 index 000000000..f3b75f3b0 --- /dev/null +++ b/Archie-Core/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/Archie-Core/gradlew.bat b/Archie-Core/gradlew.bat new file mode 100644 index 000000000..9d21a2183 --- /dev/null +++ b/Archie-Core/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/Archie-Core/settings.gradle.kts b/Archie-Core/settings.gradle.kts new file mode 100644 index 000000000..f9949d06b --- /dev/null +++ b/Archie-Core/settings.gradle.kts @@ -0,0 +1,50 @@ +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +rootProject.name = "Archie-Core" + +pluginManagement { + repositories { + maven("https://maven.fabricmc.net/") + maven("https://maven.architectury.dev/") + maven("https://maven.minecraftforge.net/") + maven("https://maven.neoforged.net/releases/") + maven("https://maven.firstdarkdev.xyz/releases") + maven { + name = "kernelpanic releases" + url = uri("https://maven.kernelpanicsoft.net/releases") + } + maven { + name = "kernelpanic snapshots" + url = uri("https://maven.kernelpanicsoft.net/snapshots") + } + mavenLocal() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + versionCatalogs { + create("libs") { + from(files("../gradle/libs.versions.toml")) + } + } +} + +// Matches terrarium-earth/Common-Storage-Lib's settings.gradle.kts layout: one nested +// / directory per platform, flattened into a single-level Gradle project name +// (e.g. core/fabric -> archie-core-fabric). archie-core is today's module; archie-datagen and +// archie-gametest (and eventually the test mod) join later via more includeModule(...) calls, +// without needing any further settings.gradle.kts restructuring. +includeCorePlatform("common") +includeCorePlatform("fabric") +includeCorePlatform("neoforge") + +fun includeModule(name: String, platform: String) { + include("$name/$platform") + project(":$name/$platform").name = "archie-$name-$platform" +} + +fun includeCorePlatform(platform: String) { + include("core/$platform") + project(":core/$platform").name = "archie-core-$platform" +} From 1f8ad13c1c9333c82063478acba4cd3efaebca5e Mon Sep 17 00:00:00 2001 From: KernelPanic Date: Mon, 10 Aug 2026 21:08:25 -0400 Subject: [PATCH 2/9] Stand up archie-datagen and archie-gametest as sibling Loom modules Ports the old Archie/common+fabric+neoforge's data and gametest packages onto their own Cloche-style nested modules (datagen/{common,fabric,neoforge}, gametest/{common,fabric,neoforge}), matching archie-core's Loom layout and Common-Storage-Lib's flattened project-naming convention. - Dissolves AEvents into ADatagenEvents (archie-datagen-common) and AGametestEvents (archie-gametest-common); generic event-wrapper plumbing (Handler/HandlerConstructor/AEventObject) stays in archie-core. - Datagen/gametest each ship as their own separate mod (own modId, own fabric.mod.json/neoforge.mods.toml, own mixins.json), triggering registration flush directly via their own mixins - no ArchieExtension dependency inversion needed for the trigger point itself. - Adds an ArchieExtension ServiceLoader hook implementation in each new module (DatagenArchieExtension/GametestArchieExtension) so Archie.kt's existing onDataGen()/onGameTest() calls activate ArchieDatagen/ ArchieGameTestwhen those modules are present, with no compile-time dependency from archie-core onto either. - Splits AConditionsPlatform: register()/codec() (runtime condition registration, needed by Archie.kt) stay in archie-core; withCondition()/ fabricRecipeProvider() (datagen-only, reference the datagen-only ARecipeProvider type) move to a new ADatagenConditionsPlatform in archie-datagen. - Moves conditions/ingredients/ACommonTags to archie-core (Archie.kt calls ABuiltinConditions.init()/ABuiltinIngredients.init()/ACommonTags.init() directly), and data/internal + gametest/internal (Archie's own dogfooded datagen providers and self-test GameTest suite) to their respective new modules. - Widens ComposeIdleAware/ComposeTestClockOverride from internal to public in archie-core so archie-gametest's client harness can reach them across the module boundary. Verified: full `./gradlew build -x test` succeeds across all 9 modules (archie-core/-datagen/-gametest x common/fabric/neoforge). Co-Authored-By: Claude Sonnet 5 --- .../net/kernelpanicsoft/archie/Archie.kt | 10 +- .../archie/data/ADataGenerator.kt | 60 - .../conditions/AConditionsPlatform.common.kt | 5 +- .../archie/events/ABasicEventObject.kt | 2 +- .../archie/events/AEventObject.kt | 22 +- .../gametest/AGameTestPlatform.common.kt | 21 +- .../archie/gui/ComposeScreen.kt | 4 +- ...rm.kt => ADataGeneratorPlatform.fabric.kt} | 0 ...tform.kt => AConditionsPlatform.fabric.kt} | 68 +- ...kt => ACustomIngredientPlatform.fabric.kt} | 0 ...tomIngredientSerializerPlatform.fabric.kt} | 0 .../gametest/AGameTestPlatformInternal.kt | 8 +- ....kt => ADataGeneratorPlatform.neoforge.kt} | 0 .../common/conditions/AConditionsPlatform.kt | 113 -- .../AConditionsPlatform.neoforge.kt | 112 ++ ... => ACustomIngredientPlatform.neoforge.kt} | 0 ...mIngredientSerializerPlatform.neoforge.kt} | 0 Archie-Core/datagen/common/build.gradle.kts | 37 + .../archie/data/ADataGenerator.kt | 322 ++++ .../archie/data/ADatagenEventObject.kt | 20 + .../archie/data/IADataProvider.kt | 41 + .../archie/data/client/ALanguageProvider.kt | 154 ++ .../data/client/model/ABlockModelBuilder.kt | 8 + .../data/client/model/ABlockModelProvider.kt | 11 + .../data/client/model/ABlockStateProvider.kt | 1583 ++++++++++++++++ .../data/client/model/AConfiguredModel.kt | 242 +++ .../data/client/model/ACustomLoaderBuilder.kt | 84 + .../data/client/model/AItemModelBuilder.kt | 88 + .../data/client/model/AItemModelProvider.kt | 30 + .../archie/data/client/model/AModelBuilder.kt | 1386 ++++++++++++++ .../archie/data/client/model/AModelFile.kt | 15 + .../data/client/model/AModelProvider.kt | 498 +++++ .../model/AMultiPartBlockStateBuilder.kt | 271 +++ .../client/model/AVariantBlockStateBuilder.kt | 328 ++++ .../client/model/IAGeneratedBlockState.kt | 10 + .../common/conditions/AConditionBuilder.kt | 62 + .../ADatagenConditionsPlatform.common.kt | 20 + .../data/common/conditions/Extensions.kt | 18 + .../data/common/crafting/ARecipeProvider.kt | 109 ++ .../recipies/ArchieCookingRecipeBuilder.kt | 109 ++ .../recipies/ArchieShapedRecipeBuilder.kt | 133 ++ .../recipies/ArchieShapelessRecipeBuilder.kt | 111 ++ .../crafting/recipies/IARecipeBuilder.kt | 21 + .../archie/data/common/tags/ATagBuilder.kt | 169 ++ .../common/tags/ATagBuilderPlatform.common.kt | 14 + .../archie/data/common/tags/ATagsProvider.kt | 423 +++++ .../archie/data/common/tags/IATagBuilder.kt | 155 ++ .../archie/data/internal/ArchieDatagen.kt | 39 + .../data/internal/DatagenArchieExtension.kt | 12 + .../common/tags/AInternalBiomeTagsProvider.kt | 349 ++++ .../common/tags/AInternalBlockTagsProvider.kt | 382 ++++ .../tags/AInternalEntityTypeTagsProvider.kt | 43 + .../common/tags/AInternalFluidTagsProvider.kt | 46 + .../common/tags/AInternalItemTagsProvider.kt | 654 +++++++ .../archie/data/util/TransformationHelper.kt | 396 ++++ .../archie/events/ADatagenEvents.kt | 68 + ...net.kernelpanicsoft.archie.ArchieExtension | 1 + Archie-Core/datagen/fabric/build.gradle.kts | 105 ++ .../fabric/FabricDataGenHelperMixin.java | 32 + .../archie/data/ADataGeneratorFabric.kt | 22 + .../data/ADataGeneratorPlatformInternal.kt | 66 + .../ADatagenConditionsPlatform.fabric.kt | 58 + .../data/common/tags/ATagBuilderPlatform.kt | 23 + .../main/resources/archie_datagen.mixins.json | 12 + .../fabric/src/main/resources/fabric.mod.json | 26 + Archie-Core/datagen/neoforge/build.gradle.kts | 102 + .../datagen/neoforge/gradle.properties | 1 + .../mixin/neoforge/DatagenModLoaderMixin.java | 31 + .../archie/data/ADataGeneratorNeoForge.kt | 17 + .../data/ADataGeneratorPlatformInternal.kt | 31 + .../ADatagenConditionsPlatform.neoforge.kt | 47 + .../data/common/tags/ATagBuilderPlatform.kt | 19 + .../resources/META-INF/neoforge.mods.toml | 38 + .../main/resources/archie_datagen.mixins.json | 12 + Archie-Core/gametest/common/build.gradle.kts | 40 + .../archie/events/AGametestEvents.kt} | 71 +- .../archie/gametest/AClientGameTestHarness.kt | 1653 +++++++++++++++++ .../archie/gametest/AGameTestEventObject.kt | 20 + .../gametest/ComposeScreenTestContext.kt | 286 +++ .../archie/gametest/GameTestAssertions.kt | 50 + .../archie/gametest/NoOpGameTest.kt | 21 + .../archie/gametest/ScreenshotComparer.kt | 101 + .../gametest/ScreenshotComparisonAlgorithm.kt | 211 +++ .../archie/gametest/ScreenshotManager.kt | 54 + .../archie/gametest/VerboseTestReporter.kt | 30 + .../gametest/internal/ArchieGameTest.kt | 49 + .../internal/GametestArchieExtension.kt | 12 + .../internal/tests/ArchieItemHandlerTests.kt | 77 + .../tests/BlockEntityNBTHolderTests.kt | 127 ++ .../tests/BlockEntityStateManagerTests.kt | 86 + .../internal/tests/ComposeRenderingTests.kt | 102 + .../internal/tests/InputComponentsGameTest.kt | 303 +++ .../tests/LayoutComponentsGameTest.kt | 211 +++ .../internal/tests/ModalComponentsGameTest.kt | 210 +++ .../gametest/junit/GameTestGradleExecutor.kt | 327 ++++ .../junit/GameTestGradleInvocation.kt | 60 + .../archie/gametest/junit/GameTestRunner.kt | 159 ++ ...net.kernelpanicsoft.archie.ArchieExtension | 1 + Archie-Core/gametest/fabric/build.gradle.kts | 103 + .../fabric/FabricGameTestHelperMixin.java | 16 + .../FabricGameTestModInitializerMixin.java | 27 + .../lifecycle/MinecraftClientMixin.java | 32 + .../AGameTestClientHarnessInternal.kt | 50 + .../gametest/AGameTestRegistrationBridge.kt | 63 + .../resources/archie_gametest.mixins.json | 16 + .../fabric/src/main/resources/fabric.mod.json | 26 + .../gametest/neoforge/build.gradle.kts | 110 ++ .../gametest/neoforge/gradle.properties | 1 + .../mixin/neoforge/GameTestHooksMixin.java | 64 + .../lifecycle/MinecraftClientMixin.java | 31 + .../AGameTestClientHarnessInternal.kt | 50 + .../gametest/AGameTestRegistrationBridge.kt | 61 + .../resources/META-INF/neoforge.mods.toml | 38 + .../resources/archie_gametest.mixins.json | 15 + Archie-Core/settings.gradle.kts | 8 + 115 files changed, 14200 insertions(+), 301 deletions(-) delete mode 100644 Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt rename Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/{ADataGeneratorPlatform.kt => ADataGeneratorPlatform.fabric.kt} (100%) rename Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/{AConditionsPlatform.kt => AConditionsPlatform.fabric.kt} (51%) rename Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/{ACustomIngredientPlatform.kt => ACustomIngredientPlatform.fabric.kt} (100%) rename Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/{ACustomIngredientSerializerPlatform.kt => ACustomIngredientSerializerPlatform.fabric.kt} (100%) rename Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/{ADataGeneratorPlatform.kt => ADataGeneratorPlatform.neoforge.kt} (100%) delete mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt create mode 100644 Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.neoforge.kt rename Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/{ACustomIngredientPlatform.kt => ACustomIngredientPlatform.neoforge.kt} (100%) rename Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/{ACustomIngredientSerializerPlatform.kt => ACustomIngredientSerializerPlatform.neoforge.kt} (100%) create mode 100644 Archie-Core/datagen/common/build.gradle.kts create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/IADataProvider.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/ALanguageProvider.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelBuilder.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelProvider.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockStateProvider.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AConfiguredModel.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ACustomLoaderBuilder.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelBuilder.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelProvider.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelFile.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelProvider.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AMultiPartBlockStateBuilder.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AVariantBlockStateBuilder.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/IAGeneratedBlockState.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.common.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieCookingRecipeBuilder.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapedRecipeBuilder.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapelessRecipeBuilder.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilder.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.common.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagsProvider.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/IATagBuilder.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/DatagenArchieExtension.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt create mode 100644 Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ADatagenEvents.kt create mode 100644 Archie-Core/datagen/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension create mode 100644 Archie-Core/datagen/fabric/build.gradle.kts create mode 100644 Archie-Core/datagen/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java create mode 100644 Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorFabric.kt create mode 100644 Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt create mode 100644 Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.fabric.kt create mode 100644 Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt create mode 100644 Archie-Core/datagen/fabric/src/main/resources/archie_datagen.mixins.json create mode 100644 Archie-Core/datagen/fabric/src/main/resources/fabric.mod.json create mode 100644 Archie-Core/datagen/neoforge/build.gradle.kts create mode 100644 Archie-Core/datagen/neoforge/gradle.properties create mode 100644 Archie-Core/datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java create mode 100644 Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorNeoForge.kt create mode 100644 Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt create mode 100644 Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.neoforge.kt create mode 100644 Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt create mode 100644 Archie-Core/datagen/neoforge/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 Archie-Core/datagen/neoforge/src/main/resources/archie_datagen.mixins.json create mode 100644 Archie-Core/gametest/common/build.gradle.kts rename Archie-Core/{core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEvents.kt => gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AGametestEvents.kt} (64%) create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ComposeScreenTestContext.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparer.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparisonAlgorithm.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotManager.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/VerboseTestReporter.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/GametestArchieExtension.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ComposeRenderingTests.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/InputComponentsGameTest.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/LayoutComponentsGameTest.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ModalComponentsGameTest.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt create mode 100644 Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt create mode 100644 Archie-Core/gametest/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension create mode 100644 Archie-Core/gametest/fabric/build.gradle.kts create mode 100644 Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestHelperMixin.java create mode 100644 Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java create mode 100644 Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/lifecycle/MinecraftClientMixin.java create mode 100644 Archie-Core/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt create mode 100644 Archie-Core/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt create mode 100644 Archie-Core/gametest/fabric/src/main/resources/archie_gametest.mixins.json create mode 100644 Archie-Core/gametest/fabric/src/main/resources/fabric.mod.json create mode 100644 Archie-Core/gametest/neoforge/build.gradle.kts create mode 100644 Archie-Core/gametest/neoforge/gradle.properties create mode 100644 Archie-Core/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java create mode 100644 Archie-Core/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/lifecycle/MinecraftClientMixin.java create mode 100644 Archie-Core/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt create mode 100644 Archie-Core/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt create mode 100644 Archie-Core/gametest/neoforge/src/main/resources/META-INF/neoforge.mods.toml create mode 100644 Archie-Core/gametest/neoforge/src/main/resources/archie_gametest.mixins.json diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt index fbf99e603..590dc67d8 100644 --- a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt @@ -14,7 +14,6 @@ import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform import net.kernelpanicsoft.archie.data.common.conditions.ABuiltinConditions import net.kernelpanicsoft.archie.data.common.crafting.ingredients.ABuiltinIngredients import net.kernelpanicsoft.archie.data.common.tags.ACommonTags -import net.kernelpanicsoft.archie.events.AEvents import net.kernelpanicsoft.archie.gametest.AGameTestPlatform import net.kernelpanicsoft.archie.gametest.AGameTestSide import net.kernelpanicsoft.archie.gametest.ThreadingImpl @@ -55,10 +54,10 @@ object Archie /** * Initializes Archie's shared (loader-independent) systems. * - * Registers Archie with [AEvents], wires up networking (skipped only for a server-only - * gametest run, since Architectury's networking registration touches client-only classes), - * initializes block entity state syncing, built-in data providers, and Archie's own config, - * and activates the datagen/gametest code paths when running under those tasks. + * Wires up networking (skipped only for a server-only gametest run, since Architectury's + * networking registration touches client-only classes), initializes block entity state + * syncing, built-in data providers, and Archie's own config, and activates the datagen/ + * gametest code paths when running under those tasks. * * @throws IllegalStateException if running on LexForge, which is not supported. */ @@ -68,7 +67,6 @@ object Archie if (Platform.isMinecraftForge()) error("LexForge is not supported. Switch to NeoForge, or don't use my mods.") - AEvents += MOD if (!AGameTestPlatform.isGameTest || AGameTestPlatform.side == AGameTestSide.CLIENT) { ArchieNetworkChannel.init() diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt deleted file mode 100644 index 0fe4bf2d5..000000000 --- a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt +++ /dev/null @@ -1,60 +0,0 @@ -package net.kernelpanicsoft.archie.data - -import dev.architectury.platform.Mod -import net.minecraft.core.HolderLookup -import net.minecraft.data.DataProvider -import net.minecraft.data.PackOutput -import java.util.concurrent.CompletableFuture - -/** - * Base class for a platform's datagen entrypoint. Trimmed to the minimal surface `archie-core` - * needs (just enough for [net.kernelpanicsoft.archie.events.AEvents]'s `GatherDataHandler` to - * reference it as a type) - the full `client { }`/`common { }` provider DSL (models, languages, - * tags, recipes) lives in `archie-datagen` as extension functions/classes on this type, since it - * pulls in the whole datagen provider graph. See `archie-datagen`'s `ADataGenerator` extensions. - */ -@Suppress("MemberVisibilityCanBePrivate", "unused") -abstract class ADataGenerator -{ - /** Whether client-only providers should run, from the `archie.datagen.client` system property. */ - val isClient: Boolean - get() = System.getProperty("archie.datagen.client").toBoolean() - - /** Whether server-only providers should run, from the `archie.datagen.server` system property. */ - val isServer: Boolean - get() = System.getProperty("archie.datagen.server").toBoolean() - - abstract val mod: Mod - - /** - * Registers [factory] with the underlying platform data generator, running it only when - * [run] is `true`, and returns the constructed provider so it can be reused (e.g. an item - * tags provider depending on a previously created block tags provider). - */ - abstract fun addProvider( - run: Boolean = true, - factory: ARegistryAwareDataProviderFactory - ): T - - /** [addProvider] overload for providers that don't need access to [HolderLookup.Provider]. */ - fun addProvider(run: Boolean = true, factory: ADataProviderFactory): T - { - return addProvider(run) { output, _ -> - factory(output) - } - } - - operator fun invoke(block: ADataGenerator.() -> Unit) = apply(block) - - /** Factory for a [DataProvider] that only needs a [PackOutput] to be constructed. */ - fun interface ADataProviderFactory - { - operator fun invoke(output: PackOutput): T - } - - /** Factory for a [DataProvider] that also needs the registry [HolderLookup.Provider] future. */ - fun interface ARegistryAwareDataProviderFactory - { - operator fun invoke(output: PackOutput, registries: CompletableFuture): T - } -} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt index c660e024e..509503761 100644 --- a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt @@ -8,9 +8,8 @@ import net.minecraft.resources.ResourceLocation * Cross-loader hooks that plug [IACondition] into each loader's native datapack condition * system, since Fabric and NeoForge each have their own recipe/tag condition machinery. * - * Trimmed to the runtime-needed half (backs [IACondition.register]/[IACondition.CODEC], evaluated - * whenever a datapack loads a condition) - `withCondition`/`fabricRecipeProvider` (used only by the - * recipe-datagen DSL) live in `archie-datagen` instead. + * Datagen-only condition plumbing (attaching a condition to a generated recipe) lives separately + * in archie-datagen's `ADatagenConditionsPlatform`, since it needs no runtime presence. */ expect object AConditionsPlatform { diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt index 6556c99e8..e53aa4518 100644 --- a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt @@ -5,7 +5,7 @@ import dev.architectury.event.Event /** * A simpler alternative to [AEventObject] for wrapping an Architectury [event] that isn't * scoped to a particular [dev.architectury.platform.Mod] and doesn't need a - * [AEvents.HandlerConstructor]. + * [HandlerConstructor]. * * @param T The Architectury handler/listener type expected by [event]. */ diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt index 5ae408e7f..37a2282d6 100644 --- a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt @@ -3,20 +3,30 @@ package net.kernelpanicsoft.archie.events import dev.architectury.event.Event import dev.architectury.platform.Mod +/** Marker for a mod-scoped Architectury event listener created by a [HandlerConstructor]. */ +interface Handler + +/** Builds a mod-scoped [H] whose body invokes `block` on the event's [T] payload. */ +fun interface HandlerConstructor> +{ + /** Creates an [H] for [mod] that runs [block] against the [T] payload when invoked. */ + fun create(mod: Mod, block: T.() -> Unit): H +} + /** - * Base class for Archie's mod-scoped Architectury event wrappers, such as - * [AEvents.GatherDataHandler] and [AEvents.RegisterGameTestHandler]. + * Base class for a mod-scoped Architectury event wrapper (e.g. `archie-datagen`'s + * `GatherDataHandler`, `archie-gametest`'s `RegisterGameTestHandler`). * * Subclasses wire together an Architectury [event], the [handlerConstructor] that builds a * [mod]-scoped [H] from a [T] callback, and the [handler] logic itself, then call [init] once * (idempotently, thread-safely) to register with the underlying event. * * @param T The event payload/receiver type passed to [handler]. - * @param H The [AEvents.Handler] type produced for [mod]. - * @param C The [AEvents.HandlerConstructor] that builds an [H]. + * @param H The [Handler] type produced for [mod]. + * @param C The [HandlerConstructor] that builds an [H]. * @param mod The [Mod] this event object is scoped to. */ -abstract class AEventObject, C : AEvents.HandlerConstructor>(val mod: Mod) +abstract class AEventObject, C : HandlerConstructor>(val mod: Mod) { /** The underlying Architectury event this wrapper registers a handler with. */ abstract val event: Event @@ -44,4 +54,4 @@ abstract class AEventObject, C : AEvents.HandlerConstr initialized = true } } -} \ No newline at end of file +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt index e86cc366f..e903142f3 100644 --- a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt @@ -32,19 +32,20 @@ expect object AGameTestPlatform } /** - * Restricts which [AEvents.REGISTER_GAME_TEST]-registered mods a single `runGametest`/ + * Restricts which `AGametestEvents.REGISTER_GAME_TEST`-registered mods a single `runGametest`/ * `runGametestClient` invocation actually runs, via the [GAMETEST_MOD_ID_FILTER_PROPERTY] * system property (a comma-separated list of mod ids). * - * Every mod that has called `AEvents += MOD` shares one JVM-wide [AEvents.MODS] list - which - * matters because a composite build's included builds can *both* end up in that list within - * the same process. Archie-Test's Loom `runs{}` blocks `includeBuild("../Archie")`, and both - * `Archie` and `ArchieTest`'s mod init call `AEvents += MOD`, so launching Archie-Test's own - * `runGametestClient`/`runGametest` previously ran Archie's *entire* GameTest suite a second - * time in the same process, without Archie-Test's own suite being any bigger - only - * distinguishable by the test count not matching the log's actual line count. Every loader's - * `AGameTestPlatformInternal`/`AClientGameTestHarness` server- and client-side test collection - * should call [selectMods] on [AEvents.MODS] before iterating, instead of iterating it directly. + * Every mod that has called `AGametestEvents += MOD` (in `archie-gametest`) shares one JVM-wide + * `MODS` list - which matters because a composite build's included builds can *both* end up in + * that list within the same process. Archie-Test's Loom `runs{}` blocks + * `includeBuild("../Archie")`, and both `Archie` and `ArchieTest`'s mod init call + * `AGametestEvents += MOD`, so launching Archie-Test's own `runGametestClient`/`runGametest` + * previously ran Archie's *entire* GameTest suite a second time in the same process, without + * Archie-Test's own suite being any bigger - only distinguishable by the test count not matching + * the log's actual line count. Every loader's `AGameTestRegistrationBridge`/ + * `AClientGameTestHarness` server- and client-side test collection should call [selectMods] on + * `AGametestEvents.MODS` before iterating, instead of iterating it directly. */ object AGameTestModFilter { private const val GAMETEST_MOD_ID_FILTER_PROPERTY = "archie.gametest.modid" diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt index 9a27044e3..e4ebdbfe7 100644 --- a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt +++ b/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt @@ -31,7 +31,7 @@ val LocalScreen: ProvidableCompositionLocal = * poll for a settled frame (no pending or in-flight recomposition) before asserting on rendered * output - e.g. before taking a screenshot right after simulating a click. */ -internal interface ComposeIdleAware { +interface ComposeIdleAware { /** `true` when there is no snapshot-write notification, frame request, or recompose job pending. */ fun isComposeIdle(): Boolean } @@ -63,7 +63,7 @@ interface LayerManagerProvider * `AGameTestPlatform.isGameTest`) construct the actual `StandardTestDispatcher`/ * `TestCoroutineScheduler` instances installed here. */ -internal object ComposeTestClockOverride { +object ComposeTestClockOverride { /** The dispatcher to back new [ComposeScreen]s' coroutine scope with, in place of [kotlinx.coroutines.Dispatchers.Default]. */ @Volatile var dispatcher: CoroutineDispatcher? = null diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.kt rename to Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.fabric.kt similarity index 51% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt rename to Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.fabric.kt index 94a42dcdf..65b29dda6 100644 --- a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.fabric.kt @@ -22,8 +22,8 @@ import net.minecraft.resources.ResourceLocation */ actual object AConditionsPlatform { - /** Fabric condition types registered via [register], keyed by their [IACondition.identifier]. */ - private val registry: MutableMap> = mutableMapOf() + /** Fabric condition types registered via [register], keyed by their [IACondition.identifier]. Public so archie-datagen's `ADatagenConditionsPlatform` can wrap conditions too. */ + val registry: MutableMap> = mutableMapOf() /** Registers [identifier] as a Fabric [ResourceConditionType], backed by [codec] via the [FabricCondition] wrapper. */ actual fun register(identifier: ResourceLocation, codec: MapCodec) @@ -48,46 +48,46 @@ actual object AConditionsPlatform } ) } +} - /** Wraps this condition as a Fabric [ResourceCondition]. */ - val IACondition.fabric - get() = FabricCondition(this) +/** Wraps this condition as a Fabric [ResourceCondition]. Public so archie-datagen can attach conditions to generated recipes. */ +val IACondition.fabric: FabricCondition + get() = FabricCondition(this) - /** Unwraps a Fabric [ResourceCondition] back to its originating [IACondition]. Throws if it wasn't created via [fabric]. */ - val ResourceCondition.archie - get() = ((this as? FabricCondition) ?: throw AssertionError()).condition +/** Unwraps a Fabric [ResourceCondition] back to its originating [IACondition]. Throws if it wasn't created via [fabric]. */ +val ResourceCondition.archie: IACondition + get() = ((this as? FabricCondition) ?: throw AssertionError()).condition - /** Adapts an [IACondition] to Fabric's [ResourceCondition] interface, delegating [getType] and [test] to it. */ - class FabricCondition( - val condition: IACondition - ) : ResourceCondition +/** Adapts an [IACondition] to Fabric's [ResourceCondition] interface, delegating [getType] and [test] to it. */ +class FabricCondition( + val condition: IACondition +) : ResourceCondition +{ + override fun getType(): ResourceConditionType<*> { - override fun getType(): ResourceConditionType<*> - { - return registry[condition.identifier]!! - } + return AConditionsPlatform.registry[condition.identifier]!! + } - override fun test(registryLookup: HolderLookup.Provider?): Boolean - { - return registryLookup?.let { - condition.test(ConditionContext(it)) - } ?: false - } + override fun test(registryLookup: HolderLookup.Provider?): Boolean + { + return registryLookup?.let { + condition.test(ConditionContext(it)) + } ?: false } +} - /** [IACondition.IContext] backed directly by a Fabric registry lookup, used when Fabric evaluates a condition. */ - class ConditionContext(private val registryLookup: HolderLookup.Provider) : - IACondition.IContext +/** [IACondition.IContext] backed directly by a Fabric registry lookup, used when Fabric evaluates a condition. */ +class ConditionContext(private val registryLookup: HolderLookup.Provider) : + IACondition.IContext +{ + override fun getAllTags(registry: ResourceKey>): Map>> { - override fun getAllTags(registry: ResourceKey>): Map>> - { - return registryLookup.lookupOrThrow(registry).listTags().toList() - .associateBy({ it.key().location }, { it.toList() }) - } + return registryLookup.lookupOrThrow(registry).listTags().toList() + .associateBy({ it.key().location }, { it.toList() }) + } - override fun getRegistry(registry: ResourceKey>): Registry - { - return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY).registryOrThrow(registry) - } + override fun getRegistry(registry: ResourceKey>): Registry + { + return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY).registryOrThrow(registry) } } diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.fabric.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.kt rename to Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.fabric.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.fabric.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.kt rename to Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.fabric.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt index 5da3f7e18..cc1535a42 100644 --- a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt +++ b/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt @@ -5,10 +5,10 @@ import dev.architectury.platform.Mod /** * Backs [AGameTestPlatform] on Fabric: holds registered test classes. * - * Trimmed to the [testClasses] map [AGameTestPlatform.register] needs - the actual - * `registerGameTests()` driving logic (firing [net.kernelpanicsoft.archie.events.AEvents - * .REGISTER_GAME_TEST], wiring `GameTestRegistry`/`FabricGameTestModInitializerMixin`) is - * gametest-run-only and lives in `archie-gametest` instead. + * Trimmed to the [testClasses] map [AGameTestPlatform.register] needs - the actual driving logic + * (firing `AGametestEvents.REGISTER_GAME_TEST`, wiring `GameTestRegistry`/ + * `FabricGameTestModInitializerMixin`) is gametest-run-only and lives in `archie-gametest`'s + * `AGameTestRegistrationBridge` instead. */ internal object AGameTestPlatformInternal { diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.kt rename to Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt deleted file mode 100644 index 0419713ee..000000000 --- a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.kt +++ /dev/null @@ -1,113 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.mojang.serialization.* -import dev.nyon.klf.MOD_BUS -import net.minecraft.core.Holder -import net.minecraft.core.HolderLookup -import net.minecraft.core.Registry -import net.minecraft.core.RegistryAccess -import net.minecraft.core.registries.BuiltInRegistries -import net.minecraft.resources.ResourceKey -import net.minecraft.resources.ResourceLocation -import net.neoforged.neoforge.common.conditions.ICondition -import net.neoforged.neoforge.registries.DeferredRegister -import net.neoforged.neoforge.registries.NeoForgeRegistries -import java.util.stream.Stream -import net.kernelpanicsoft.archie.data.common.conditions.IACondition as ArchieCondition - -/** - * NeoForge implementation of [AConditionsPlatform], adapting Archie's platform-neutral - * [ArchieCondition] onto NeoForge's `ICondition` API. - * - * Every [ArchieCondition] is wrapped as a [NeoForgeCondition] to cross into NeoForge's condition - * system, and unwrapped again via [ICondition.archie] when Archie code needs the original back. - */ -actual object AConditionsPlatform -{ - /** Registers [identifier] as a NeoForge condition serializer, backed by [codec] via the [NeoForgeConditionCodec] wrapper. */ - actual fun register(identifier: ResourceLocation, codec: MapCodec) - { - val registry = DeferredRegister.create(NeoForgeRegistries.CONDITION_SERIALIZERS, identifier.namespace) - registry.register(identifier.path) { _ -> - codec.neoforge - } - registry.register(MOD_BUS) - } - - /** Codec for [ArchieCondition] backed by `ICondition.CODEC`, round-tripping through [neoforge]/[archie]. */ - actual fun codec(): Codec - { - return ICondition.CODEC.xmap({ - it.archie - }, { - it.neoforge - }) - } - - /** Unwraps a NeoForge [ICondition] back to its originating [ArchieCondition]. Throws if it wasn't created via [neoforge]. */ - val ICondition.archie - get() = ((this as? NeoForgeCondition) ?: throw AssertionError()).condition - - /** Wraps this condition as a NeoForge [ICondition]. */ - val ArchieCondition.neoforge - get() = NeoForgeCondition(this) - - /** Wraps this codec as a NeoForge condition-serializer codec. */ - val MapCodec.neoforge - get() = NeoForgeConditionCodec(this) - - /** Adapts an [ArchieCondition] to NeoForge's [ICondition] interface, delegating [test] to it and [codec] to the registered serializer. */ - class NeoForgeCondition( - val condition: ArchieCondition - ) : ICondition - { - override fun test(iContext: ICondition.IContext): Boolean - { - return condition.test(object : ArchieCondition.IContext - { - override fun getAllTags(registry: ResourceKey>): Map>> - { - return iContext.getAllTags(registry) - } - - override fun getRegistry(registry: ResourceKey>): Registry - { - return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY).registryOrThrow(registry) - } - }) - } - - override fun codec(): MapCodec - { - return NeoForgeRegistries.CONDITION_SERIALIZERS[condition.identifier]!! - } - } - - /** Adapts a [MapCodec] of [ArchieCondition] to one producing/consuming [NeoForgeCondition] wrappers. */ - class NeoForgeConditionCodec( - private val codec: MapCodec - ) : MapCodec() - { - override fun encode( - input: NeoForgeCondition, - ops: DynamicOps, - prefix: RecordBuilder - ): RecordBuilder - { - @Suppress("UNCHECKED_CAST") - return (codec as MapCodec).encode(input.condition, ops, prefix) - } - - override fun keys(ops: DynamicOps): Stream - { - return codec.keys(ops) - } - - override fun decode(ops: DynamicOps, input: MapLike): DataResult - { - return codec.decode(ops, input).map { result -> - result.neoforge - } - } - } -} diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.neoforge.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.neoforge.kt new file mode 100644 index 000000000..3866573e5 --- /dev/null +++ b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.neoforge.kt @@ -0,0 +1,112 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import com.mojang.serialization.* +import dev.nyon.klf.MOD_BUS +import net.minecraft.core.Holder +import net.minecraft.core.RegistryAccess +import net.minecraft.core.Registry +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.resources.ResourceKey +import net.minecraft.resources.ResourceLocation +import net.neoforged.neoforge.common.conditions.ICondition +import net.neoforged.neoforge.registries.DeferredRegister +import net.neoforged.neoforge.registries.NeoForgeRegistries +import java.util.stream.Stream +import net.kernelpanicsoft.archie.data.common.conditions.IACondition as ArchieCondition + +/** + * NeoForge implementation of [AConditionsPlatform], adapting Archie's platform-neutral + * [ArchieCondition] onto NeoForge's `ICondition` API. + * + * Every [ArchieCondition] is wrapped as a [NeoForgeCondition] to cross into NeoForge's condition + * system, and unwrapped again via [ICondition.archie] when Archie code needs the original back. + */ +actual object AConditionsPlatform +{ + /** Registers [identifier] as a NeoForge condition serializer, backed by [codec] via the [NeoForgeConditionCodec] wrapper. */ + actual fun register(identifier: ResourceLocation, codec: MapCodec) + { + val registry = DeferredRegister.create(NeoForgeRegistries.CONDITION_SERIALIZERS, identifier.namespace) + registry.register(identifier.path) { _ -> + codec.neoforge + } + registry.register(MOD_BUS) + } + + /** Codec for [ArchieCondition] backed by `ICondition.CODEC`, round-tripping through [neoforge]/[archie]. */ + actual fun codec(): Codec + { + return ICondition.CODEC.xmap({ + it.archie + }, { + it.neoforge + }) + } +} + +/** Unwraps a NeoForge [ICondition] back to its originating [ArchieCondition]. Throws if it wasn't created via [neoforge]. */ +val ICondition.archie: ArchieCondition + get() = ((this as? NeoForgeCondition) ?: throw AssertionError()).condition + +/** Wraps this condition as a NeoForge [ICondition]. Public so archie-datagen can attach conditions to generated recipes. */ +val ArchieCondition.neoforge: NeoForgeCondition + get() = NeoForgeCondition(this) + +/** Wraps this codec as a NeoForge condition-serializer codec. */ +val MapCodec.neoforge: MapCodec + get() = NeoForgeConditionCodec(this) + +/** Adapts an [ArchieCondition] to NeoForge's [ICondition] interface, delegating [test] to it and [codec] to the registered serializer. */ +class NeoForgeCondition( + val condition: ArchieCondition +) : ICondition +{ + override fun test(iContext: ICondition.IContext): Boolean + { + return condition.test(object : ArchieCondition.IContext + { + override fun getAllTags(registry: ResourceKey>): Map>> + { + return iContext.getAllTags(registry) + } + + override fun getRegistry(registry: ResourceKey>): Registry + { + return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY).registryOrThrow(registry) + } + }) + } + + override fun codec(): MapCodec + { + return NeoForgeRegistries.CONDITION_SERIALIZERS[condition.identifier]!! + } +} + +/** Adapts a [MapCodec] of [ArchieCondition] to one producing/consuming [NeoForgeCondition] wrappers. */ +class NeoForgeConditionCodec( + private val codec: MapCodec +) : MapCodec() +{ + override fun encode( + input: NeoForgeCondition, + ops: DynamicOps, + prefix: RecordBuilder + ): RecordBuilder + { + @Suppress("UNCHECKED_CAST") + return (codec as MapCodec).encode(input.condition, ops, prefix) + } + + override fun keys(ops: DynamicOps): Stream + { + return codec.keys(ops) + } + + override fun decode(ops: DynamicOps, input: MapLike): DataResult + { + return codec.decode(ops, input).map { result -> + result.neoforge + } + } +} diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.neoforge.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.kt rename to Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.neoforge.kt diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.kt b/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.neoforge.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.kt rename to Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.neoforge.kt diff --git a/Archie-Core/datagen/common/build.gradle.kts b/Archie-Core/datagen/common/build.gradle.kts new file mode 100644 index 000000000..086363cd1 --- /dev/null +++ b/Archie-Core/datagen/common/build.gradle.kts @@ -0,0 +1,37 @@ +architectury { + common("fabric", "neoforge") +} + +actualizer { + stubUnfulfilledExpects() +} + +loom { + accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) +} + +dependencies { + // modApi, not plain api - the architectury transformer needs archie-core-common as a tracked + // mod dependency to resolve its own classes (e.g. gui types referenced by datagen providers). + modApi(project(":archie-core-common")) + modApi(libs.architectury.common) + + compileOnly(kotlin("reflect")) + implementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.api) + testImplementation(kotlin("reflect")) + testRuntimeOnly(libs.junit.jupiter.engine) +} + +tasks { + base.archivesName.set(base.archivesName.get() + "-datagen-common") + + jar { + from(sourceSets.main.get().output) + exclude("**/*StubKt.class") + } + + sourcesJar { + exclude("**/*Stub.kt") + } +} diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt new file mode 100644 index 000000000..b8aa1dd8c --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt @@ -0,0 +1,322 @@ +package net.kernelpanicsoft.archie.data + +import net.kernelpanicsoft.archie.data.client.ALanguageProvider +import net.kernelpanicsoft.archie.data.client.model.ABlockModelProvider +import net.kernelpanicsoft.archie.data.client.model.ABlockStateProvider +import net.kernelpanicsoft.archie.data.client.model.AItemModelProvider +import net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider +import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider +import dev.architectury.platform.Mod +import net.minecraft.core.HolderLookup +import net.minecraft.data.DataProvider +import net.minecraft.data.PackOutput +import net.minecraft.data.recipes.RecipeOutput +import java.util.concurrent.CompletableFuture + +/** + * Base class for a platform's datagen entrypoint, providing a small DSL for registering + * [DataProvider]s without needing to interact with architectury's `DataGeneratorPlugin` + * directly. + * + * Providers are grouped by [client] and [common] since client-only providers (e.g. models, + * languages) must be skipped on a dedicated server datagen run and vice versa; [isClient] and + * [isServer] gate whether a provider actually runs based on the `archie.datagen.client`/ + * `archie.datagen.server` system properties set by the datagen run configuration. + * + * Loader modules implement [addProvider] on top of their platform's data generator and then + * invoke this generator, typically as `ArchieDatagen(mod) { client { ... }; common { ... } }`. + */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +abstract class ADataGenerator +{ + /** Whether client-only providers should run, from the `archie.datagen.client` system property. */ + val isClient: Boolean + get() = System.getProperty("archie.datagen.client").toBoolean() + + /** Whether server-only providers should run, from the `archie.datagen.server` system property. */ + val isServer: Boolean + get() = System.getProperty("archie.datagen.server").toBoolean() + + abstract val mod: Mod + + /** + * Registers [factory] with the underlying platform data generator, running it only when + * [run] is `true`, and returns the constructed provider so it can be reused (e.g. an item + * tags provider depending on a previously created block tags provider). + */ + abstract fun addProvider( + run: Boolean = true, + factory: ARegistryAwareDataProviderFactory + ): T + + /** [addProvider] overload for providers that don't need access to [HolderLookup.Provider]. */ + fun addProvider(run: Boolean = true, factory: ADataProviderFactory): T + { + return addProvider(run) { output, _ -> + factory(output) + } + } + + /** Registers client-only providers (models, languages) declared in [block] via [Client]. */ + fun client(block: Client.() -> Unit) + { + Client().apply(block) + } + + /** Registers server-only providers (tags, recipes) declared in [block] via [Common]. */ + fun common(block: Common.() -> Unit) + { + Common().apply(block) + } + + operator fun invoke(block: ADataGenerator.() -> Unit) = apply(block) + + /** Factory for a [DataProvider] that only needs a [PackOutput] to be constructed. */ + fun interface ADataProviderFactory + { + operator fun invoke(output: PackOutput): T + } + + /** Factory for a [DataProvider] that also needs the registry [HolderLookup.Provider] future. */ + fun interface ARegistryAwareDataProviderFactory + { + operator fun invoke(output: PackOutput, registries: CompletableFuture): T + } + + /** Factory for an [ATagsProvider.ItemTagsProvider] that depends on an existing block tags provider. */ + fun interface ItemTagsDataProviderFactory + { + operator fun invoke( + output: PackOutput, + registries: CompletableFuture, + blockTagsProvider: ATagsProvider.BlockTagsProvider + ): ATagsProvider.ItemTagsProvider + } + + /** DSL scope for registering client-side providers; see [ADataGenerator.client]. */ + inner class Client + { + /** Registers an [ALanguageProvider] for [locale] that generates translations in [block]. */ + fun languages(locale: String = "en_us", block: ALanguageProvider.() -> Unit): ALanguageProvider + { + return languages { packOutput -> + object : ALanguageProvider(packOutput, mod, false, locale) + { + override fun generate() + { + this.block() + } + } + } + } + + fun languages(constructor: ADataProviderFactory): ALanguageProvider + { + return addProvider(isClient, constructor) + } + + /** Registers an [ABlockModelProvider] that generates block models in [block]. */ + fun blockModels(block: ABlockModelProvider.() -> Unit): ABlockModelProvider + { + return blockModels { packOutput -> + object : ABlockModelProvider(packOutput, mod, false) + { + override fun generate() + { + this.block() + } + } + } + } + + fun blockModels(constructor: ADataProviderFactory): ABlockModelProvider + { + return addProvider(isClient, constructor) + } + + /** Registers an [AItemModelProvider] that generates item models in [block]. */ + fun itemModels(block: AItemModelProvider.() -> Unit): AItemModelProvider + { + return itemModels { packOutput -> + object : AItemModelProvider(packOutput, mod, false) + { + override fun generate() + { + this.block() + } + } + } + } + + fun itemModels(constructor: ADataProviderFactory): AItemModelProvider + { + return addProvider(isClient, constructor) + } + + /** Registers an [ABlockStateProvider] that generates blockstate JSONs in [block]. */ + fun blockStates(block: ABlockStateProvider.() -> Unit): ABlockStateProvider + { + return blockStates { packOutput -> + object : ABlockStateProvider(packOutput, mod, false) + { + override fun generate() + { + this.block() + } + } + } + } + + fun blockStates(constructor: ADataProviderFactory): ABlockStateProvider + { + return addProvider(isClient, constructor) + } + } + + /** DSL scope for registering server-side providers; see [ADataGenerator.common]. */ + inner class Common + { + /** The block tags provider registered via [blockTags], if any; used by [itemTags] to derive item tags from block tags. */ + lateinit var blockTagsProvider: ATagsProvider.BlockTagsProvider + + /** Registers an [ATagsProvider.BlockTagsProvider] that declares block tags in [block]. */ + fun blockTags(block: ATagsProvider.BlockTagsProvider.(registries: HolderLookup.Provider) -> Unit): ATagsProvider.BlockTagsProvider + { + return blockTags { packOutput, registries -> + object : ATagsProvider.BlockTagsProvider(packOutput, mod, registries, false) + { + override fun generate(registries: HolderLookup.Provider) + { + this.block(registries) + } + } + } + } + + fun blockTags(constructor: ARegistryAwareDataProviderFactory): ATagsProvider.BlockTagsProvider + { + return addProvider(isServer, constructor).also { + blockTagsProvider = it + } + } + + /** + * Registers an [ATagsProvider.ItemTagsProvider] that declares item tags in [block]. + * Constructs it with [blockTagsProvider] when a block tags provider was already + * registered via [blockTags], enabling `copy(blockTag, itemTag)`. + */ + fun itemTags(block: ATagsProvider.ItemTagsProvider.(registries: HolderLookup.Provider) -> Unit): ATagsProvider.ItemTagsProvider + { + return if (::blockTagsProvider.isInitialized) + itemTags { packOutput, registries, blockTagsProvider -> + object : ATagsProvider.ItemTagsProvider(packOutput, mod, registries, blockTagsProvider, false) + { + override fun generate(registries: HolderLookup.Provider) + { + this.block(registries) + } + } + } + else + itemTags { packOutput, registries -> + object : ATagsProvider.ItemTagsProvider(packOutput, mod, registries, false) + { + override fun generate(registries: HolderLookup.Provider) + { + this.block(registries) + } + } + } + } + + fun itemTags(constructor: ARegistryAwareDataProviderFactory): ATagsProvider.ItemTagsProvider + { + return addProvider(isServer, constructor) + } + + fun itemTags(constructor: ItemTagsDataProviderFactory): ATagsProvider.ItemTagsProvider + { + if (!::blockTagsProvider.isInitialized) + throw IllegalStateException("You did not register a block tags provider. you must do that to use this overload") + return addProvider(isServer) { packOutput, registries -> + constructor(packOutput, registries, blockTagsProvider) + } + } + + /** Registers an [ATagsProvider.BiomeTagsProvider] that declares biome tags in [block]. */ + fun biomeTags(block: ATagsProvider.BiomeTagsProvider.(registries: HolderLookup.Provider) -> Unit): ATagsProvider.BiomeTagsProvider + { + return biomeTags { packOutput, registries -> + object : ATagsProvider.BiomeTagsProvider(packOutput, mod, registries, false) + { + override fun generate(registries: HolderLookup.Provider) + { + this.block(registries) + } + } + } + } + + fun biomeTags(constructor: ARegistryAwareDataProviderFactory): ATagsProvider.BiomeTagsProvider + { + return addProvider(isServer, constructor) + } + + /** Registers an [ATagsProvider.EntityTypeTagsProvider] that declares entity type tags in [block]. */ + fun entityTags(block: ATagsProvider.EntityTypeTagsProvider.(registries: HolderLookup.Provider) -> Unit): ATagsProvider.EntityTypeTagsProvider + { + return entityTags { packOutput, registries -> + object : ATagsProvider.EntityTypeTagsProvider(packOutput, mod, registries, false) + { + override fun generate(registries: HolderLookup.Provider) + { + this.block(registries) + } + } + } + } + + fun entityTags(constructor: ARegistryAwareDataProviderFactory): ATagsProvider.EntityTypeTagsProvider + { + return addProvider(isServer, constructor) + } + + /** Registers an [ATagsProvider.FluidTagsProvider] that declares fluid tags in [block]. */ + fun fluidTags(block: ATagsProvider.FluidTagsProvider.(registries: HolderLookup.Provider) -> Unit): ATagsProvider.FluidTagsProvider + { + return fluidTags { packOutput, registries -> + object : ATagsProvider.FluidTagsProvider(packOutput, mod, registries, false) + { + override fun generate(registries: HolderLookup.Provider) + { + this.block(registries) + } + } + } + } + + fun fluidTags(constructor: ARegistryAwareDataProviderFactory): ATagsProvider.FluidTagsProvider + { + return addProvider(isServer, constructor) + } + + /** Registers an [ARecipeProvider] that declares recipes via [block]. */ + fun recipes(block: ARecipeProvider.(recipeOutput: RecipeOutput) -> Unit): ARecipeProvider + { + return addProvider(isServer) { packOutput, registries -> + return@addProvider object : ARecipeProvider(packOutput, mod, registries, false) + { + override fun generate(recipeOutput: RecipeOutput) + { + this.block(recipeOutput) + } + } + } + } + + fun recipes(constructor: ARegistryAwareDataProviderFactory): ARecipeProvider + { + return addProvider(isServer, constructor) + } + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt new file mode 100644 index 000000000..1a91f6c7d --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt @@ -0,0 +1,20 @@ +package net.kernelpanicsoft.archie.data + +import net.kernelpanicsoft.archie.events.ADatagenEvents +import net.kernelpanicsoft.archie.events.ADatagenEvents.GatherDataHandler +import net.kernelpanicsoft.archie.events.AEventObject +import dev.architectury.event.Event +import dev.architectury.platform.Mod + +/** + * Convenience [AEventObject] base for hooking into [ADatagenEvents.GATHER_DATA], the event fired + * by the loader during a datagen run. Implement [handler] to build and run an [ADataGenerator]. + */ +abstract class ADatagenEventObject(mod: Mod) : + AEventObject( + mod + ) +{ + override val event: Event = ADatagenEvents.GATHER_DATA + override val handlerConstructor: GatherDataHandler.Companion = GatherDataHandler.Companion +} diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/IADataProvider.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/IADataProvider.kt new file mode 100644 index 000000000..9aa234512 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/IADataProvider.kt @@ -0,0 +1,41 @@ +package net.kernelpanicsoft.archie.data + +import dev.architectury.platform.Mod +import dev.architectury.platform.Platform +import net.minecraft.data.DataProvider +import net.minecraft.data.PackOutput +import net.minecraft.resources.ResourceLocation + +/** + * Common contract shared by Archie's [DataProvider] implementations, adding the current [mod] + * and a couple of ID/path helpers used when generating output files. + */ +interface IADataProvider : DataProvider +{ + val output: PackOutput + + /** The mod this provider is generating data for. */ + val mod: Mod + + /** Whether datagen should abort with an error instead of logging and continuing. */ + val exitOnError: Boolean + + /** Builds a [ResourceLocation] in [mod]'s namespace, e.g. for output file paths. */ + fun modLoc(name: String): ResourceLocation + { + return ResourceLocation.fromNamespaceAndPath(mod.modId, name) + } + + /** Builds a [ResourceLocation] in the `minecraft` namespace. */ + fun mcLoc(name: String): ResourceLocation + { + return ResourceLocation.withDefaultNamespace(name) + } + + /** + * Formats a provider display [name] (used for `getName()`), prefixing it with [mod]'s name + * on loaders other than Fabric so providers from different mods are distinguishable in + * datagen logs. + */ + fun format(name: String): String = if (Platform.isFabric()) name else "${mod.name}/$name" +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/ALanguageProvider.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/ALanguageProvider.kt new file mode 100644 index 000000000..75213be0c --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/ALanguageProvider.kt @@ -0,0 +1,154 @@ +package net.kernelpanicsoft.archie.data.client + +import com.google.gson.JsonObject +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.IADataProvider +import dev.architectury.platform.Mod +import net.minecraft.data.CachedOutput +import net.minecraft.data.DataProvider +import net.minecraft.data.PackOutput +import net.minecraft.world.effect.MobEffect +import net.minecraft.world.entity.EntityType +import net.minecraft.world.item.Item +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.enchantment.Enchantment +import net.minecraft.world.level.block.Block +import java.nio.file.Path +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.function.Supplier +import kotlin.system.exitProcess + +/** + * Datagen provider that builds a `assets//lang/.json` translation file. + * Implement [generate] and call the `add*` helpers to register translation keys; use via + * [net.kernelpanicsoft.archie.data.ADataGenerator.Client.languages]. + */ +@Suppress("unused") +abstract class ALanguageProvider( + override val output: PackOutput, + override val mod: Mod, + override val exitOnError: Boolean, + private val locale: String = "en_us" +) : + IADataProvider +{ + private val data: MutableMap = TreeMap() + + /** Called once during [run] to register translations via the `add*` helpers. */ + protected abstract fun generate() + + override fun run(cache: CachedOutput): CompletableFuture<*> + { + runCatching { + generate() + }.onFailure { + Archie.LOGGER.error( + "Data Provider $name failed with exception: ${it.message}\n" + + "Stacktrace: ${it.stackTraceToString()}" + ) + if (exitOnError) exitProcess(-1) + } + if (data.isNotEmpty()) return save( + cache, + output.getOutputFolder(PackOutput.Target.RESOURCE_PACK).resolve(this.mod.modId).resolve("lang").resolve( + this.locale + ".json" + ) + ) + + + return CompletableFuture.allOf() + } + + override fun getName(): String = format("Languages - $locale") + + private fun save(cache: CachedOutput, target: Path): CompletableFuture<*> + { + // TODO: DataProvider.saveStable handles the caching and hashing already, but creating the JSON Object this way seems unreliable. -C + val json = JsonObject() + data.forEach { (property: String?, value: String?) -> + json.addProperty( + property, + value + ) + } + + return DataProvider.saveStable(cache, json, target) + } + + /** Translates a deferred [Block] to [name]; see [add]. */ + fun addBlock(name: String, key: Supplier) + { + add(key.get(), name) + } + + /** Translates [key]'s `descriptionId` to [name]; see [add]. */ + fun add(key: Block, name: String) + { + add(key.descriptionId, name) + } + + /** Translates a deferred [Item] to [name]; see [add]. */ + fun addItem(name: String, key: Supplier) + { + add(key.get(), name) + } + + /** Translates [key]'s `descriptionId` to [name]; see [add]. */ + fun add(key: Item, name: String) + { + add(key.descriptionId, name) + } + + /** Translates a deferred [ItemStack] to [name]; see [add]. */ + fun addItemStack(name: String, key: Supplier) + { + add(key.get(), name) + } + + /** Translates [key]'s `descriptionId` to [name]; see [add]. */ + fun add(key: ItemStack, name: String) + { + add(key.descriptionId, name) + } + +// fun addEnchantment(name: String, key: Supplier) +// { +// add(key.get(), name) +// } +// +// fun add(key: Enchantment, name: String) +// { +// add(key.descriptionId, name) +// } + + /** Translates a deferred [MobEffect] to [name]; see [add]. */ + fun addEffect(name: String, key: Supplier) + { + add(key.get(), name) + } + + /** Translates [key]'s `descriptionId` to [name]; see [add]. */ + fun add(key: MobEffect, name: String) + { + add(key.descriptionId, name) + } + + /** Translates a deferred [EntityType] to [name]; see [add]. */ + fun addEntityType(name: String, key: Supplier>) + { + add(key.get(), name) + } + + /** Translates [key]'s `descriptionId` to [name]; see [add]. */ + fun add(key: EntityType<*>, name: String) + { + add(key.descriptionId, name) + } + + /** Registers a raw translation [key] to [value]. Throws if [key] is already registered. */ + fun add(key: String, value: String) + { + check(data.put(key, value) == null) { "Duplicate translation key $key" } + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelBuilder.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelBuilder.kt new file mode 100644 index 000000000..47eb7de99 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelBuilder.kt @@ -0,0 +1,8 @@ +package net.kernelpanicsoft.archie.data.client.model + +import net.minecraft.resources.ResourceLocation + +/** [AModelBuilder] for a block model at [outputLocation], produced by [ABlockModelProvider]. */ +class ABlockModelBuilder( + outputLocation: ResourceLocation +) : AModelBuilder(outputLocation) \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelProvider.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelProvider.kt new file mode 100644 index 000000000..0d57bafe7 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelProvider.kt @@ -0,0 +1,11 @@ +package net.kernelpanicsoft.archie.data.client.model + +import dev.architectury.platform.Mod +import net.minecraft.data.PackOutput + +/** [AModelProvider] that generates block models under `models/block/`. */ +abstract class ABlockModelProvider(output: PackOutput, mod: Mod, exitOnError: Boolean) : + AModelProvider(output, mod, BLOCK_FOLDER, ::ABlockModelBuilder, exitOnError) +{ + override fun getName(): String = format("Block Models") +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockStateProvider.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockStateProvider.kt new file mode 100644 index 000000000..4ad796513 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockStateProvider.kt @@ -0,0 +1,1583 @@ +package net.kernelpanicsoft.archie.data.client.model + +import com.google.common.base.Preconditions +import com.google.gson.* +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.IADataProvider +import dev.architectury.platform.Mod +import net.minecraft.core.Direction +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.data.CachedOutput +import net.minecraft.data.DataProvider +import net.minecraft.data.PackOutput +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.level.block.* +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.level.block.state.properties.* +import org.apache.logging.log4j.LogManager +import org.apache.logging.log4j.Logger +import org.jetbrains.annotations.VisibleForTesting +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer +import java.util.function.Function +import kotlin.system.exitProcess + +/** + * Datagen provider that builds `blockstates`/`*.json` files, along with the block/item models + * they reference via the embedded [blockModels] and [itemModels] providers. + * + * Implement [generate] and, for each block, call either [getVariantBuilder] (for a simple + * `variants` blockstate) or [getMultipartBuilder] (for a `multipart` blockstate), then use the + * `*Block`/`*BlockWithRenderType` helpers (e.g. `stairsBlock`, `slabBlock`, `fenceBlock`, + * `signBlock`) or [simpleBlock] to wire up standard model shapes. These helpers mirror + * NeoForge's vanilla `BlockStateProvider` datagen helpers, so their names/parameters match that + * API 1:1. Use via [net.kernelpanicsoft.archie.data.ADataGenerator.Client.blockStates] + */ +@Suppress("MemberVisibilityCanBePrivate", "unused") +abstract class ABlockStateProvider( + final override val output: PackOutput, + final override val mod: Mod, + final override val exitOnError: Boolean +) : IADataProvider +{ + @VisibleForTesting + protected val registeredBlocks: MutableMap = + LinkedHashMap() + + private val blockModels: ABlockModelProvider = + object : ABlockModelProvider( + output, + mod, + exitOnError + ) + { + override fun run(cache: CachedOutput): CompletableFuture<*> + { + return CompletableFuture.allOf() + } + + override fun generate() = Unit + } + private val itemModels: AItemModelProvider = + object : AItemModelProvider( + output, + mod, + exitOnError + ) + { + override fun run(cache: CachedOutput): CompletableFuture<*> + { + return CompletableFuture.allOf() + } + + override fun generate() = Unit + } + + override fun run(cache: CachedOutput): CompletableFuture<*> + { + blockModels().clear() + itemModels().clear() + registeredBlocks.clear() + runCatching { + generate() + }.onFailure { + Archie.LOGGER.error( + "Data Provider $name failed with exception: ${it.message}\n" + + "Stacktrace: ${it.stackTraceToString()}" + ) + if (exitOnError) exitProcess(-1) + } + val futures: Array?> = arrayOfNulls(2 + registeredBlocks.size) + var i = 0 + futures[i++] = blockModels().generateAll(cache) + futures[i++] = itemModels().generateAll(cache) + for ((key, value) in registeredBlocks) + { + futures[i++] = saveBlockState(cache, value.toJson(), key) + } + return CompletableFuture.allOf(*futures) + } + + /** Called once during [run] to register blockstates via [getVariantBuilder]/[getMultipartBuilder]. */ + protected abstract fun generate() + + /** + * Gets (or creates) the [AVariantBlockStateBuilder] for block [b], applying [block] to it. + * Throws if [b] was already registered with [getMultipartBuilder] instead. + */ + fun getVariantBuilder(b: Block, block: AVariantBlockStateBuilder.() -> Unit = {}): AVariantBlockStateBuilder + { + if (registeredBlocks.containsKey(b)) + { + val old: IAGeneratedBlockState? = registeredBlocks[b] + Preconditions.checkState(old is AVariantBlockStateBuilder) + return (old as AVariantBlockStateBuilder).apply(block) + } else + { + val ret = AVariantBlockStateBuilder(b).apply(block) + registeredBlocks[b] = ret + return ret + } + } + + /** + * Gets (or creates) the [AMultiPartBlockStateBuilder] for block [b], applying [block] to it. + * Throws if [b] was already registered with [getVariantBuilder] instead. + */ + fun getMultipartBuilder(b: Block, block: AMultiPartBlockStateBuilder.() -> Unit = {}): AMultiPartBlockStateBuilder + { + if (registeredBlocks.containsKey(b)) + { + val old: IAGeneratedBlockState? = registeredBlocks[b] + Preconditions.checkState(old is AMultiPartBlockStateBuilder) + return (old as AMultiPartBlockStateBuilder).apply(block) + } else + { + val ret = AMultiPartBlockStateBuilder(b).apply(block) + registeredBlocks[b] = ret + return ret + } + } + + /** Applies [block] to the block model provider embedded in this blockstate provider. */ + fun blockModels(block: ABlockModelProvider.() -> Unit = {}): ABlockModelProvider + { + return runCatching { + blockModels.apply(block) + }.onFailure { + Archie.LOGGER.error( + "Data Provider $name failed with exception: ${it.message}\n" + + "Stacktrace: ${it.stackTraceToString()}" + ) + if (exitOnError) exitProcess(-1) + }.getOrElse { + blockModels.clear() + blockModels + } + } + + /** Applies [block] to the item model provider embedded in this blockstate provider. */ + fun itemModels(block: AItemModelProvider.() -> Unit = {}): AItemModelProvider + { + return runCatching { + itemModels.apply(block) + }.onFailure { + Archie.LOGGER.error( + "Data Provider $name failed with exception: ${it.message}\n" + + "Stacktrace: ${it.stackTraceToString()}" + ) + if (exitOnError) exitProcess(-1) + }.getOrElse { + itemModels.clear() + itemModels + } + } + + private fun key(block: Block): ResourceLocation + { + return BuiltInRegistries.BLOCK.getKey(block) + } + + private fun name(block: Block): String + { + return key(block).path + } + + /** Returns the conventional `block/` texture location for [block]. */ + fun blockTexture(block: Block): ResourceLocation + { + val name = key(block) + return ResourceLocation.fromNamespaceAndPath( + name.namespace, + AModelProvider.BLOCK_FOLDER + "/" + name.path + ) + } + + private fun extend(rl: ResourceLocation, suffix: String): ResourceLocation + { + return ResourceLocation.fromNamespaceAndPath(rl.namespace, rl.path + suffix) + } + + /** Creates a `block/cube_all` model for [block] using [blockTexture] on every face. */ + fun cubeAll(block: Block): AModelFile + { + return blockModels().cubeAll(name(block), blockTexture(block)) + } + + /** Registers a single-variant blockstate for [block] using [expander] to derive [AConfiguredModel]s from [cubeAll]. */ + fun simpleBlock( + block: Block, + expander: Function> + ) + { + simpleBlock(block, *expander.apply(cubeAll(block))) + } + + /** Registers a single-variant blockstate for [block] pointing at [model] (defaults to [cubeAll]). */ + @JvmOverloads + fun simpleBlock(block: Block, model: AModelFile = cubeAll(block)) + { + simpleBlock(block, AConfiguredModel(model)) + } + + /** Sets [block]'s item model to inherit from [model] with no extra elements/overrides. */ + fun simpleBlockItem(block: Block, model: AModelFile) + { + itemModels().getBuilder(key(block).path).parent(model) + } + + /** Combines [simpleBlock] and [simpleBlockItem] for [block] against the same [model]. */ + fun simpleBlockWithItem(block: Block, model: AModelFile = cubeAll(block)) + { + simpleBlock(block, model) + simpleBlockItem(block, model) + } + + /** Registers a single-variant blockstate for [block] that randomly picks between [models]. */ + fun simpleBlock(block: Block, vararg models: AConfiguredModel) + { + getVariantBuilder(block) + .partialState().setModels(*models) + } + + fun logBlock(block: RotatedPillarBlock) + { + axisBlock(block, blockTexture(block), extend(blockTexture(block), "_top")) + } + + @JvmOverloads + fun axisBlock(block: RotatedPillarBlock, baseName: ResourceLocation = blockTexture(block)) + { + axisBlock(block, extend(baseName, "_side"), extend(baseName, "_end")) + } + + fun axisBlock(block: RotatedPillarBlock, side: ResourceLocation, end: ResourceLocation) + { + axisBlock( + block, + blockModels().cubeColumn(name(block), side, end), + blockModels().cubeColumnHorizontal(name(block) + "_horizontal", side, end) + ) + } + + fun axisBlockWithRenderType(block: RotatedPillarBlock, renderType: String) + { + axisBlockWithRenderType(block, blockTexture(block), renderType) + } + + fun logBlockWithRenderType(block: RotatedPillarBlock, renderType: String) + { + axisBlockWithRenderType(block, blockTexture(block), extend(blockTexture(block), "_top"), renderType) + } + + fun axisBlockWithRenderType(block: RotatedPillarBlock, baseName: ResourceLocation, renderType: String) + { + axisBlockWithRenderType(block, extend(baseName, "_side"), extend(baseName, "_end"), renderType) + } + + fun axisBlockWithRenderType( + block: RotatedPillarBlock, + side: ResourceLocation, + end: ResourceLocation, + renderType: String + ) + { + axisBlock( + block, + blockModels().cubeColumn(name(block), side, end).renderType(renderType), + blockModels().cubeColumnHorizontal(name(block) + "_horizontal", side, end).renderType(renderType) + ) + } + + fun axisBlockWithRenderType(block: RotatedPillarBlock, renderType: ResourceLocation) + { + axisBlockWithRenderType(block, blockTexture(block), renderType) + } + + fun logBlockWithRenderType(block: RotatedPillarBlock, renderType: ResourceLocation) + { + axisBlockWithRenderType(block, blockTexture(block), extend(blockTexture(block), "_top"), renderType) + } + + fun axisBlockWithRenderType(block: RotatedPillarBlock, baseName: ResourceLocation, renderType: ResourceLocation) + { + axisBlockWithRenderType(block, extend(baseName, "_side"), extend(baseName, "_end"), renderType) + } + + fun axisBlockWithRenderType( + block: RotatedPillarBlock, + side: ResourceLocation, + end: ResourceLocation, + renderType: ResourceLocation + ) + { + axisBlock( + block, + blockModels().cubeColumn(name(block), side, end).renderType(renderType), + blockModels().cubeColumnHorizontal(name(block) + "_horizontal", side, end).renderType(renderType) + ) + } + + fun axisBlock( + block: RotatedPillarBlock, + vertical: AModelFile, + horizontal: AModelFile + ) + { + getVariantBuilder(block) + .partialState().with(RotatedPillarBlock.AXIS, Direction.Axis.Y) + .modelForState().modelFile(vertical).addModel() + .partialState().with(RotatedPillarBlock.AXIS, Direction.Axis.Z) + .modelForState().modelFile(horizontal).rotationX(90).addModel() + .partialState().with(RotatedPillarBlock.AXIS, Direction.Axis.X) + .modelForState().modelFile(horizontal).rotationX(90).rotationY(90).addModel() + } + + fun horizontalBlock(block: Block, side: ResourceLocation, front: ResourceLocation, top: ResourceLocation) + { + horizontalBlock(block, blockModels().orientable(name(block), side, front, top)) + } + + @JvmOverloads + fun horizontalBlock( + block: Block, + model: AModelFile, + angleOffset: Int = DEFAULT_ANGLE_OFFSET + ) + { + horizontalBlock( + block, + { model }, + angleOffset + ) + } + + @JvmOverloads + fun horizontalBlock( + block: Block, + modelFunc: Function, + angleOffset: Int = DEFAULT_ANGLE_OFFSET + ) + { + getVariantBuilder(block) + .forAllStates { state: BlockState -> + AConfiguredModel.builder() + .modelFile(modelFunc.apply(state)) + .rotationY( + (state.getValue(BlockStateProperties.HORIZONTAL_FACING) + .toYRot().toInt() + angleOffset) % 360 + ) + .build() + } + } + + @JvmOverloads + fun horizontalFaceBlock( + block: Block, + model: AModelFile, + angleOffset: Int = DEFAULT_ANGLE_OFFSET + ) + { + horizontalFaceBlock( + block, + { model }, + angleOffset + ) + } + + @JvmOverloads + fun horizontalFaceBlock( + block: Block, + modelFunc: Function, + angleOffset: Int = DEFAULT_ANGLE_OFFSET + ) + { + getVariantBuilder(block) + .forAllStates { state: BlockState -> + AConfiguredModel.builder() + .modelFile(modelFunc.apply(state)) + .rotationX(state.getValue(BlockStateProperties.ATTACH_FACE).ordinal * 90) + .rotationY( + ((state.getValue(BlockStateProperties.HORIZONTAL_FACING) + .toYRot() + .toInt() + angleOffset) + (if (state.getValue( + BlockStateProperties.ATTACH_FACE + ) == AttachFace.CEILING + ) 180 else 0)) % 360 + ) + .build() + } + } + + @JvmOverloads + fun directionalBlock( + block: Block, + model: AModelFile, + angleOffset: Int = DEFAULT_ANGLE_OFFSET + ) + { + directionalBlock( + block, + { model }, + angleOffset + ) + } + + @JvmOverloads + fun directionalBlock( + block: Block, + modelFunc: Function, + angleOffset: Int = DEFAULT_ANGLE_OFFSET + ) + { + getVariantBuilder(block) + .forAllStates { state: BlockState -> + val dir = + state.getValue(BlockStateProperties.FACING) + AConfiguredModel.builder() + .modelFile(modelFunc.apply(state)) + .rotationX( + if (dir == Direction.DOWN) 180 else if (dir.axis.isHorizontal) 90 else 0 + ) + .rotationY( + if (dir.axis.isVertical) 0 else ((dir.toYRot() + .toInt()) + angleOffset) % 360 + ) + .build() + } + } + + fun stairsBlock(block: StairBlock, texture: ResourceLocation) + { + stairsBlock(block, texture, texture, texture) + } + + fun stairsBlock(block: StairBlock, name: String, texture: ResourceLocation) + { + stairsBlock(block, name, texture, texture, texture) + } + + fun stairsBlock(block: StairBlock, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation) + { + stairsBlockInternal(block, key(block).toString(), side, bottom, top) + } + + fun stairsBlock( + block: StairBlock, + name: String, + side: ResourceLocation, + bottom: ResourceLocation, + top: ResourceLocation + ) + { + stairsBlockInternal(block, name + "_stairs", side, bottom, top) + } + + fun stairsBlockWithRenderType(block: StairBlock, texture: ResourceLocation, renderType: String) + { + stairsBlockWithRenderType(block, texture, texture, texture, renderType) + } + + fun stairsBlockWithRenderType(block: StairBlock, name: String, texture: ResourceLocation, renderType: String) + { + stairsBlockWithRenderType(block, name, texture, texture, texture, renderType) + } + + fun stairsBlockWithRenderType( + block: StairBlock, + side: ResourceLocation, + bottom: ResourceLocation, + top: ResourceLocation, + renderType: String + ) + { + stairsBlockInternalWithRenderType( + block, + key(block).toString(), + side, + bottom, + top, + ResourceLocation.parse(renderType) + ) + } + + fun stairsBlockWithRenderType( + block: StairBlock, + name: String, + side: ResourceLocation, + bottom: ResourceLocation, + top: ResourceLocation, + renderType: String + ) + { + stairsBlockInternalWithRenderType( + block, + name + "_stairs", + side, + bottom, + top, + ResourceLocation.parse(renderType) + ) + } + + fun stairsBlockWithRenderType(block: StairBlock, texture: ResourceLocation, renderType: ResourceLocation) + { + stairsBlockWithRenderType(block, texture, texture, texture, renderType) + } + + fun stairsBlockWithRenderType( + block: StairBlock, + name: String, + texture: ResourceLocation, + renderType: ResourceLocation + ) + { + stairsBlockWithRenderType(block, name, texture, texture, texture, renderType) + } + + fun stairsBlockWithRenderType( + block: StairBlock, + side: ResourceLocation, + bottom: ResourceLocation, + top: ResourceLocation, + renderType: ResourceLocation + ) + { + stairsBlockInternalWithRenderType(block, key(block).toString(), side, bottom, top, renderType) + } + + fun stairsBlockWithRenderType( + block: StairBlock, + name: String, + side: ResourceLocation, + bottom: ResourceLocation, + top: ResourceLocation, + renderType: ResourceLocation + ) + { + stairsBlockInternalWithRenderType(block, name + "_stairs", side, bottom, top, renderType) + } + + private fun stairsBlockInternal( + block: StairBlock, + baseName: String, + side: ResourceLocation, + bottom: ResourceLocation, + top: ResourceLocation + ) + { + val stairs: AModelFile = + blockModels().stairs(baseName, side, bottom, top) + val stairsInner: AModelFile = + blockModels().stairsInner(baseName + "_inner", side, bottom, top) + val stairsOuter: AModelFile = + blockModels().stairsOuter(baseName + "_outer", side, bottom, top) + stairsBlock(block, stairs, stairsInner, stairsOuter) + } + + private fun stairsBlockInternalWithRenderType( + block: StairBlock, + baseName: String, + side: ResourceLocation, + bottom: ResourceLocation, + top: ResourceLocation, + renderType: ResourceLocation + ) + { + val stairs: AModelFile = + blockModels().stairs(baseName, side, bottom, top).renderType(renderType) + val stairsInner: AModelFile = + blockModels().stairsInner(baseName + "_inner", side, bottom, top).renderType(renderType) + val stairsOuter: AModelFile = + blockModels().stairsOuter(baseName + "_outer", side, bottom, top).renderType(renderType) + stairsBlock(block, stairs, stairsInner, stairsOuter) + } + + fun stairsBlock( + block: StairBlock, + stairs: AModelFile, + stairsInner: AModelFile, + stairsOuter: AModelFile + ) + { + getVariantBuilder(block) + .forAllStatesExcept({ state: BlockState -> + val facing = + state.getValue(StairBlock.FACING) + val half = + state.getValue(StairBlock.HALF) + val shape = + state.getValue(StairBlock.SHAPE) + var yRot = + facing.clockWise.toYRot().toInt() // Stairs model is rotated 90 degrees clockwise for some reason + if (shape == StairsShape.INNER_LEFT || shape == StairsShape.OUTER_LEFT) + { + yRot += 270 // Left facing stairs are rotated 90 degrees clockwise + } + if (shape != StairsShape.STRAIGHT && half == Half.TOP) + { + yRot += 90 // Top stairs are rotated 90 degrees clockwise + } + yRot %= 360 + val uvlock = + yRot != 0 || half == Half.TOP // Don't set uvlock for states that have no rotation + AConfiguredModel.builder() + .modelFile(if (shape == StairsShape.STRAIGHT) stairs else if (shape == StairsShape.INNER_LEFT || shape == StairsShape.INNER_RIGHT) stairsInner else stairsOuter) + .rotationX(if (half == Half.BOTTOM) 0 else 180) + .rotationY(yRot) + .uvLock(uvlock) + .build() + }, StairBlock.WATERLOGGED) + } + + fun slabBlock(block: SlabBlock, doubleslab: ResourceLocation, texture: ResourceLocation) + { + slabBlock(block, doubleslab, texture, texture, texture) + } + + fun slabBlock( + block: SlabBlock, + doubleslab: ResourceLocation, + side: ResourceLocation, + bottom: ResourceLocation, + top: ResourceLocation + ) + { + slabBlock( + block, + blockModels().slab(name(block), side, bottom, top), + blockModels().slabTop(name(block) + "_top", side, bottom, top), + blockModels().getExistingFile(doubleslab) + ) + } + + fun slabBlock( + block: SlabBlock, + bottom: AModelFile, + top: AModelFile, + doubleslab: AModelFile + ) + { + getVariantBuilder(block) + .partialState().with(SlabBlock.TYPE, SlabType.BOTTOM) + .addModels(AConfiguredModel(bottom)) + .partialState().with(SlabBlock.TYPE, SlabType.TOP) + .addModels(AConfiguredModel(top)) + .partialState().with(SlabBlock.TYPE, SlabType.DOUBLE) + .addModels(AConfiguredModel(doubleslab)) + } + + fun buttonBlock(block: ButtonBlock, texture: ResourceLocation) + { + val button: AModelFile = blockModels().button(name(block), texture) + val buttonPressed: AModelFile = + blockModels().buttonPressed(name(block) + "_pressed", texture) + buttonBlock(block, button, buttonPressed) + } + + fun buttonBlock( + block: ButtonBlock, + button: AModelFile, + buttonPressed: AModelFile + ) + { + getVariantBuilder(block).forAllStates(Function> { state: BlockState -> + val facing = + state.getValue(ButtonBlock.FACING) + val face = + state.getValue(ButtonBlock.FACE) + val powered = + state.getValue(ButtonBlock.POWERED) + AConfiguredModel.builder() + .modelFile(if (powered) buttonPressed else button) + .rotationX(if (face == AttachFace.FLOOR) 0 else if (face == AttachFace.WALL) 90 else 180) + .rotationY( + (if (face == AttachFace.CEILING) facing else facing.opposite).toYRot() + .toInt() + ) + .uvLock(face == AttachFace.WALL) + .build() + }) + } + + fun pressurePlateBlock(block: PressurePlateBlock, texture: ResourceLocation) + { + val pressurePlate: AModelFile = + blockModels().pressurePlate(name(block), texture) + val pressurePlateDown: AModelFile = + blockModels().pressurePlateDown(name(block) + "_down", texture) + pressurePlateBlock(block, pressurePlate, pressurePlateDown) + } + + fun pressurePlateBlock( + block: PressurePlateBlock, + pressurePlate: AModelFile, + pressurePlateDown: AModelFile + ) + { + getVariantBuilder(block) + .partialState().with(PressurePlateBlock.POWERED, true) + .addModels(AConfiguredModel(pressurePlateDown)) + .partialState().with(PressurePlateBlock.POWERED, false) + .addModels(AConfiguredModel(pressurePlate)) + } + + fun signBlock(signBlock: StandingSignBlock, wallSignBlock: WallSignBlock, texture: ResourceLocation) + { + val sign: AModelFile = blockModels().sign(name(signBlock), texture) + signBlock(signBlock, wallSignBlock, sign) + } + + fun signBlock( + signBlock: StandingSignBlock, + wallSignBlock: WallSignBlock, + sign: AModelFile + ) + { + simpleBlock(signBlock, sign) + simpleBlock(wallSignBlock, sign) + } + + fun fourWayBlock( + block: CrossCollisionBlock, + post: AModelFile, + side: AModelFile + ) + { + val builder: AMultiPartBlockStateBuilder = + getMultipartBuilder(block) + .part().modelFile(post).addModel().end() + fourWayMultipart(builder, side) + } + + fun fourWayMultipart( + builder: AMultiPartBlockStateBuilder, + side: AModelFile + ) + { + PipeBlock.PROPERTY_BY_DIRECTION.entries.forEach(Consumer> { e: Map.Entry -> + val dir = e.key + if (dir.axis.isHorizontal) + { + builder.part().modelFile(side) + .rotationY(((dir.toYRot().toInt()) + 180) % 360).uvLock(true).addModel() + .condition(e.value, true) + } + }) + } + + fun fenceBlock(block: FenceBlock, texture: ResourceLocation) + { + val baseName = key(block).toString() + fourWayBlock( + block, + blockModels().fencePost(baseName + "_post", texture), + blockModels().fenceSide(baseName + "_side", texture) + ) + } + + fun fenceBlock(block: FenceBlock, name: String, texture: ResourceLocation) + { + fourWayBlock( + block, + blockModels().fencePost(name + "_fence_post", texture), + blockModels().fenceSide(name + "_fence_side", texture) + ) + } + + fun fenceBlockWithRenderType(block: FenceBlock, texture: ResourceLocation, renderType: String) + { + val baseName = key(block).toString() + fourWayBlock( + block, + blockModels().fencePost(baseName + "_post", texture).renderType(renderType), + blockModels().fenceSide(baseName + "_side", texture).renderType(renderType) + ) + } + + fun fenceBlockWithRenderType(block: FenceBlock, name: String, texture: ResourceLocation, renderType: String) + { + fourWayBlock( + block, + blockModels().fencePost(name + "_fence_post", texture).renderType(renderType), + blockModels().fenceSide(name + "_fence_side", texture).renderType(renderType) + ) + } + + fun fenceBlockWithRenderType(block: FenceBlock, texture: ResourceLocation, renderType: ResourceLocation) + { + val baseName = key(block).toString() + fourWayBlock( + block, + blockModels().fencePost(baseName + "_post", texture).renderType(renderType), + blockModels().fenceSide(baseName + "_side", texture).renderType(renderType) + ) + } + + fun fenceBlockWithRenderType( + block: FenceBlock, + name: String, + texture: ResourceLocation, + renderType: ResourceLocation + ) + { + fourWayBlock( + block, + blockModels().fencePost(name + "_fence_post", texture).renderType(renderType), + blockModels().fenceSide(name + "_fence_side", texture).renderType(renderType) + ) + } + + fun fenceGateBlock(block: FenceGateBlock, texture: ResourceLocation) + { + fenceGateBlockInternal(block, key(block).toString(), texture) + } + + fun fenceGateBlock(block: FenceGateBlock, name: String, texture: ResourceLocation) + { + fenceGateBlockInternal(block, name + "_fence_gate", texture) + } + + fun fenceGateBlockWithRenderType(block: FenceGateBlock, texture: ResourceLocation, renderType: String) + { + fenceGateBlockInternalWithRenderType( + block, + key(block).toString(), + texture, + ResourceLocation.parse(renderType) + ) + } + + fun fenceGateBlockWithRenderType( + block: FenceGateBlock, + name: String, + texture: ResourceLocation, + renderType: String + ) + { + fenceGateBlockInternalWithRenderType( + block, + name + "_fence_gate", + texture, + ResourceLocation.parse(renderType) + ) + } + + fun fenceGateBlockWithRenderType(block: FenceGateBlock, texture: ResourceLocation, renderType: ResourceLocation) + { + fenceGateBlockInternalWithRenderType(block, key(block).toString(), texture, renderType) + } + + fun fenceGateBlockWithRenderType( + block: FenceGateBlock, + name: String, + texture: ResourceLocation, + renderType: ResourceLocation + ) + { + fenceGateBlockInternalWithRenderType(block, name + "_fence_gate", texture, renderType) + } + + private fun fenceGateBlockInternal(block: FenceGateBlock, baseName: String, texture: ResourceLocation) + { + val gate: AModelFile = blockModels().fenceGate(baseName, texture) + val gateOpen: AModelFile = + blockModels().fenceGateOpen(baseName + "_open", texture) + val gateWall: AModelFile = + blockModels().fenceGateWall(baseName + "_wall", texture) + val gateWallOpen: AModelFile = + blockModels().fenceGateWallOpen(baseName + "_wall_open", texture) + fenceGateBlock(block, gate, gateOpen, gateWall, gateWallOpen) + } + + private fun fenceGateBlockInternalWithRenderType( + block: FenceGateBlock, + baseName: String, + texture: ResourceLocation, + renderType: ResourceLocation + ) + { + val gate: AModelFile = + blockModels().fenceGate(baseName, texture).renderType(renderType) + val gateOpen: AModelFile = + blockModels().fenceGateOpen(baseName + "_open", texture).renderType(renderType) + val gateWall: AModelFile = + blockModels().fenceGateWall(baseName + "_wall", texture).renderType(renderType) + val gateWallOpen: AModelFile = + blockModels().fenceGateWallOpen(baseName + "_wall_open", texture).renderType(renderType) + fenceGateBlock(block, gate, gateOpen, gateWall, gateWallOpen) + } + + fun fenceGateBlock( + block: FenceGateBlock, + gate: AModelFile, + gateOpen: AModelFile, + gateWall: AModelFile, + gateWallOpen: AModelFile + ) + { + getVariantBuilder(block).forAllStatesExcept({ state: BlockState -> + var model: AModelFile = gate + if (state.getValue(FenceGateBlock.IN_WALL)) + { + model = gateWall + } + if (state.getValue(FenceGateBlock.OPEN)) + { + model = if (model === gateWall) gateWallOpen else gateOpen + } + AConfiguredModel.builder() + .modelFile(model) + .rotationY( + state.getValue(FenceGateBlock.FACING) + .toYRot().toInt() + ) + .uvLock(true) + .build() + }, FenceGateBlock.POWERED) + } + + fun wallBlock(block: WallBlock, texture: ResourceLocation) + { + wallBlockInternal(block, key(block).toString(), texture) + } + + fun wallBlock(block: WallBlock, name: String, texture: ResourceLocation) + { + wallBlockInternal(block, name + "_wall", texture) + } + + fun wallBlockWithRenderType(block: WallBlock, texture: ResourceLocation, renderType: String) + { + wallBlockInternalWithRenderType(block, key(block).toString(), texture, ResourceLocation.parse(renderType)) + } + + fun wallBlockWithRenderType(block: WallBlock, name: String, texture: ResourceLocation, renderType: String) + { + wallBlockInternalWithRenderType(block, name + "_wall", texture, ResourceLocation.parse(renderType)) + } + + fun wallBlockWithRenderType(block: WallBlock, texture: ResourceLocation, renderType: ResourceLocation) + { + wallBlockInternalWithRenderType(block, key(block).toString(), texture, renderType) + } + + fun wallBlockWithRenderType( + block: WallBlock, + name: String, + texture: ResourceLocation, + renderType: ResourceLocation + ) + { + wallBlockInternalWithRenderType(block, name + "_wall", texture, renderType) + } + + private fun wallBlockInternal(block: WallBlock, baseName: String, texture: ResourceLocation) + { + wallBlock( + block, blockModels().wallPost(baseName + "_post", texture), + blockModels().wallSide(baseName + "_side", texture), + blockModels().wallSideTall(baseName + "_side_tall", texture) + ) + } + + private fun wallBlockInternalWithRenderType( + block: WallBlock, + baseName: String, + texture: ResourceLocation, + renderType: ResourceLocation + ) + { + wallBlock( + block, blockModels().wallPost(baseName + "_post", texture).renderType(renderType), + blockModels().wallSide(baseName + "_side", texture).renderType(renderType), + blockModels().wallSideTall(baseName + "_side_tall", texture).renderType(renderType) + ) + } + + fun wallBlock( + block: WallBlock, + post: AModelFile, + side: AModelFile, + sideTall: AModelFile + ) + { + val builder: AMultiPartBlockStateBuilder = + getMultipartBuilder(block) + .part().modelFile(post).addModel() + .condition(WallBlock.UP, true).end() + WALL_PROPS.entries.stream() + .filter { e: Map.Entry> -> + e.key.axis.isHorizontal + } + .forEach { e: Map.Entry> -> + wallSidePart(builder, side, e, WallSide.LOW) + wallSidePart(builder, sideTall, e, WallSide.TALL) + } + } + + private fun wallSidePart( + builder: AMultiPartBlockStateBuilder, + model: AModelFile, + entry: Map.Entry>, + height: WallSide + ) + { + builder.part() + .modelFile(model) + .rotationY(((entry.key.toYRot().toInt()) + 180) % 360) + .uvLock(true) + .addModel() + .condition(entry.value, height) + } + + fun paneBlock(block: IronBarsBlock, pane: ResourceLocation, edge: ResourceLocation) + { + paneBlockInternal(block, key(block).toString(), pane, edge) + } + + fun paneBlock(block: IronBarsBlock, name: String, pane: ResourceLocation, edge: ResourceLocation) + { + paneBlockInternal(block, name + "_pane", pane, edge) + } + + fun paneBlockWithRenderType( + block: IronBarsBlock, + pane: ResourceLocation, + edge: ResourceLocation, + renderType: String + ) + { + paneBlockInternalWithRenderType(block, key(block).toString(), pane, edge, ResourceLocation.parse(renderType)) + } + + fun paneBlockWithRenderType( + block: IronBarsBlock, + name: String, + pane: ResourceLocation, + edge: ResourceLocation, + renderType: String + ) + { + paneBlockInternalWithRenderType(block, name + "_pane", pane, edge, ResourceLocation.parse(renderType)) + } + + fun paneBlockWithRenderType( + block: IronBarsBlock, + pane: ResourceLocation, + edge: ResourceLocation, + renderType: ResourceLocation + ) + { + paneBlockInternalWithRenderType(block, key(block).toString(), pane, edge, renderType) + } + + fun paneBlockWithRenderType( + block: IronBarsBlock, + name: String, + pane: ResourceLocation, + edge: ResourceLocation, + renderType: ResourceLocation + ) + { + paneBlockInternalWithRenderType(block, name + "_pane", pane, edge, renderType) + } + + private fun paneBlockInternal( + block: IronBarsBlock, + baseName: String, + pane: ResourceLocation, + edge: ResourceLocation + ) + { + val post: AModelFile = + blockModels().panePost(baseName + "_post", pane, edge) + val side: AModelFile = + blockModels().paneSide(baseName + "_side", pane, edge) + val sideAlt: AModelFile = + blockModels().paneSideAlt(baseName + "_side_alt", pane, edge) + val noSide: AModelFile = + blockModels().paneNoSide(baseName + "_noside", pane) + val noSideAlt: AModelFile = + blockModels().paneNoSideAlt(baseName + "_noside_alt", pane) + paneBlock(block, post, side, sideAlt, noSide, noSideAlt) + } + + private fun paneBlockInternalWithRenderType( + block: IronBarsBlock, + baseName: String, + pane: ResourceLocation, + edge: ResourceLocation, + renderType: ResourceLocation + ) + { + val post: AModelFile = + blockModels().panePost(baseName + "_post", pane, edge).renderType(renderType) + val side: AModelFile = + blockModels().paneSide(baseName + "_side", pane, edge).renderType(renderType) + val sideAlt: AModelFile = + blockModels().paneSideAlt(baseName + "_side_alt", pane, edge).renderType(renderType) + val noSide: AModelFile = + blockModels().paneNoSide(baseName + "_noside", pane).renderType(renderType) + val noSideAlt: AModelFile = + blockModels().paneNoSideAlt(baseName + "_noside_alt", pane).renderType(renderType) + paneBlock(block, post, side, sideAlt, noSide, noSideAlt) + } + + fun paneBlock( + block: IronBarsBlock, + post: AModelFile, + side: AModelFile, + sideAlt: AModelFile, + noSide: AModelFile, + noSideAlt: AModelFile + ) + { + val builder: AMultiPartBlockStateBuilder = + getMultipartBuilder(block) + .part().modelFile(post).addModel().end() + PipeBlock.PROPERTY_BY_DIRECTION.entries.forEach(Consumer> { e: Map.Entry -> + val dir = e.key + if (dir.axis.isHorizontal) + { + val alt = dir == Direction.SOUTH + builder.part().modelFile(if (alt || dir == Direction.WEST) sideAlt else side) + .rotationY(if (dir.axis === Direction.Axis.X) 90 else 0).addModel() + .condition(e.value, true).end() + .part().modelFile(if (alt || dir == Direction.EAST) noSideAlt else noSide) + .rotationY(if (dir == Direction.WEST) 270 else if (dir == Direction.SOUTH) 90 else 0) + .addModel() + .condition(e.value, false) + } + }) + } + + fun doorBlock(block: DoorBlock, bottom: ResourceLocation, top: ResourceLocation) + { + doorBlockInternal(block, key(block).toString(), bottom, top) + } + + fun doorBlock(block: DoorBlock, name: String, bottom: ResourceLocation, top: ResourceLocation) + { + doorBlockInternal(block, name + "_door", bottom, top) + } + + fun doorBlockWithRenderType(block: DoorBlock, bottom: ResourceLocation, top: ResourceLocation, renderType: String) + { + doorBlockInternalWithRenderType( + block, + key(block).toString(), + bottom, + top, + ResourceLocation.parse(renderType) + ) + } + + fun doorBlockWithRenderType( + block: DoorBlock, + name: String, + bottom: ResourceLocation, + top: ResourceLocation, + renderType: String + ) + { + doorBlockInternalWithRenderType(block, name + "_door", bottom, top, ResourceLocation.parse(renderType)) + } + + fun doorBlockWithRenderType( + block: DoorBlock, + bottom: ResourceLocation, + top: ResourceLocation, + renderType: ResourceLocation + ) + { + doorBlockInternalWithRenderType(block, key(block).toString(), bottom, top, renderType) + } + + fun doorBlockWithRenderType( + block: DoorBlock, + name: String, + bottom: ResourceLocation, + top: ResourceLocation, + renderType: ResourceLocation + ) + { + doorBlockInternalWithRenderType(block, name + "_door", bottom, top, renderType) + } + + private fun doorBlockInternal(block: DoorBlock, baseName: String, bottom: ResourceLocation, top: ResourceLocation) + { + val bottomLeft: AModelFile = + blockModels().doorBottomLeft(baseName + "_bottom_left", bottom, top) + val bottomLeftOpen: AModelFile = + blockModels().doorBottomLeftOpen(baseName + "_bottom_left_open", bottom, top) + val bottomRight: AModelFile = + blockModels().doorBottomRight(baseName + "_bottom_right", bottom, top) + val bottomRightOpen: AModelFile = + blockModels().doorBottomRightOpen(baseName + "_bottom_right_open", bottom, top) + val topLeft: AModelFile = + blockModels().doorTopLeft(baseName + "_top_left", bottom, top) + val topLeftOpen: AModelFile = + blockModels().doorTopLeftOpen(baseName + "_top_left_open", bottom, top) + val topRight: AModelFile = + blockModels().doorTopRight(baseName + "_top_right", bottom, top) + val topRightOpen: AModelFile = + blockModels().doorTopRightOpen(baseName + "_top_right_open", bottom, top) + doorBlock( + block, + bottomLeft, + bottomLeftOpen, + bottomRight, + bottomRightOpen, + topLeft, + topLeftOpen, + topRight, + topRightOpen + ) + } + + private fun doorBlockInternalWithRenderType( + block: DoorBlock, + baseName: String, + bottom: ResourceLocation, + top: ResourceLocation, + renderType: ResourceLocation + ) + { + val bottomLeft: AModelFile = + blockModels().doorBottomLeft(baseName + "_bottom_left", bottom, top).renderType(renderType) + val bottomLeftOpen: AModelFile = + blockModels().doorBottomLeftOpen(baseName + "_bottom_left_open", bottom, top).renderType(renderType) + val bottomRight: AModelFile = + blockModels().doorBottomRight(baseName + "_bottom_right", bottom, top).renderType(renderType) + val bottomRightOpen: AModelFile = + blockModels().doorBottomRightOpen(baseName + "_bottom_right_open", bottom, top).renderType(renderType) + val topLeft: AModelFile = + blockModels().doorTopLeft(baseName + "_top_left", bottom, top).renderType(renderType) + val topLeftOpen: AModelFile = + blockModels().doorTopLeftOpen(baseName + "_top_left_open", bottom, top).renderType(renderType) + val topRight: AModelFile = + blockModels().doorTopRight(baseName + "_top_right", bottom, top).renderType(renderType) + val topRightOpen: AModelFile = + blockModels().doorTopRightOpen(baseName + "_top_right_open", bottom, top).renderType(renderType) + doorBlock( + block, + bottomLeft, + bottomLeftOpen, + bottomRight, + bottomRightOpen, + topLeft, + topLeftOpen, + topRight, + topRightOpen + ) + } + + fun doorBlock( + block: DoorBlock, + bottomLeft: AModelFile, + bottomLeftOpen: AModelFile, + bottomRight: AModelFile, + bottomRightOpen: AModelFile, + topLeft: AModelFile, + topLeftOpen: AModelFile, + topRight: AModelFile, + topRightOpen: AModelFile + ) + { + getVariantBuilder(block).forAllStatesExcept({ state: BlockState -> + var yRot = + state.getValue(DoorBlock.FACING).toYRot() + .toInt() + 90 + val right = + state.getValue(DoorBlock.HINGE) == DoorHingeSide.RIGHT + val open = state.getValue(DoorBlock.OPEN) + val lower = + state.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER + if (open) + { + yRot += 90 + } + if (right && open) + { + yRot += 180 + } + yRot %= 360 + + val model: AModelFile = when + { + lower && right && open -> + { + bottomRightOpen + } + + lower && !right && open -> + { + bottomLeftOpen + } + + lower && right && !open -> + { + bottomRight + } + + lower && !right && !open -> + { + bottomLeft + } + + !lower && right && open -> + { + topRightOpen + } + + !lower && !right && open -> + { + topLeftOpen + } + + !lower && right && !open -> + { + topRight + } + + !lower && !right && !open -> + { + topLeft + } + + else -> null + }!! + AConfiguredModel.builder().modelFile(model) + .rotationY(yRot) + .build() + }, DoorBlock.POWERED) + } + + fun trapdoorBlock(block: TrapDoorBlock, texture: ResourceLocation, orientable: Boolean) + { + trapdoorBlockInternal(block, key(block).toString(), texture, orientable) + } + + fun trapdoorBlock(block: TrapDoorBlock, name: String, texture: ResourceLocation, orientable: Boolean) + { + trapdoorBlockInternal(block, name + "_trapdoor", texture, orientable) + } + + fun trapdoorBlockWithRenderType( + block: TrapDoorBlock, + texture: ResourceLocation, + orientable: Boolean, + renderType: String + ) + { + trapdoorBlockInternalWithRenderType( + block, + key(block).toString(), + texture, + orientable, + ResourceLocation.parse(renderType) + ) + } + + fun trapdoorBlockWithRenderType( + block: TrapDoorBlock, + name: String, + texture: ResourceLocation, + orientable: Boolean, + renderType: String + ) + { + trapdoorBlockInternalWithRenderType( + block, + name + "_trapdoor", + texture, + orientable, + ResourceLocation.parse(renderType) + ) + } + + fun trapdoorBlockWithRenderType( + block: TrapDoorBlock, + texture: ResourceLocation, + orientable: Boolean, + renderType: ResourceLocation + ) + { + trapdoorBlockInternalWithRenderType(block, key(block).toString(), texture, orientable, renderType) + } + + fun trapdoorBlockWithRenderType( + block: TrapDoorBlock, + name: String, + texture: ResourceLocation, + orientable: Boolean, + renderType: ResourceLocation + ) + { + trapdoorBlockInternalWithRenderType(block, name + "_trapdoor", texture, orientable, renderType) + } + + private fun trapdoorBlockInternal( + block: TrapDoorBlock, + baseName: String, + texture: ResourceLocation, + orientable: Boolean + ) + { + val bottom: AModelFile = + if (orientable) blockModels().trapdoorOrientableBottom( + baseName + "_bottom", + texture + ) else blockModels().trapdoorBottom(baseName + "_bottom", texture) + val top: AModelFile = + if (orientable) blockModels().trapdoorOrientableTop( + baseName + "_top", + texture + ) else blockModels().trapdoorTop( + baseName + "_top", + texture + ) + val open: AModelFile = + if (orientable) blockModels().trapdoorOrientableOpen( + baseName + "_open", + texture + ) else blockModels().trapdoorOpen( + baseName + "_open", + texture + ) + trapdoorBlock(block, bottom, top, open, orientable) + } + + private fun trapdoorBlockInternalWithRenderType( + block: TrapDoorBlock, + baseName: String, + texture: ResourceLocation, + orientable: Boolean, + renderType: ResourceLocation + ) + { + val bottom: AModelFile = + if (orientable) blockModels().trapdoorOrientableBottom(baseName + "_bottom", texture) + .renderType(renderType) else blockModels().trapdoorBottom(baseName + "_bottom", texture) + .renderType(renderType) + val top: AModelFile = + if (orientable) blockModels().trapdoorOrientableTop(baseName + "_top", texture) + .renderType(renderType) else blockModels().trapdoorTop(baseName + "_top", texture) + .renderType(renderType) + val open: AModelFile = + if (orientable) blockModels().trapdoorOrientableOpen(baseName + "_open", texture) + .renderType(renderType) else blockModels().trapdoorOpen(baseName + "_open", texture) + .renderType(renderType) + trapdoorBlock(block, bottom, top, open, orientable) + } + + fun trapdoorBlock( + block: TrapDoorBlock, + bottom: AModelFile, + top: AModelFile, + open: AModelFile, + orientable: Boolean + ) + { + getVariantBuilder(block).forAllStatesExcept({ state: BlockState -> + var xRot = 0 + var yRot = + state.getValue(TrapDoorBlock.FACING) + .toYRot().toInt() + 180 + val isOpen = + state.getValue(TrapDoorBlock.OPEN) + if (orientable && isOpen && state.getValue(TrapDoorBlock.HALF) == Half.TOP) + { + xRot += 180 + yRot += 180 + } + if (!orientable && !isOpen) + { + yRot = 0 + } + yRot %= 360 + AConfiguredModel.builder().modelFile( + if (isOpen) open else if (state.getValue(TrapDoorBlock.HALF) == Half.TOP) top else bottom + ) + .rotationX(xRot) + .rotationY(yRot) + .build() + }, TrapDoorBlock.POWERED, TrapDoorBlock.WATERLOGGED) + } + + private fun saveBlockState(cache: CachedOutput, stateJson: JsonObject, owner: Block): CompletableFuture<*> + { + val blockName = Preconditions.checkNotNull(key(owner)) + val outputPath = output.getOutputFolder(PackOutput.Target.RESOURCE_PACK) + .resolve(blockName.namespace).resolve("blockstates").resolve(blockName.path + ".json") + return DataProvider.saveStable(cache, stateJson, outputPath) + } + + override fun getName(): String + { + return format("Block States") + } + + class ConfiguredModelList private constructor(models: List) + { + private val models: List + + init + { + Preconditions.checkArgument(models.isNotEmpty()) + this.models = models + } + + constructor(vararg models: AConfiguredModel) : this( + listOf( + *models + ) + ) + + fun toJSON(): JsonElement + { + if (models.size == 1) + { + return models[0].toJSON(false) + } else + { + val ret = JsonArray() + for (m in models) + { + ret.add(m.toJSON(true)) + } + return ret + } + } + + fun append(vararg models: AConfiguredModel): ConfiguredModelList + { + return ConfiguredModelList( + buildList { + addAll(this@ConfiguredModelList.models) + addAll(models) + } + ) + } + } + + companion object + { + private val LOGGER: Logger = LogManager.getLogger() + private val GSON: Gson = + GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create() + + private const val DEFAULT_ANGLE_OFFSET = 180 + + val WALL_PROPS: Map> = + buildMap { + put(Direction.EAST, BlockStateProperties.EAST_WALL) + put(Direction.NORTH, BlockStateProperties.NORTH_WALL) + put(Direction.SOUTH, BlockStateProperties.SOUTH_WALL) + put(Direction.WEST, BlockStateProperties.WEST_WALL) + } + + } +} diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AConfiguredModel.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AConfiguredModel.kt new file mode 100644 index 000000000..be2c1b0ab --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AConfiguredModel.kt @@ -0,0 +1,242 @@ +package net.kernelpanicsoft.archie.data.client.model + +import com.google.common.base.Preconditions +import com.google.common.collect.ImmutableList +import com.google.common.collect.ObjectArrays +import com.google.gson.JsonObject +import net.minecraft.client.resources.model.BlockModelRotation +import java.util.* +import java.util.function.Function +import java.util.stream.Collectors +import java.util.stream.IntStream + +/** One weighted, rotated variant entry pointing at a [model], as used in blockstate `variants`. Build via [builder]. */ +class AConfiguredModel @JvmOverloads constructor( + model: AModelFile, + rotationX: Int = 0, + rotationY: Int = 0, + uvLock: Boolean = false, + weight: Int = DEFAULT_WEIGHT +) +{ + val model: AModelFile + val rotationX: Int + val rotationY: Int + val uvLock: Boolean + val weight: Int + + init + { + Preconditions.checkNotNull(model) + this.model = model + checkRotation(rotationX, rotationY) + this.rotationX = rotationX + this.rotationY = rotationY + this.uvLock = uvLock + checkWeight(weight) + this.weight = weight + } + + fun toJSON(includeWeight: Boolean): JsonObject + { + val modelJson = JsonObject() + modelJson.addProperty("model", model.location.toString()) + if (rotationX != 0) modelJson.addProperty("x", rotationX) + if (rotationY != 0) modelJson.addProperty("y", rotationY) + if (uvLock) modelJson.addProperty("uvlock", uvLock) + if (includeWeight && weight != DEFAULT_WEIGHT) modelJson.addProperty("weight", weight) + return modelJson + } + + /** + * A builder for one or more [AConfiguredModel]s, optionally backed by a callback that + * consumes the finished result and returns [T] (the owning builder, e.g. an + * [AVariantBlockStateBuilder.PartialBlockstate]). Without a callback (as from the standalone + * [AConfiguredModel.builder]), [addModel] is unavailable; use [build]/[buildLast] instead. + * + * Multiple weighted variants can be configured at once through [nextModel]/[model]. + */ + class Builder @JvmOverloads internal constructor( + private val callback: Function, T>? = null, + private var otherModels: List = listOf() + ) + { + private var model: AModelFile? = null + private var rotationX = 0 + private var rotationY = 0 + private var uvLock = false + private var weight = DEFAULT_WEIGHT + + fun modelFile(model: AModelFile): Builder + { + Preconditions.checkNotNull( + model, + "Model must not be null" + ) + this.model = model + return this + } + + fun rotationX(value: Int): Builder + { + checkRotation(value, rotationY) + rotationX = value + return this + } + + fun rotationY(value: Int): Builder + { + checkRotation(rotationX, value) + rotationY = value + return this + } + + fun uvLock(value: Boolean): Builder + { + uvLock = value + return this + } + + fun weight(value: Int): Builder + { + checkWeight(value) + weight = value + return this + } + + /** Builds only the currently-configured [AConfiguredModel], discarding [otherModels]. */ + fun buildLast(): AConfiguredModel + { + return AConfiguredModel(model!!, rotationX, rotationY, uvLock, weight) + } + + /** Builds every configured model, including any queued via [nextModel]. */ + fun build(): Array + { + return ObjectArrays.concat(otherModels.toTypedArray(), buildLast()) + } + + /** Finalizes [build] and hands the result to the owning builder's callback, returning [T]. */ + fun addModel(): T + { + Preconditions.checkNotNull(callback, "Cannot use addModel() without an owning builder present") + return callback!!.apply(build()) + } + + /** Starts configuring another weighted variant, keeping models built so far. */ + fun nextModel(): Builder + { + return Builder(callback, build().toList()) + } + + /** Configures the current (or, once already configured, the next) variant with [block]. */ + fun model(block: Builder.() -> Unit): Builder + { + if (otherModels.isEmpty()) + block() + else + otherModels = nextModel().apply(block).otherModels + return this + } + } + + companion object + { + const val DEFAULT_WEIGHT: Int = 1 + + private fun validRotations(): IntStream + { + return IntStream.range(0, 4).map { i: Int -> i * 90 } + } + + /** Builds one [AConfiguredModel] of [model] per valid Y rotation (0/90/180/270), all at fixed X rotation [x]. */ + @JvmOverloads + fun allYRotations( + model: AModelFile, + x: Int, + uvlock: Boolean, + weight: Int = DEFAULT_WEIGHT + ): Array + { + return validRotations() + .mapToObj { y: Int -> AConfiguredModel(model, x, y, uvlock, weight) } + .collect(Collectors.toList()).toTypedArray() + } + + /** Builds one [AConfiguredModel] of [model] for every valid X/Y rotation combination. */ + @JvmOverloads + fun allRotations( + model: AModelFile, + uvlock: Boolean, + weight: Int = DEFAULT_WEIGHT + ): Array + { + return validRotations() + .mapToObj { x: Int -> + allYRotations( + model, + x, + uvlock, + weight + ) + } + .flatMap { array: Array -> + Arrays.stream( + array + ) + }.collect(Collectors.toList()).toTypedArray() + } + + fun checkRotation(rotationX: Int, rotationY: Int) + { + Preconditions.checkArgument( + BlockModelRotation.by(rotationX, rotationY) != null, + "Invalid model rotation x=%d, y=%d", + rotationX, + rotationY + ) + } + + fun checkWeight(weight: Int) + { + Preconditions.checkArgument( + weight >= 1, + "Model weight must be greater than or equal to 1. Found: %d", + weight + ) + } + + /** Creates a standalone [Builder] with no owning-builder callback; use [Builder.build]/[Builder.buildLast]. */ + fun builder(block: Builder<*>.() -> Unit = {}): Builder<*> + { + return Builder().apply(block) + } + + fun builder( + outer: AVariantBlockStateBuilder, + state: AVariantBlockStateBuilder.PartialBlockstate + ): Builder + { + return Builder({ models: Array -> + outer.setModels( + state, + *models + ) + }, ImmutableList.of()) + } + + fun builder(outer: AMultiPartBlockStateBuilder): Builder + { + return Builder( + { models: Array -> + val ret: AMultiPartBlockStateBuilder.PartBuilder = + outer.PartBuilder( + ABlockStateProvider.ConfiguredModelList(*models) + ) + outer.addPart(ret) + ret + }, ImmutableList.of() + ) + } + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ACustomLoaderBuilder.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ACustomLoaderBuilder.kt new file mode 100644 index 000000000..d669f953d --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ACustomLoaderBuilder.kt @@ -0,0 +1,84 @@ +package net.kernelpanicsoft.archie.data.client.model + +import com.google.common.base.Preconditions +import com.google.gson.JsonObject +import net.minecraft.resources.ResourceLocation + +/** + * Base for a custom geometry loader's model JSON, embedded in an [AModelBuilder] via + * [AModelBuilder.customLoader]. Subclasses add their loader's own fields by overriding + * [toJson]; this base handles the common `loader`/`visibility`/`optional` fields. + */ +abstract class ACustomLoaderBuilder> protected constructor( + /** The id of the associated geometry loader. */ + val loaderId: ResourceLocation, + /** The [AModelBuilder] this loader is being configured on; returned by [end]. */ + protected val parent: T, + val allowInlineElements: Boolean +) +{ + protected val visibility: MutableMap = LinkedHashMap() + private var optional = false + + @Deprecated("Use the (loaderId, parent, allowInlineElements) constructor instead") + protected constructor( + loaderId: ResourceLocation, + parent: T, + ) : this(loaderId, parent, false) + + /** Sets whether the model part named [partName] is initially visible. */ + fun visibility(partName: String, show: Boolean): ACustomLoaderBuilder + { + Preconditions.checkNotNull(partName, "partName must not be null") + visibility[partName] = show + return this + } + + /** + * Mark the custom loader as optional for this model to allow it to be loaded through vanilla paths + * if the loader is not present + */ + fun optional(): ACustomLoaderBuilder + { + Preconditions.checkState( + allowInlineElements, + "Only loaders with support for inline elements can be marked as optional" + ) + this.optional = true + return this + } + + /** Returns to the enclosing [AModelBuilder] this loader was configured on. */ + fun end(): T + { + return parent + } + + open fun toJson(json: JsonObject): JsonObject + { + if (optional) + { + val loaderObj = JsonObject() + loaderObj.addProperty("id", loaderId.toString()) + loaderObj.addProperty("optional", true) + json.add("loader", loaderObj) + } else + { + json.addProperty("loader", loaderId.toString()) + } + + if (visibility.isNotEmpty()) + { + val visibilityObj = JsonObject() + + for ((key, value) in visibility) + { + visibilityObj.addProperty(key, value) + } + + json.add("visibility", visibilityObj) + } + + return json + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelBuilder.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelBuilder.kt new file mode 100644 index 000000000..b92a9538b --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelBuilder.kt @@ -0,0 +1,88 @@ +package net.kernelpanicsoft.archie.data.client.model + +import com.google.common.base.Preconditions +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import net.minecraft.resources.ResourceLocation + +/** [AModelBuilder] for an item model at [outputLocation], adding support for `overrides` entries. */ +class AItemModelBuilder( + outputLocation: ResourceLocation, +) : AModelBuilder(outputLocation) +{ + protected var overrides: MutableList = ArrayList() + + /** Adds a new override entry, configured by [block]. */ + fun override(block: OverrideBuilder.() -> Unit = {}): OverrideBuilder + { + val ret = OverrideBuilder().apply(block) + overrides.add(ret) + return ret + } + + /** Reconfigures the existing override at [index] with [block]. */ + fun override(index: Int, block: OverrideBuilder.() -> Unit = {}): OverrideBuilder + { + Preconditions.checkElementIndex(index, overrides.size, "override") + return overrides[index].apply(block) + } + + override fun toJson(): JsonObject + { + val root: JsonObject = super.toJson() + if (overrides.isNotEmpty()) + { + val overridesJson = JsonArray() + overrides.stream().map { obj: OverrideBuilder -> obj.toJson() } + .forEach { element: JsonObject? -> + overridesJson.add( + element + ) + } + root.add("overrides", overridesJson) + } + return root + } + + /** Builder for a single `overrides` entry: a [model] shown when its [predicate]s are all satisfied. */ + inner class OverrideBuilder + { + private var model: AModelFile? = null + private val predicates: MutableMap = LinkedHashMap() + + /** Sets the model to use when this override's predicates match. */ + fun model(model: AModelFile): OverrideBuilder + { + this.model = model + return this + } + + /** Requires item property [key] to be at least [value] for this override to apply. */ + fun predicate(key: ResourceLocation, value: Float): OverrideBuilder + { + predicates[key] = value + return this + } + + /** Returns to the enclosing [AItemModelBuilder]. */ + fun end(): AItemModelBuilder + { + return this@AItemModelBuilder + } + + fun toJson(): JsonObject + { + val ret = JsonObject() + val predicatesJson = JsonObject() + predicates.forEach { (key: ResourceLocation, `val`: Float?) -> + predicatesJson.addProperty( + key.toString(), + `val` + ) + } + ret.add("predicate", predicatesJson) + ret.addProperty("model", model?.location.toString()) + return ret + } + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelProvider.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelProvider.kt new file mode 100644 index 000000000..35b4849e4 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelProvider.kt @@ -0,0 +1,30 @@ +package net.kernelpanicsoft.archie.data.client.model + +import dev.architectury.platform.Mod +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.data.PackOutput +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.Item +import java.util.* + +/** [AModelProvider] that generates item models under `models/item/`. */ +abstract class AItemModelProvider(output: PackOutput, mod: Mod, exitOnError: Boolean) : + AModelProvider(output, mod, ITEM_FOLDER, ::AItemModelBuilder, exitOnError) +{ + /** Registers a `item/generated` model for [item] using its own `item/` texture as `layer0`. */ + fun basicItem(item: Item, block: AItemModelBuilder.() -> Unit = {}): AItemModelBuilder + { + return basicItem(Objects.requireNonNull(BuiltInRegistries.ITEM.getKey(item)), block) + } + + /** Registers a `item/generated` model at [item] using an `item/` texture as `layer0`. */ + fun basicItem(item: ResourceLocation, block: AItemModelBuilder.() -> Unit = {}): AItemModelBuilder + { + return getBuilder(item.toString()) { + parent(AModelFile("item/generated")) + texture("layer0", ResourceLocation.fromNamespaceAndPath(item.namespace, "item/${item.path}")) + }.apply(block) + } + + override fun getName(): String = format("Item Models") +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt new file mode 100644 index 000000000..4338a6221 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt @@ -0,0 +1,1386 @@ +package net.kernelpanicsoft.archie.data.client.model + +import com.google.common.base.Preconditions +import com.google.gson.* +import com.mojang.blaze3d.vertex.PoseStack +import com.mojang.datafixers.util.Either +import com.mojang.math.Transformation +import com.mojang.serialization.Codec +import com.mojang.serialization.JsonOps +import com.mojang.serialization.codecs.RecordCodecBuilder +import net.kernelpanicsoft.archie.data.util.TransformationHelper +import dev.architectury.platform.Platform +import net.minecraft.client.renderer.block.model.* +import net.minecraft.client.renderer.texture.MissingTextureAtlasSprite +import net.minecraft.core.Direction +import net.minecraft.resources.ResourceLocation +import net.minecraft.util.GsonHelper +import net.minecraft.util.Mth +import net.minecraft.world.item.ItemDisplayContext +import org.joml.Quaternionf +import org.joml.Vector3f +import java.lang.reflect.Type +import java.util.* +import java.util.function.* +import java.util.function.Function +import java.util.stream.Collectors +import kotlin.Any +import kotlin.Boolean +import kotlin.Char +import kotlin.Float +import kotlin.FloatArray +import kotlin.IllegalArgumentException +import kotlin.Int +import kotlin.NullPointerException +import kotlin.Number +import kotlin.String +import kotlin.Suppress +import kotlin.Throws +import kotlin.Unit +import kotlin.apply +import kotlin.checkNotNull +import kotlin.floatArrayOf +import kotlin.toString + +/** + * Base fluent builder for a block/item model JSON at [location], mirroring NeoForge's vanilla + * `ModelBuilder` datagen helper 1:1. Set [parent], [texture]s, [renderType], [ao]/[guiLight], and + * either inline [element]s or [customLoader] data, plus display [transforms]/[rootTransforms]. + * Subclassed by [ABlockModelBuilder] and [AItemModelBuilder]; obtained via [AModelProvider]. + */ +@Suppress("unused") +open class AModelBuilder>(location: ResourceLocation) : AModelFile(location) +{ + protected var parent: AModelFile? = null + protected val textures: MutableMap = linkedMapOf() + protected val transforms: TransformsBuilder = TransformsBuilder() + + protected var renderType: String? = null + protected var ambientOcclusion: Boolean = true + protected var guiLight: BlockModel.GuiLight? = null + + protected val elements: MutableList = mutableListOf() + + protected var customLoader: ACustomLoaderBuilder? = null + + private val rootTransforms: RootTransformsBuilder = RootTransformsBuilder() + + @Suppress("UNCHECKED_CAST") + private val self: T + get() = this as T + + operator fun invoke(block: AModelBuilder.() -> Unit) = apply(block) + + /** Sets the model's `parent` reference to [parent]. */ + fun parent(parent: AModelFile): T + { + Preconditions.checkNotNull(parent, "Parent must not be null") + this.parent = parent + return self + } + + /** Binds texture variable [key] to [texture] (a texture id, or a `#other_key` reference). */ + fun texture(key: String, texture: String): T + { + Preconditions.checkNotNull(key, "Key must not be null") + Preconditions.checkNotNull(texture, "Texture must not be null") + if (texture[0] == '#') + { + textures[key] = texture + return self + } else + { + val asLoc: ResourceLocation = if (texture.contains(":")) + { + ResourceLocation.parse(texture) + } else + { + ResourceLocation.fromNamespaceAndPath(location.namespace, texture) + } + return texture(key, asLoc) + } + } + + /** Binds texture variable [key] to [texture]. */ + fun texture(key: String, texture: ResourceLocation): T + { + Preconditions.checkNotNull(key, "Key must not be null") + Preconditions.checkNotNull(texture, "Texture must not be null") + textures[key] = texture.toString() + return self + } + + /** Sets the render type (parsed as a [ResourceLocation]) used to draw this model. */ + fun renderType(renderType: String): T + { + Preconditions.checkNotNull(renderType, "Render type must not be null") + return renderType(ResourceLocation.parse(renderType)) + } + + /** Sets the render type used to draw this model. */ + fun renderType(renderType: ResourceLocation): T + { + Preconditions.checkNotNull(renderType, "Render type must not be null") + this.renderType = renderType.toString() + return self + } + + /** Configures the vanilla per-[ItemDisplayContext] `display` transforms via [block]. */ + fun transforms(block: TransformsBuilder.() -> Unit = {}): TransformsBuilder + { + return transforms.apply(block) + } + + /** Sets whether ambient occlusion is used when rendering this model. */ + fun ao(ao: Boolean): T + { + this.ambientOcclusion = ao + return self + } + + /** Sets whether this model is lit from the front (GUI-style) or diagonally (3D-style); `null` to inherit from [parent]. */ + fun guiLight(light: BlockModel.GuiLight?): T + { + this.guiLight = light + return self + } + + /** Adds a new inline cuboid element, configured by [block]. Throws if [customLoader] disallows inline elements. */ + fun element(block: ElementBuilder.() -> Unit = {}): ElementBuilder + { + Preconditions.checkState( + customLoader == null || customLoader!!.allowInlineElements, + "Custom model loader %s does not support inline elements", + customLoader?.loaderId + ) + val ret = ElementBuilder().apply(block) + elements.add(ret) + return ret + } + + /** Reconfigures the existing element at [index] with [block]. */ + fun element(index: Int, block: ElementBuilder.() -> Unit = {}): ElementBuilder + { + Preconditions.checkState( + customLoader == null || customLoader!!.allowInlineElements, + "Custom model loader %s does not support inline elements", + customLoader?.loaderId + ) + Preconditions.checkElementIndex(index, elements.size, "Element index") + return elements[index].apply(block) + } + + /** The number of inline [element]s configured so far. */ + fun getElementCount(): Int + { + return elements.size + } + + /** + * Builds and attaches an [ACustomLoaderBuilder] via [customLoaderFactory], replacing vanilla + * element-based geometry with a custom loader's own JSON. Forge-like loaders only. + */ + fun ?> customLoader(customLoaderFactory: Function): L + { + check(Platform.isForgeLike()) { "Custom Loader only supported on forge like loaders" } + val customLoader = customLoaderFactory.apply(self)!! + Preconditions.checkState( + customLoader.allowInlineElements || elements.isEmpty(), + "Custom model loader %s does not support inline elements", + customLoader.loaderId + ) + this.customLoader = customLoader + return customLoader + } + + /** Configures NeoForge's extended root-level (pre-display) transform via [block]. */ + fun rootTransforms(block: RootTransformsBuilder.() -> Unit = {}): RootTransformsBuilder + { + return rootTransforms.apply(block) + } + + private fun BlockElement.computeUvsByFace(face: Direction): FloatArray + { + when (face) + { + Direction.DOWN -> + { + return floatArrayOf(this.from.x(), 16.0f - this.to.z(), this.to.x(), 16.0f - this.from.z()) + } + + Direction.UP -> + { + return floatArrayOf(this.from.x(), this.from.z(), this.to.x(), this.to.z()) + } + + Direction.SOUTH -> + { + return floatArrayOf(this.from.x(), 16.0f - this.to.y(), this.to.x(), 16.0f - this.from.y()) + } + + Direction.WEST -> + { + return floatArrayOf(this.from.z(), 16.0f - this.to.y(), this.to.z(), 16.0f - this.from.y()) + } + + Direction.EAST -> + { + } + + else -> + { + return floatArrayOf( + 16.0f - this.to.x(), + 16.0f - this.to.y(), + 16.0f - this.from.x(), + 16.0f - this.from.y() + ) + } + } + return floatArrayOf(16.0f - this.to.z(), 16.0f - this.to.y(), 16.0f - this.from.z(), 16.0f - this.from.y()) + } + + open fun toJson(): JsonObject + { + val root = JsonObject() + + if (this.parent != null) + { + root.addProperty("parent", parent.toString()) + } + + if (!this.ambientOcclusion) + { + root.addProperty("ambientocclusion", this.ambientOcclusion) + } + + if (this.guiLight != null) + { + root.addProperty("gui_light", this.guiLight!!.name) + } + + if (this.renderType != null) + { + root.addProperty("render_type", this.renderType) + } + + val transforms: Map = + this.transforms.build() + if (transforms.isNotEmpty()) + { + val display = JsonObject() + for ((key, vec) in transforms) + { + val transform = JsonObject() + if (vec == PlatformItemTransform.NO_TRANSFORM) continue + val hasRightRotation: Boolean = + vec.rightRotation != PlatformItemTransform.Deserializer.DEFAULT_ROTATION + if (vec.translation != PlatformItemTransform.Deserializer.DEFAULT_TRANSLATION) + { + transform.add("translation", serializeVector3f(vec.translation)) + } + if (vec.rotation != PlatformItemTransform.Deserializer.DEFAULT_ROTATION) + { + transform.add( + if (hasRightRotation) "left_rotation" else "rotation", + serializeVector3f(vec.rotation) + ) + } + if (vec.scale != PlatformItemTransform.Deserializer.DEFAULT_SCALE) + { + transform.add("scale", serializeVector3f(vec.scale)) + } + if (hasRightRotation) + { + transform.add("right_rotation", serializeVector3f(vec.rightRotation)) + } + display.add(key.serializedName, transform) + } + root.add("display", display) + } + + if (textures.isNotEmpty()) + { + val textures = JsonObject() + for ((key, value) in this.textures) + { + textures.addProperty(key, serializeLocOrKey(value)) + } + root.add("textures", textures) + } + + if (this.elements.isNotEmpty()) + { + val elements = JsonArray() + this.elements.stream() + .map { obj: ElementBuilder -> obj.build() } + .forEach { part: BlockElement -> + val partObj = JsonObject() + partObj.add("from", serializeVector3f(part.from)) + partObj.add("to", serializeVector3f(part.to)) + + if (part.rotation != null) + { + val rotation = JsonObject() + rotation.add("origin", serializeVector3f(part.rotation.origin())) + rotation.addProperty("axis", part.rotation.axis().serializedName) + rotation.addProperty("angle", part.rotation.angle()) + if (part.rotation.rescale()) + { + rotation.addProperty("rescale", part.rotation.rescale()) + } + partObj.add("rotation", rotation) + } + + if (!part.shade) + { + partObj.addProperty("shade", part.shade) + } + + if (part is PlatformBlockElement) + { + if (part.faceData is PlatformFaceData.ExtraFaceData && part.faceData != PlatformFaceData.ExtraFaceData.DEFAULT) + { + partObj.add( + "neoforge_data", + PlatformFaceData.ExtraFaceData.CODEC.encodeStart( + JsonOps.INSTANCE, + part.faceData + ).result().get() + ) + } + } + + val faces = JsonObject() + for (dir in Direction.entries) + { + val face = part.faces[dir] ?: continue + + val faceObj = JsonObject() + faceObj.addProperty("texture", serializeLocOrKey(face.texture)) + if (!face.uv.uvs.contentEquals(part.computeUvsByFace(dir))) + { + faceObj.add("uv", Gson().toJsonTree(face.uv.uvs)) + } + if (face.cullForDirection != null) + { + faceObj.addProperty("cullface", face.cullForDirection!!.serializedName) + } + if (face.uv.rotation != 0) + { + faceObj.addProperty("rotation", face.uv.rotation) + } + if (face.tintIndex != -1) + { + faceObj.addProperty("tintindex", face.tintIndex) + } + if (face is PlatformBlockElementFace) + { + when + { + face.faceData is PlatformFaceData.ExtraFaceData && face.faceData != PlatformFaceData.ExtraFaceData.DEFAULT -> + { + faceObj.add( + "neoforge_data", + PlatformFaceData.ExtraFaceData.CODEC.encodeStart( + JsonOps.INSTANCE, + face.faceData + ).result().get() + ) + } + + face.faceData is PlatformFaceData.ForgeFaceData && face.faceData != PlatformFaceData.ForgeFaceData.DEFAULT -> + { + faceObj.add( + "forge_data", + PlatformFaceData.ForgeFaceData.CODEC.encodeStart( + JsonOps.INSTANCE, + face.faceData + ).result().get() + ) + } + } + } + + faces.add(dir.serializedName, faceObj) + } + if (!part.faces.isEmpty()) + { + partObj.add("faces", faces) + } + elements.add(partObj) + } + root.add("elements", elements) + } + + // If there were any transform properties set, add them to the output. + val transform: JsonObject = rootTransforms.toJson() + if (transform.size() > 0) + { + root.add("transform", transform) + } + + return customLoader?.toJson(root) ?: root + } + + private fun serializeLocOrKey(tex: String): String + { + if (tex[0] == '#') + { + return tex + } + return ResourceLocation.parse(tex).toString() + } + + private fun serializeVector3f(vec: Vector3f): JsonArray + { + val ret = JsonArray() + ret.add(serializeFloat(vec.x())) + ret.add(serializeFloat(vec.y())) + ret.add(serializeFloat(vec.z())) + return ret + } + + private fun serializeFloat(f: Float): Number + { + if (f.toInt().toFloat() == f) + { + return f.toInt() + } + return f + } + + /** Builder for one inline cuboid `elements` entry, added via [element]. */ + inner class ElementBuilder + { + private var from = Vector3f() + private var to = Vector3f(16f, 16f, 16f) + private val faces: MutableMap = LinkedHashMap() + private var rotation: RotationBuilder? = null + private var shade = true + private var color = -0x1 + private var blockLight = 0 + private var skyLight = 0 + private var hasAmbientOcclusion = true + + private fun validateCoordinate(coord: Float, name: Char) + { + Preconditions.checkArgument( + !(coord < -16.0f) && !(coord > 32.0f), + "Position $name out of range, must be within [-16, 32]. Found: %d", coord + ) + } + + private fun validatePosition(pos: Vector3f) + { + validateCoordinate(pos.x(), 'x') + validateCoordinate(pos.y(), 'y') + validateCoordinate(pos.z(), 'z') + } + + fun from(x: Float, y: Float, z: Float): ElementBuilder + { + this.from = Vector3f(x, y, z) + validatePosition(this.from) + return this + } + + fun to(x: Float, y: Float, z: Float): ElementBuilder + { + this.to = Vector3f(x, y, z) + validatePosition(this.to) + return this + } + + fun face(dir: Direction, block: FaceBuilder.() -> Unit = {}): FaceBuilder + { + Preconditions.checkNotNull(dir, "Direction must not be null") + return faces.computeIfAbsent( + dir + ) { dir: Direction -> + FaceBuilder( + dir + ) + }.apply(block) + } + + fun rotation(block: RotationBuilder.() -> Unit = {}): RotationBuilder + { + if (this.rotation == null) + { + this.rotation = RotationBuilder() + } + return this.rotation!!.apply(block) + } + + fun shade(shade: Boolean): ElementBuilder + { + this.shade = shade + return this + } + + fun allFaces(action: BiConsumer): ElementBuilder + { + Arrays.stream(Direction.entries.toTypedArray()) + .forEach { d: Direction -> + action.accept( + d, + face(d) + ) + } + return this + } + + fun faces(action: BiConsumer): ElementBuilder + { + faces.entries.stream() + .forEach { e: Map.Entry -> + action.accept( + e.key, + e.value + ) + } + return this + } + + fun textureAll(texture: String): ElementBuilder + { + return allFaces(addTexture(texture)) + } + + fun texture(texture: String): ElementBuilder + { + return faces(addTexture(texture)) + } + + fun cube(texture: String): ElementBuilder + { + return allFaces(addTexture(texture).andThen { dir: Direction?, f: FaceBuilder -> + f.cullface( + dir + ) + }) + } + + fun emissivity(blockLight: Int, skyLight: Int): ElementBuilder + { + this.blockLight = blockLight + this.skyLight = skyLight + return this + } + + fun color(color: Int): ElementBuilder + { + this.color = color + return this + } + + fun ao(ao: Boolean): ElementBuilder + { + this.hasAmbientOcclusion = ao + return this + } + + private fun addTexture(texture: String): BiConsumer + { + return BiConsumer { `$`: Direction?, f: FaceBuilder -> + f.texture( + texture + ) + } + } + + fun build(): BlockElement + { + val faces: Map = + faces.entries.stream() + .collect( + Collectors.toMap( + { it.key }, + { e: Map.Entry -> e.value.build() }, + { k1: BlockElementFace?, k2: BlockElementFace? -> + throw IllegalArgumentException() + }, + { LinkedHashMap() }) + ) + return PlatformBlockElement( + from, to, faces, if (rotation == null) null else rotation!!.build(), shade, when + { + Platform.isNeoForge() -> + PlatformFaceData.ExtraFaceData( + color = color, + blockLight = blockLight, + skyLight = skyLight, + ambientOcclusion = hasAmbientOcclusion + ) + + Platform.isMinecraftForge() -> + PlatformFaceData.ForgeFaceData( + color = color, + blockLight = blockLight, + skyLight = skyLight, + ambientOcclusion = hasAmbientOcclusion + ) + + else -> + PlatformFaceData.None + } + ) + } + + + fun end(): T + { + return self + } + + inner class FaceBuilder internal constructor(dir: Direction) + { + private var cullface: Direction? = null + private var tintindex = -1 + private var texture: String? = MissingTextureAtlasSprite.getLocation().toString() + private lateinit var uvs: FloatArray + private var rotation: FaceRotation = FaceRotation.ZERO + private var color = -0x1 + private var blockLight = 0 + private var skyLight = 0 + private var hasAmbientOcclusion = true + + fun cullface(dir: Direction?): FaceBuilder + { + this.cullface = dir + return this + } + + fun tintindex(index: Int): FaceBuilder + { + this.tintindex = index + return this + } + + fun texture(texture: String): FaceBuilder + { + Preconditions.checkNotNull(texture, "Texture must not be null") + this.texture = texture + return this + } + + fun uvs(u1: Float, v1: Float, u2: Float, v2: Float): FaceBuilder + { + this.uvs = floatArrayOf(u1, v1, u2, v2) + return this + } + + fun rotation(rot: FaceRotation): FaceBuilder + { + Preconditions.checkNotNull(rot, "Rotation must not be null") + this.rotation = rot + return this + } + + fun emissivity( + blockLight: Int, + skyLight: Int + ): FaceBuilder + { + this.blockLight = blockLight + this.skyLight = skyLight + return this + } + + fun color(color: Int): FaceBuilder + { + this.color = color + return this + } + + fun ao(ao: Boolean): FaceBuilder + { + this.hasAmbientOcclusion = ao + return this + } + + fun build(): BlockElementFace + { + checkNotNull(this.texture) { "A model face must have a texture" } + return PlatformBlockElementFace( + cullface, tintindex, texture!!, BlockFaceUV(uvs, rotation.rotation), when + { + Platform.isNeoForge() -> + PlatformFaceData.ExtraFaceData( + color = color, + blockLight = blockLight, + skyLight = skyLight, + ambientOcclusion = hasAmbientOcclusion + ) + + Platform.isMinecraftForge() -> + PlatformFaceData.ForgeFaceData( + color = color, + blockLight = blockLight, + skyLight = skyLight, + ambientOcclusion = hasAmbientOcclusion + ) + + else -> + PlatformFaceData.None + } + ) + } + + fun end(): ElementBuilder + { + return this@ElementBuilder + } + } + + inner class RotationBuilder + { + private lateinit var origin: Vector3f + private lateinit var axis: Direction.Axis + private var angle = 0f + private var rescale = false + + fun origin(x: Float, y: Float, z: Float): RotationBuilder + { + this.origin = Vector3f(x, y, z) + return this + } + + /** + * @param axis the axis of rotation + * @return this builder + * @throws NullPointerException if `axis` is `null` + */ + fun axis(axis: Direction.Axis): RotationBuilder + { + Preconditions.checkNotNull(axis, "Axis must not be null") + this.axis = axis + return this + } + + /** + * @param angle the rotation angle + * @return this builder + * @throws IllegalArgumentException if `angle` is invalid (not one of 0, +/-22.5, +/-45) + */ + fun angle(angle: Float): RotationBuilder + { + // Same logic from BlockPart.Deserializer#parseAngle + Preconditions.checkArgument( + angle == 0.0f || Mth.abs(angle) == 22.5f || Mth.abs( + angle + ) == 45.0f, "Invalid rotation %f found, only -45/-22.5/0/22.5/45 allowed", angle + ) + this.angle = angle + return this + } + + fun rescale(rescale: Boolean): RotationBuilder + { + this.rescale = rescale + return this + } + + fun build(): BlockElementRotation + { + return BlockElementRotation(origin, axis, angle, rescale) + } + + fun end(): ElementBuilder + { + return this@ElementBuilder + } + } + } + + enum class FaceRotation(val rotation: Int) + { + ZERO(0), + CLOCKWISE_90(90), + UPSIDE_DOWN(180), + COUNTERCLOCKWISE_90(270), + } + + class PlatformBlockElement( + from: Vector3f, to: Vector3f, faces: Map, + rotation: BlockElementRotation?, shade: Boolean, val faceData: PlatformFaceData + ) : BlockElement( + from, to, + faces, rotation, shade + ) + + class PlatformBlockElementFace( + cullForDirection: Direction?, + tintIndex: Int, texture: String, uv: BlockFaceUV, val faceData: PlatformFaceData + ) : BlockElementFace(cullForDirection, tintIndex, texture, uv) + + sealed class PlatformFaceData + { + data object None : PlatformFaceData() + data class ExtraFaceData( + val color: Int, + val blockLight: Int, + val skyLight: Int, + val ambientOcclusion: Boolean + ) : PlatformFaceData() + { + companion object + { + val DEFAULT: ExtraFaceData = ExtraFaceData(-0x1, 0, 0, true) + + val COLOR: Codec = Codec.either(Codec.INT, Codec.STRING).xmap( + { either: Either -> + either.map( + Function.identity() + ) { str: String -> + str.toLong(16).toInt() + } + }, + { color: Int? -> + Either.right( + Integer.toHexString( + color!! + ) + ) + }) + + val CODEC: Codec = + RecordCodecBuilder.create { builder: RecordCodecBuilder.Instance -> + builder + .group( + COLOR.optionalFieldOf("color", -0x1).forGetter(ExtraFaceData::color), + Codec.intRange(0, 15).optionalFieldOf("block_light", 0) + .forGetter(ExtraFaceData::blockLight), + Codec.intRange(0, 15).optionalFieldOf("sky_light", 0) + .forGetter(ExtraFaceData::skyLight), + Codec.BOOL.optionalFieldOf("ambient_occlusion", true) + .forGetter(ExtraFaceData::ambientOcclusion) + ) + .apply( + builder + ) { color: Int, blockLight: Int, skyLight: Int, ambientOcclusion: Boolean -> + ExtraFaceData( + color, + blockLight, + skyLight, + ambientOcclusion + ) + } + } + } + } + + data class ForgeFaceData( + val color: Int, + val blockLight: Int, + val skyLight: Int, + val ambientOcclusion: Boolean, + val calculateNormals: Boolean + ) : PlatformFaceData() + { + constructor(color: Int, blockLight: Int, skyLight: Int, ambientOcclusion: Boolean) : this( + color, + blockLight, + skyLight, + ambientOcclusion, + false + ) + + companion object + { + val DEFAULT: ForgeFaceData = ForgeFaceData(-0x1, 0, 0, true, false) + + val COLOR: Codec = Codec.either(Codec.INT, Codec.STRING).xmap( + { either: Either -> + either.map( + Function.identity() + ) { str: String -> + str.toLong(16).toInt() + } + }, + { color: Int? -> + Either.right( + Integer.toHexString( + color!! + ) + ) + }) + + val CODEC: Codec = + RecordCodecBuilder.create { builder: RecordCodecBuilder.Instance -> + builder.group( + COLOR.optionalFieldOf("color", -0x1).forGetter(ForgeFaceData::color), + Codec.intRange(0, 15).optionalFieldOf("block_light", 0) + .forGetter(ForgeFaceData::blockLight), + Codec.intRange(0, 15).optionalFieldOf("sky_light", 0) + .forGetter(ForgeFaceData::skyLight), + Codec.BOOL.optionalFieldOf("ambient_occlusion", true) + .forGetter(ForgeFaceData::ambientOcclusion), + Codec.BOOL.optionalFieldOf("calculate_normals", false) + .forGetter(ForgeFaceData::calculateNormals) + ) + .apply( + builder + ) { color: Int, blockLight: Int, skyLight: Int, ambientOcclusion: Boolean, calculateNormals: Boolean -> + ForgeFaceData( + color, + blockLight, + skyLight, + ambientOcclusion, + calculateNormals + ) + } + } + } + } + } + + inner class TransformsBuilder + { + private val transforms: MutableMap = LinkedHashMap() + + /** + * Begin building a new transform for the given perspective. + * + * @param type the perspective to create or return the builder for + * @return the builder for the given perspective + * @throws NullPointerException if `type` is `null` + */ + fun transform(type: ItemDisplayContext): TransformVecBuilder + { + Preconditions.checkNotNull(type, "Perspective cannot be null") + return transforms.computeIfAbsent( + type + ) { type: ItemDisplayContext? -> + TransformVecBuilder( + type + ) + } + } + + fun build(): Map + { + return transforms.entries.stream() + .collect( + Collectors.toMap( + { it.key }, + { e: Map.Entry -> e.value.build() }, + { k1: PlatformItemTransform?, k2: PlatformItemTransform? -> + throw java.lang.IllegalArgumentException() + }, + { LinkedHashMap() }) + ) + } + + fun end(): T + { + return self + } + + inner class TransformVecBuilder internal constructor(type: ItemDisplayContext?) + { + private var rotation = Vector3f(PlatformItemTransform.Deserializer.DEFAULT_ROTATION) + private var translation = Vector3f(PlatformItemTransform.Deserializer.DEFAULT_TRANSLATION) + private var scale = Vector3f(PlatformItemTransform.Deserializer.DEFAULT_SCALE) + private var rightRotation = Vector3f(PlatformItemTransform.Deserializer.DEFAULT_ROTATION) + + fun rotation(x: Float, y: Float, z: Float): TransformVecBuilder + { + this.rotation = Vector3f(x, y, z) + return this + } + + fun leftRotation(x: Float, y: Float, z: Float): TransformVecBuilder + { + return rotation(x, y, z) + } + + fun translation(x: Float, y: Float, z: Float): TransformVecBuilder + { + this.translation = Vector3f(x, y, z) + return this + } + + fun scale(sc: Float): TransformVecBuilder + { + return scale(sc, sc, sc) + } + + fun scale(x: Float, y: Float, z: Float): TransformVecBuilder + { + this.scale = Vector3f(x, y, z) + return this + } + + fun rightRotation( + x: Float, + y: Float, + z: Float + ): TransformVecBuilder + { + this.rightRotation = Vector3f(x, y, z) + return this + } + + fun build(): PlatformItemTransform + { + return PlatformItemTransform(rotation, translation, scale, rightRotation) + } + + fun end(): TransformsBuilder + { + return this@TransformsBuilder + } + } + } + + class PlatformItemTransform(rotation: Vector3f, translation: Vector3f, scale: Vector3f, rightRotation: Vector3f) + { + val rotation: Vector3f = Vector3f(rotation) + val translation: Vector3f = Vector3f(translation) + val scale: Vector3f = Vector3f(scale) + val rightRotation: Vector3f = Vector3f(rightRotation) + + constructor(rotation: Vector3f, translation: Vector3f, scale: Vector3f) : this( + rotation, + translation, + scale, + Vector3f() + ) + + fun apply(leftHand: Boolean, poseStack: PoseStack) + { + if (this !== NO_TRANSFORM) + { + val f = rotation.x() + var f1 = rotation.y() + var f2 = rotation.z() + if (leftHand) + { + f1 = -f1 + f2 = -f2 + } + val i = if (leftHand) -1 else 1 + poseStack.translate( + i.toFloat() * translation.x(), + translation.y(), translation.z() + ) + poseStack.mulPose( + Quaternionf().rotationXYZ( + f * (Math.PI.toFloat() / 180), + f1 * (Math.PI.toFloat() / 180), + f2 * (Math.PI.toFloat() / 180) + ) + ) + poseStack.scale(scale.x(), scale.y(), scale.z()) + poseStack.mulPose( + TransformationHelper.quatFromXYZ( + rightRotation.x(), + rightRotation.y() * (if (leftHand) -1 else 1).toFloat(), + rightRotation.z() * (if (leftHand) -1 else 1).toFloat(), true + ) + ) + } + } + + override fun equals(other: Any?): Boolean + { + if (this === other) + { + return true + } + if (this.javaClass != other?.javaClass) + { + return false + } + val itemtransform = other as PlatformItemTransform + return this.rotation == itemtransform.rotation && (this.scale == itemtransform.scale) && (this.translation == itemtransform.translation) + } + + override fun hashCode(): Int + { + var i = rotation.hashCode() + i = 31 * i + translation.hashCode() + return 31 * i + scale.hashCode() + } + + class Deserializer protected constructor() : JsonDeserializer + { + @Throws(JsonParseException::class) + override fun deserialize( + json: JsonElement, + type: Type, + context: JsonDeserializationContext + ): PlatformItemTransform + { + val jsonObject = json.asJsonObject + val vector3f = this.getVector3f(jsonObject, "rotation", DEFAULT_ROTATION) + val vector3f2 = this.getVector3f(jsonObject, "translation", DEFAULT_TRANSLATION) + vector3f2.mul(0.0625f) + vector3f2[Mth.clamp(vector3f2.x, -5.0f, 5.0f), Mth.clamp(vector3f2.y, -5.0f, 5.0f)] = + Mth.clamp(vector3f2.z, -5.0f, 5.0f) + val vector3f3 = this.getVector3f(jsonObject, "scale", DEFAULT_SCALE) + vector3f3[Mth.clamp(vector3f3.x, -4.0f, 4.0f), Mth.clamp(vector3f3.y, -4.0f, 4.0f)] = + Mth.clamp(vector3f3.z, -4.0f, 4.0f) + val rightRotation = + this.getVector3f(jsonObject, "right_rotation", DEFAULT_ROTATION) + return PlatformItemTransform(vector3f, vector3f2, vector3f3, rightRotation) + } + + private fun getVector3f(json: JsonObject, key: String, fallback: Vector3f): Vector3f + { + if (!json.has(key)) + { + return fallback + } + val jsonArray = GsonHelper.getAsJsonArray(json, key) + if (jsonArray.size() != 3) + { + throw JsonParseException("Expected 3 " + key + " values, found: " + jsonArray.size()) + } + val fs = FloatArray(3) + for (i in fs.indices) + { + fs[i] = GsonHelper.convertToFloat(jsonArray[i], "$key[$i]") + } + return Vector3f(fs[0], fs[1], fs[2]) + } + + companion object + { + val DEFAULT_ROTATION: Vector3f = Vector3f(0.0f, 0.0f, 0.0f) + val DEFAULT_TRANSLATION: Vector3f = Vector3f(0.0f, 0.0f, 0.0f) + val DEFAULT_SCALE: Vector3f = Vector3f(1.0f, 1.0f, 1.0f) + const val MAX_TRANSLATION: Float = 5.0f + const val MAX_SCALE: Float = 4.0f + } + } + + companion object + { + val NO_TRANSFORM: PlatformItemTransform = + PlatformItemTransform(Vector3f(), Vector3f(), Vector3f(1.0f, 1.0f, 1.0f)) + } + } + + inner class RootTransformsBuilder internal constructor() + { + private var translation = Vector3f() + private var leftRotation = Quaternionf() + private var rightRotation = Quaternionf() + private var scale = ONE + + private var origin: TransformationHelper.TransformOrigin? = null + private var originVec: Vector3f? = null + + /** + * Sets the translation of the root transform. + * + * @param translation the translation + * @return this builder + * @throws NullPointerException if `translation` is `null` + */ + fun translation(translation: Vector3f?): RootTransformsBuilder + { + this.translation = Preconditions.checkNotNull(translation, "Translation must not be null") + return this + } + + /** + * Sets the translation of the root transform. + * + * @param x x translation + * @param y y translation + * @param z z translation + * @return this builder + */ + fun translation(x: Float, y: Float, z: Float): RootTransformsBuilder + { + return translation(Vector3f(x, y, z)) + } + + /** + * Sets the left rotation of the root transform. + * + * @param rotation the left rotation + * @return this builder + * @throws NullPointerException if `rotation` is `null` + */ + fun rotation(rotation: Quaternionf?): RootTransformsBuilder + { + this.leftRotation = Preconditions.checkNotNull(rotation, "Rotation must not be null") + return this + } + + /** + * Sets the left rotation of the root transform. + * + * @param x x rotation + * @param y y rotation + * @param z z rotation + * @param isDegrees whether the rotation is in degrees or radians + * @return this builder + */ + fun rotation(x: Float, y: Float, z: Float, isDegrees: Boolean): RootTransformsBuilder + { + return rotation(TransformationHelper.quatFromXYZ(x, y, z, isDegrees)) + } + + /** + * Sets the left rotation of the root transform. + * + * @param leftRotation the left rotation + * @return this builder + * @throws NullPointerException if `leftRotation` is `null` + */ + fun leftRotation(leftRotation: Quaternionf?): RootTransformsBuilder + { + return rotation(leftRotation) + } + + fun leftRotation(x: Float, y: Float, z: Float, isDegrees: Boolean): RootTransformsBuilder + { + return leftRotation(TransformationHelper.quatFromXYZ(x, y, z, isDegrees)) + } + + fun rightRotation(rightRotation: Quaternionf?): RootTransformsBuilder + { + this.rightRotation = Preconditions.checkNotNull(rightRotation, "Rotation must not be null") + return this + } + + fun rightRotation(x: Float, y: Float, z: Float, isDegrees: Boolean): RootTransformsBuilder + { + return rightRotation( + TransformationHelper.quatFromXYZ( + x, + y, + z, + isDegrees + ) + ) + } + + fun postRotation(postRotation: Quaternionf?): RootTransformsBuilder + { + return rightRotation(postRotation) + } + + fun postRotation(x: Float, y: Float, z: Float, isDegrees: Boolean): RootTransformsBuilder + { + return postRotation(TransformationHelper.quatFromXYZ(x, y, z, isDegrees)) + } + + fun scale(scale: Float): RootTransformsBuilder + { + return scale(Vector3f(scale, scale, scale)) + } + + fun scale(xScale: Float, yScale: Float, zScale: Float): RootTransformsBuilder + { + return scale(Vector3f(xScale, yScale, zScale)) + } + + fun scale(scale: Vector3f?): RootTransformsBuilder + { + this.scale = Preconditions.checkNotNull(scale, "Scale must not be null") + return this + } + + fun transform(transformation: Transformation): RootTransformsBuilder + { + Preconditions.checkNotNull(transformation, "Transformation must not be null") + this.translation = transformation.translation + this.leftRotation = transformation.leftRotation + this.rightRotation = transformation.rightRotation + this.scale = transformation.scale + return this + } + + fun origin(origin: Vector3f?): RootTransformsBuilder + { + this.originVec = Preconditions.checkNotNull(origin, "Origin must not be null") + this.origin = null + return this + } + + fun origin(origin: TransformationHelper.TransformOrigin?): RootTransformsBuilder + { + this.origin = + Preconditions.checkNotNull( + origin, + "Origin must not be null" + ) + this.originVec = null + return this + } + + fun end(): AModelBuilder + { + return this@AModelBuilder + } + + fun toJson(): JsonObject + { + // Write the transform to an object + val transform = JsonObject() + + if (!translation.equals(0f, 0f, 0f)) + { + transform.add("translation", writeVec3(translation)) + } + + if (scale != ONE) + { + transform.add("scale", writeVec3(scale)) + } + + if (!leftRotation.equals(0f, 0f, 0f, 1f)) + { + transform.add("rotation", writeQuaternion(leftRotation)) + } + + if (!rightRotation.equals(0f, 0f, 0f, 1f)) + { + transform.add("post_rotation", writeQuaternion(rightRotation)) + } + + if (origin != null) + { + transform.addProperty("origin", origin!!.getSerializedName()) + } else if (originVec != null && !originVec!!.equals(0f, 0f, 0f)) + { + transform.add("origin", writeVec3(originVec!!)) + } + + return transform + } + } + + companion object + { + private val ONE = Vector3f(1f, 1f, 1f) + + private fun writeVec3(vector: Vector3f): JsonArray + { + val array = JsonArray() + array.add(vector.x()) + array.add(vector.y()) + array.add(vector.z()) + return array + } + + private fun writeQuaternion(quaternion: Quaternionf): JsonArray + { + val array = JsonArray() + array.add(quaternion.x()) + array.add(quaternion.y()) + array.add(quaternion.z()) + array.add(quaternion.w()) + return array + } + } + + +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelFile.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelFile.kt new file mode 100644 index 000000000..df0eb9c2b --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelFile.kt @@ -0,0 +1,15 @@ +package net.kernelpanicsoft.archie.data.client.model + +import net.minecraft.resources.ResourceLocation + +/** A reference to a model JSON file by [location], usable as another model's `parent` or a variant's model. */ +open class AModelFile(val location: ResourceLocation) +{ + constructor(location: String) : this(ResourceLocation.parse(location)) + + override fun toString(): String + { + return location.toString() + } + +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelProvider.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelProvider.kt new file mode 100644 index 000000000..e94bceeb4 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelProvider.kt @@ -0,0 +1,498 @@ +package net.kernelpanicsoft.archie.data.client.model + +import dev.architectury.platform.Mod +import com.google.common.base.Preconditions +import com.google.gson.Gson +import com.google.gson.GsonBuilder +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.IADataProvider +import net.minecraft.data.CachedOutput +import net.minecraft.data.DataProvider +import net.minecraft.data.PackOutput +import net.minecraft.resources.ResourceLocation +import org.jetbrains.annotations.VisibleForTesting +import java.nio.file.Path +import java.util.concurrent.CompletableFuture +import java.util.function.Function +import kotlin.system.exitProcess + +/** + * Datagen provider that builds model JSON files of builder type [T] (block or item models) + * under [folder], mirroring NeoForge's vanilla `ModelProvider` datagen helpers 1:1 in name and + * parameters (e.g. `cube`, `cubeAll`, `door*`, `fence*`, `pane*`, `trapdoor*`, `torch*`). Start a + * model with [getBuilder] or [withExistingParent], or use one of the vanilla-shape helpers. + */ +abstract class AModelProvider>( + final override val output: PackOutput, + final override val mod: Mod, + private val folder: String, + private val factory: Function, + final override val exitOnError: Boolean +) : IADataProvider +{ + @VisibleForTesting + val generatedModels: MutableMap = mutableMapOf() + + /** Called once during [generateAll] to register models via [getBuilder]/the shape helpers. */ + protected abstract fun generate() + + override fun getName(): String = format("Models") + + + /** Gets (or creates) the builder for the model at [path] (in [mod]'s namespace under [folder] unless [path] has its own namespace/subfolder), applying [block]. */ + fun getBuilder(path: String, block: T.() -> Unit = {}): T + { + Preconditions.checkNotNull(path, "Path must not be null") + val outputLoc = + extendWithFolder(if (path.contains(":")) ResourceLocation.parse(path) else ResourceLocation.fromNamespaceAndPath(mod.modId, path)) + return generatedModels.computeIfAbsent(outputLoc, factory).apply(block) + } + + private fun extendWithFolder(rl: ResourceLocation): ResourceLocation + { + if (rl.path.contains("/")) + { + return rl + } + return ResourceLocation.fromNamespaceAndPath(rl.namespace, folder + "/" + rl.path) + } + + /** [withExistingParent] with [parent] resolved as a `minecraft`-namespaced id. */ + fun withExistingParent(name: String, parent: String, block: T.() -> Unit = {}): T + { + return withExistingParent(name, mcLoc(parent)).apply(block) + } + + /** Gets (or creates) the builder for the model named [name], with its `parent` set to the existing model at [parent]. */ + fun withExistingParent(name: String, parent: ResourceLocation, block: T.() -> Unit = {}): T + { + return getBuilder(name).parent(getExistingFile(parent)).apply(block) + } + + fun cube( + name: String, + down: ResourceLocation, + up: ResourceLocation, + north: ResourceLocation, + south: ResourceLocation, + east: ResourceLocation, + west: ResourceLocation + ): T + { + return withExistingParent(name, "cube") + .texture("down", down) + .texture("up", up) + .texture("north", north) + .texture("south", south) + .texture("east", east) + .texture("west", west) + } + + private fun singleTexture(name: String, parent: String, texture: ResourceLocation): T + { + return singleTexture(name, mcLoc(parent), texture) + } + + fun singleTexture(name: String, parent: ResourceLocation, texture: ResourceLocation): T + { + return singleTexture(name, parent, "texture", texture) + } + + private fun singleTexture(name: String, parent: String, textureKey: String, texture: ResourceLocation): T + { + return singleTexture(name, mcLoc(parent), textureKey, texture) + } + + fun singleTexture(name: String, parent: ResourceLocation, textureKey: String, texture: ResourceLocation): T + { + return withExistingParent(name, parent) + .texture(textureKey, texture) + } + + fun cubeAll(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/cube_all", "all", texture) + } + + fun cubeTop(name: String, side: ResourceLocation, top: ResourceLocation): T + { + return withExistingParent(name, "$BLOCK_FOLDER/cube_top") + .texture("side", side) + .texture("top", top) + } + + private fun sideBottomTop( + name: String, + parent: String, + side: ResourceLocation, + bottom: ResourceLocation, + top: ResourceLocation + ): T + { + return withExistingParent(name, parent) + .texture("side", side) + .texture("bottom", bottom) + .texture("top", top) + } + + fun cubeBottomTop(name: String, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation): T + { + return sideBottomTop(name, "$BLOCK_FOLDER/cube_bottom_top", side, bottom, top) + } + + fun cubeColumn(name: String, side: ResourceLocation, end: ResourceLocation): T + { + return withExistingParent(name, "$BLOCK_FOLDER/cube_column") + .texture("side", side) + .texture("end", end) + } + + fun cubeColumnHorizontal(name: String, side: ResourceLocation, end: ResourceLocation): T + { + return withExistingParent(name, "$BLOCK_FOLDER/cube_column_horizontal") + .texture("side", side) + .texture("end", end) + } + + fun orientableVertical(name: String, side: ResourceLocation, front: ResourceLocation): T + { + return withExistingParent(name, "$BLOCK_FOLDER/orientable_vertical") + .texture("side", side) + .texture("front", front) + } + + fun orientableWithBottom( + name: String, + side: ResourceLocation, + front: ResourceLocation, + bottom: ResourceLocation, + top: ResourceLocation + ): T + { + return withExistingParent(name, "$BLOCK_FOLDER/orientable_with_bottom") + .texture("side", side) + .texture("front", front) + .texture("bottom", bottom) + .texture("top", top) + } + + fun orientable(name: String, side: ResourceLocation, front: ResourceLocation, top: ResourceLocation): T + { + return withExistingParent(name, "$BLOCK_FOLDER/orientable") + .texture("side", side) + .texture("front", front) + .texture("top", top) + } + + fun crop(name: String, crop: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/crop", "crop", crop) + } + + fun cross(name: String, cross: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/cross", "cross", cross) + } + + fun stairs(name: String, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation): T + { + return sideBottomTop(name, "$BLOCK_FOLDER/stairs", side, bottom, top) + } + + fun stairsOuter(name: String, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation): T + { + return sideBottomTop(name, "$BLOCK_FOLDER/outer_stairs", side, bottom, top) + } + + fun stairsInner(name: String, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation): T + { + return sideBottomTop(name, "$BLOCK_FOLDER/inner_stairs", side, bottom, top) + } + + fun slab(name: String, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation): T + { + return sideBottomTop(name, "$BLOCK_FOLDER/slab", side, bottom, top) + } + + fun slabTop(name: String, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation): T + { + return sideBottomTop(name, "$BLOCK_FOLDER/slab_top", side, bottom, top) + } + + fun button(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/button", texture) + } + + fun buttonPressed(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/button_pressed", texture) + } + + fun buttonInventory(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/button_inventory", texture) + } + + fun pressurePlate(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/pressure_plate_up", texture) + } + + fun pressurePlateDown(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/pressure_plate_down", texture) + } + + fun sign(name: String, texture: ResourceLocation): T + { + return getBuilder(name).texture("particle", texture) + } + + fun fencePost(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/fence_post", texture) + } + + fun fenceSide(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/fence_side", texture) + } + + fun fenceInventory(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/fence_inventory", texture) + } + + fun fenceGate(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_fence_gate", texture) + } + + fun fenceGateOpen(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_fence_gate_open", texture) + } + + fun fenceGateWall(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_fence_gate_wall", texture) + } + + fun fenceGateWallOpen(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_fence_gate_wall_open", texture) + } + + fun wallPost(name: String, wall: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_wall_post", "wall", wall) + } + + fun wallSide(name: String, wall: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_wall_side", "wall", wall) + } + + fun wallSideTall(name: String, wall: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_wall_side_tall", "wall", wall) + } + + fun wallInventory(name: String, wall: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/wall_inventory", "wall", wall) + } + + private fun pane(name: String, parent: String, pane: ResourceLocation, edge: ResourceLocation): T + { + return withExistingParent(name, "$BLOCK_FOLDER/$parent") + .texture("pane", pane) + .texture("edge", edge) + } + + fun panePost(name: String, pane: ResourceLocation, edge: ResourceLocation): T + { + return pane(name, "template_glass_pane_post", pane, edge) + } + + fun paneSide(name: String, pane: ResourceLocation, edge: ResourceLocation): T + { + return pane(name, "template_glass_pane_side", pane, edge) + } + + fun paneSideAlt(name: String, pane: ResourceLocation, edge: ResourceLocation): T + { + return pane(name, "template_glass_pane_side_alt", pane, edge) + } + + fun paneNoSide(name: String, pane: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_glass_pane_noside", "pane", pane) + } + + fun paneNoSideAlt(name: String, pane: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_glass_pane_noside_alt", "pane", pane) + } + + private fun door(name: String, model: String, bottom: ResourceLocation, top: ResourceLocation): T + { + return withExistingParent(name, "$BLOCK_FOLDER/$model") + .texture("bottom", bottom) + .texture("top", top) + } + + fun doorBottomLeft(name: String, bottom: ResourceLocation, top: ResourceLocation): T + { + return door(name, "door_bottom_left", bottom, top) + } + + fun doorBottomLeftOpen(name: String, bottom: ResourceLocation, top: ResourceLocation): T + { + return door(name, "door_bottom_left_open", bottom, top) + } + + fun doorBottomRight(name: String, bottom: ResourceLocation, top: ResourceLocation): T + { + return door(name, "door_bottom_right", bottom, top) + } + + fun doorBottomRightOpen(name: String, bottom: ResourceLocation, top: ResourceLocation): T + { + return door(name, "door_bottom_right_open", bottom, top) + } + + fun doorTopLeft(name: String, bottom: ResourceLocation, top: ResourceLocation): T + { + return door(name, "door_top_left", bottom, top) + } + + fun doorTopLeftOpen(name: String, bottom: ResourceLocation, top: ResourceLocation): T + { + return door(name, "door_top_left_open", bottom, top) + } + + fun doorTopRight(name: String, bottom: ResourceLocation, top: ResourceLocation): T + { + return door(name, "door_top_right", bottom, top) + } + + fun doorTopRightOpen(name: String, bottom: ResourceLocation, top: ResourceLocation): T + { + return door(name, "door_top_right_open", bottom, top) + } + + fun trapdoorBottom(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_trapdoor_bottom", texture) + } + + fun trapdoorTop(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_trapdoor_top", texture) + } + + fun trapdoorOpen(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_trapdoor_open", texture) + } + + fun trapdoorOrientableBottom(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_orientable_trapdoor_bottom", texture) + } + + fun trapdoorOrientableTop(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_orientable_trapdoor_top", texture) + } + + fun trapdoorOrientableOpen(name: String, texture: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_orientable_trapdoor_open", texture) + } + + fun torch(name: String, torch: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_torch", "torch", torch) + } + + fun torchWall(name: String, torch: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/template_torch_wall", "torch", torch) + } + + fun carpet(name: String, wool: ResourceLocation): T + { + return singleTexture(name, "$BLOCK_FOLDER/carpet", "wool", wool) + } + + /** A model builder that's not registered/saved to disk, meant for inline use inside a custom model loader's JSON. */ + fun nested(): T + { + return factory.apply(ResourceLocation.parse("dummy:dummy")) + } + + /** References the model at [path] (extended with [folder] if it has no subfolder) without requiring it to already be built by this provider. */ + fun getExistingFile(path: ResourceLocation): AModelFile + { + val ret = + AModelFile( + extendWithFolder(path) + ) + return ret + } + + /** Discards every model registered so far via [getBuilder]. */ + fun clear() + { + generatedModels.clear() + } + + override fun run(cache: CachedOutput): CompletableFuture<*> + { + clear() + runCatching { + generate() + }.onFailure { + Archie.LOGGER.error( + "Data Provider $name failed with exception: ${it.message}\n" + + "Stacktrace: ${it.stackTraceToString()}" + ) + if (exitOnError) exitProcess(-1) + } + return generateAll(cache) + } + + /** Writes every model currently registered via [getBuilder] to disk under [cache]. */ + fun generateAll(cache: CachedOutput): CompletableFuture<*> + { + val futures: Array?> = arrayOfNulls( + generatedModels.size + ) + + for ((i, model) in generatedModels.values.withIndex()) + { + val target = getPath(model) + futures[i] = DataProvider.saveStable(cache, model.toJson(), target) + } + + return CompletableFuture.allOf(*futures) + } + + protected fun getPath(model: T): Path + { + val loc: ResourceLocation = model.location + return output.getOutputFolder(PackOutput.Target.RESOURCE_PACK).resolve(loc.namespace).resolve("models") + .resolve(loc.path + ".json") + } + + companion object + { + const val BLOCK_FOLDER: String = "block" + const val ITEM_FOLDER: String = "item" + + private val GSON: Gson = GsonBuilder().setPrettyPrinting().create() + } +} diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AMultiPartBlockStateBuilder.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AMultiPartBlockStateBuilder.kt new file mode 100644 index 000000000..53a089ed7 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AMultiPartBlockStateBuilder.kt @@ -0,0 +1,271 @@ +package net.kernelpanicsoft.archie.data.client.model + +import com.google.common.base.Preconditions +import com.google.common.collect.Multimap +import com.google.common.collect.MultimapBuilder +import com.google.gson.JsonArray +import com.google.gson.JsonObject +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.state.properties.Property +import java.util.* + +/** + * Builds a `multipart`-style blockstate JSON for [owner], where each [PartBuilder] applies its + * model(s) whenever its `when` conditions (block property values, optionally grouped with + * AND/OR) match. Obtain via [ABlockStateProvider.getMultipartBuilder]. + */ +class AMultiPartBlockStateBuilder(private val owner: Block) : IAGeneratedBlockState +{ + private val parts: MutableList = ArrayList() + + private var config: (PartBuilder.() -> Unit)? = null + + /** Starts an [AConfiguredModel.Builder] whose [AConfiguredModel.Builder.addModel] creates and adds a new unconditional [PartBuilder]. */ + fun part(): AConfiguredModel.Builder + { + return AConfiguredModel.builder(this) + } + + /** Builds a new [PartBuilder] with its model(s) declared in [block], adds it, and applies any pending [configure] callback. */ + fun part(block: AConfiguredModel.Builder.() -> Unit): PartBuilder + { + return AConfiguredModel.builder(this) + .apply(block) + .addModel() + .apply { + config?.let { it() } + } + } + + /** Registers an already-built [part]. */ + fun addPart(part: PartBuilder): AMultiPartBlockStateBuilder + { + parts.add(part) + return this + } + + /** Sets a callback run against every part built by [part] after this call, e.g. to add shared conditions. */ + fun configure(block: (PartBuilder.() -> Unit)?) + { + config = block + } + + /** Serializes to the `{"multipart": [...]}` blockstate JSON. */ + override fun toJson(): JsonObject + { + val variants = JsonArray() + for (part in parts) + { + variants.add(part.toJson()) + } + val main = JsonObject() + main.add("multipart", variants) + return main + } + + /** A single `multipart` entry: applies [models] when its (possibly nested) conditions match. */ + inner class PartBuilder internal constructor(models: ABlockStateProvider.ConfiguredModelList) + { + var models: ABlockStateProvider.ConfiguredModelList = models + var useOr: Boolean = false + val conditions: Multimap, Comparable<*>> = + MultimapBuilder.linkedHashKeys().arrayListValues().build() + val nestedConditionGroups: MutableList = ArrayList() + + /** Combines this part's [conditions] with OR instead of the default AND. */ + fun useOr(): PartBuilder + { + this.useOr = true + return this + } + + /** Requires [prop] to equal one of [values] (OR'd together) for this part to apply. Cannot be mixed with [nestedGroup]. */ + @SafeVarargs + fun > condition(prop: Property, vararg values: T): PartBuilder + { + Preconditions.checkNotNull(prop, "Property must not be null") + Preconditions.checkNotNull(values, "Value list must not be null") + Preconditions.checkArgument(values.isNotEmpty(), "Value list must not be empty") + Preconditions.checkArgument( + !conditions.containsKey(prop), + "Cannot set condition for property \"%s\" more than once", + prop.name + ) + Preconditions.checkArgument( + canApplyTo(owner), "IProperty %s is not valid for the block %s", prop, + owner + ) + Preconditions.checkState( + nestedConditionGroups.isEmpty(), + "Can't have normal conditions if there are already nested condition groups" + ) + conditions.putAll(prop, listOf(*values)) + return this + } + + /** Starts a nested [ConditionGroup] under this part. Cannot be mixed with [condition]. */ + fun nestedGroup(): ConditionGroup + { + Preconditions.checkState( + conditions.isEmpty, + "Can't have nested condition groups if there are already normal conditions" + ) + val group = ConditionGroup() + nestedConditionGroups.add(group) + return group + } + + /** Returns to the enclosing [AMultiPartBlockStateBuilder]. */ + fun end(): AMultiPartBlockStateBuilder + { + return this@AMultiPartBlockStateBuilder + } + + /** Serializes this part's `when`/`apply` entry. */ + fun toJson(): JsonObject + { + val out = JsonObject() + if (!conditions.isEmpty) + { + out.add("when", toJson(this.conditions, this.useOr)) + } else if (nestedConditionGroups.isNotEmpty()) + { + out.add("when", toJson(this.nestedConditionGroups, this.useOr)) + } + out.add("apply", models.toJSON()) + return out + } + + /** Whether every property referenced by this part's conditions exists on [b]. */ + fun canApplyTo(b: Block): Boolean + { + return b.stateDefinition.properties.containsAll(conditions.keySet()) + } + + /** A nested AND/OR group of conditions within a [PartBuilder]'s `when` clause. */ + inner class ConditionGroup + { + val conditions: Multimap, Comparable<*>> = + MultimapBuilder.linkedHashKeys().arrayListValues().build() + val nestedConditionGroups: MutableList = ArrayList() + private var parent: ConditionGroup? = null + var useOr: Boolean = false + + @SafeVarargs + fun ?> condition(prop: Property, vararg values: T): ConditionGroup + { + Preconditions.checkNotNull(prop, "Property must not be null") + Preconditions.checkNotNull(values, "Value list must not be null") + Preconditions.checkArgument(values.isNotEmpty(), "Value list must not be empty") + Preconditions.checkArgument( + !conditions.containsKey(prop), + "Cannot set condition for property \"%s\" more than once", + prop.name + ) + Preconditions.checkArgument( + canApplyTo(owner), "IProperty %s is not valid for the block %s", prop, + owner + ) + Preconditions.checkState( + nestedConditionGroups.isEmpty(), + "Can't have normal conditions if there are already nested condition groups" + ) + this.conditions.putAll(prop, listOf(*values)) + return this + } + + fun nestedGroup(): ConditionGroup + { + Preconditions.checkState( + conditions.isEmpty, + "Can't have nested condition groups if there are already normal conditions" + ) + val group = ConditionGroup() + group.parent = this + this.nestedConditionGroups.add(group) + return group + } + + fun endNestedGroup(): ConditionGroup + { + checkNotNull(parent) { "This condition group is not nested, use end() instead" } + return parent!! + } + + fun end(): PartBuilder + { + check(this.parent == null) { "This is a nested condition group, use endNestedGroup() instead" } + return this@PartBuilder + } + + fun useOr(): ConditionGroup + { + this.useOr = true + return this + } + + fun toJson(): JsonObject + { + if (!this.conditions.isEmpty) + { + return toJson(this.conditions, this.useOr) + } else if (this.nestedConditionGroups.isNotEmpty()) + { + return toJson(this.nestedConditionGroups, this.useOr) + } + return JsonObject() + } + } + } + + companion object + { + @Suppress("UNCHECKED_CAST") + private fun propertyValueName(key: Property<*>, value: Comparable<*>): String + { + val typedKey = key as Property> + val typedValue = value as Comparable + return typedKey.getName(typedValue) + } + + private fun toJson(conditions: List, useOr: Boolean): JsonObject + { + val groupJson = JsonObject() + val innerGroupJson = JsonArray() + groupJson.add(if (useOr) "OR" else "AND", innerGroupJson) + for (group in conditions) + { + innerGroupJson.add(group.toJson()) + } + return groupJson + } + + private fun toJson(conditions: Multimap, Comparable<*>>, useOr: Boolean): JsonObject + { + var groupJson = JsonObject() + for ((key, value) in conditions.asMap()) + { + val activeString = StringBuilder() + for (`val` in value) + { + if (activeString.isNotEmpty()) activeString.append("|") + activeString.append(propertyValueName(key, `val`)) + } + groupJson.addProperty(key.name, activeString.toString()) + } + if (useOr) + { + val innerWhen = JsonArray() + for ((key, value) in groupJson.entrySet()) + { + val obj = JsonObject() + obj.add(key, value) + innerWhen.add(obj) + } + groupJson = JsonObject() + groupJson.add("OR", innerWhen) + } + return groupJson + } + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AVariantBlockStateBuilder.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AVariantBlockStateBuilder.kt new file mode 100644 index 000000000..fd9df7e16 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AVariantBlockStateBuilder.kt @@ -0,0 +1,328 @@ +package net.kernelpanicsoft.archie.data.client.model + +import com.google.common.base.Preconditions +import com.google.common.collect.ImmutableMap +import com.google.common.collect.Lists +import com.google.common.collect.Maps +import com.google.gson.JsonObject +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.state.BlockState +import net.minecraft.world.level.block.state.properties.Property +import java.util.* +import java.util.function.Function +import java.util.function.Predicate + +/** + * Builds a `variants`-style blockstate JSON for [owner], mapping [PartialBlockstate] property + * combinations to one or more [AConfiguredModel]s. Obtain via + * [ABlockStateProvider.getVariantBuilder]; every possible [BlockState] of [owner] must end up + * covered by some registered [PartialBlockstate] before [toJson] is called (e.g. via + * [forAllStates]/[forAllStatesExcept], or individual [partialState] + [setModels] calls). + */ +class AVariantBlockStateBuilder internal constructor(val owner: Block) : IAGeneratedBlockState +{ + private val models: MutableMap = + LinkedHashMap() + private val coveredStates: MutableSet = HashSet() + + /** The configured models, keyed by the partial state they apply to. */ + fun getModels(): Map + { + return models + } + + /** Serializes to the `{"variants": {...}}` blockstate JSON. Throws if any state of [owner] is uncovered. */ + override fun toJson(): JsonObject + { + val missingStates: MutableList = Lists.newArrayList( + owner.stateDefinition.possibleStates + ) + missingStates.removeAll(coveredStates) + Preconditions.checkState( + missingStates.isEmpty(), "Blockstate for block %s does not cover all states. Missing: %s", + owner, missingStates + ) + val variants = JsonObject() + getModels().entries.stream() + .sorted(java.util.Map.Entry.comparingByKey(PartialBlockstate.comparingByProperties())) + .forEach { entry: Map.Entry -> + variants.add( + entry.key.toString(), + entry.value.toJSON() + ) + } + val main = JsonObject() + main.add("variants", variants) + return main + } + + /** Adds [models] as (further) choices for [state], appending if [state] was already configured. */ + fun addModels( + state: PartialBlockstate, + vararg models: AConfiguredModel + ): AVariantBlockStateBuilder + { + Preconditions.checkNotNull(state, "state must not be null") + Preconditions.checkArgument(models.isNotEmpty(), "Cannot set models to empty array") + Preconditions.checkArgument( + state.owner === owner, "Cannot set models for a different block. Found: %s, Current: %s", + state.owner, owner + ) + if (!this.models.containsKey(state)) + { + Preconditions.checkArgument( + disjointToAll(state), + "Cannot set models for a state for which a partial match has already been configured" + ) + this.models[state] = ABlockStateProvider.ConfiguredModelList(*models) + for (fullState in owner.stateDefinition.possibleStates) + { + if (state.test(fullState)) + { + coveredStates.add(fullState) + } + } + } else + { + this.models.compute(state + ) { _: PartialBlockstate, cml: ABlockStateProvider.ConfiguredModelList? -> + cml?.append( + *models + ) + } + } + return this + } + + /** Sets [model] as the choices for [state]. Throws if [state] was already configured. */ + fun setModels( + state: PartialBlockstate, + vararg model: AConfiguredModel + ): AVariantBlockStateBuilder + { + Preconditions.checkArgument( + !models.containsKey(state), + "Cannot set models for a state that has already been configured: %s", + state + ) + addModels(state, *model) + return this + } + + private fun disjointToAll(newState: PartialBlockstate): Boolean + { + return coveredStates.stream().noneMatch(newState) + } + + /** Starts a new [PartialBlockstate] with no properties set yet, to be refined via [PartialBlockstate.with]. */ + fun partialState(): PartialBlockstate + { + return PartialBlockstate(owner, this) + } + + /** Configures every possible [BlockState] of [owner] individually via [mapper]. */ + fun forAllStates(mapper: Function>): AVariantBlockStateBuilder + { + return forAllStatesExcept(mapper) + } + + /** Like [forAllStates], but groups states that only differ in [ignored] properties under one partial state. */ + fun forAllStatesExcept( + mapper: Function>, + vararg ignored: Property<*> + ): AVariantBlockStateBuilder + { + val seen: MutableSet = HashSet() + for (fullState in owner.stateDefinition.possibleStates) + { + val propertyValues: MutableMap, Comparable<*>> = Maps.newLinkedHashMap(fullState.values) + for (p in ignored) + { + propertyValues.remove(p) + } + val partialState = PartialBlockstate( + owner, propertyValues, this + ) + if (seen.add(partialState)) + { + setModels(partialState, *mapper.apply(fullState)) + } + } + return this + } + + /** + * An immutable, partially or fully specified combination of block properties, matching every + * [BlockState] of [owner] that agrees with [setStates] (unset properties match any value). + * Refine with [with]; assign models with [addModels]/[setModels]/[modelForState]. + */ + class PartialBlockstate internal constructor( + val owner: Block, + setStates: Map, Comparable<*>>, + private val outerBuilder: AVariantBlockStateBuilder + ) : + Predicate + { + val setStates: SortedMap, Comparable<*>> + + internal constructor(owner: Block, outerBuilder: AVariantBlockStateBuilder) : this( + owner, + ImmutableMap.of, Comparable<*>>(), + outerBuilder + ) + + init + { + for (entry in setStates.entries) + { + val prop = entry.key + val value = entry.value + Preconditions.checkArgument( + owner.stateDefinition.properties.contains(prop), "Property %s not found on block %s", entry, + this.owner + ) + Preconditions.checkArgument( + prop.possibleValues.contains(value), + "%s is not a valid value for %s", + value, + prop + ) + } + this.setStates = Maps.newTreeMap(Comparator.comparing { obj: Property<*> -> obj.name }) + this.setStates.putAll(setStates) + } + + /** Returns a new [PartialBlockstate] with [prop] additionally pinned to [value]. Throws if [prop] is already set. */ + fun > with(prop: Property, value: T): PartialBlockstate + { + Preconditions.checkArgument(!setStates.containsKey(prop), "Property %s has already been set", prop) + val newState: MutableMap, Comparable<*>> = HashMap(setStates) + newState[prop] = value + return PartialBlockstate(owner, newState, outerBuilder) + } + + private fun checkValidOwner() + { + Preconditions.checkNotNull( + outerBuilder, + "Partial blockstate must have a valid owner to perform this action" + ) + } + + /** Starts an [AConfiguredModel.Builder] whose [AConfiguredModel.Builder.addModel] assigns the result to this state. */ + fun modelForState(): AConfiguredModel.Builder + { + checkValidOwner() + return AConfiguredModel.builder(outerBuilder, this) + } + + /** Adds [models] as (further) choices for this state; see [AVariantBlockStateBuilder.addModels]. */ + fun addModels(vararg models: AConfiguredModel): PartialBlockstate + { + checkValidOwner() + outerBuilder!!.addModels(this, *models) + return this + } + + /** Sets [models] as the choices for this state; see [AVariantBlockStateBuilder.setModels]. */ + fun setModels(vararg models: AConfiguredModel): AVariantBlockStateBuilder + { + checkValidOwner() + return outerBuilder!!.setModels(this, *models) + } + + /** Starts a new, unrelated [PartialBlockstate] on the same owning builder; see [AVariantBlockStateBuilder.partialState]. */ + fun partialState(): PartialBlockstate + { + checkValidOwner() + return outerBuilder!!.partialState() + } + + override fun equals(other: Any?): Boolean + { + if (this === other) return true + if (other == null || javaClass != other.javaClass) return false + val that = other as PartialBlockstate + return owner == that.owner && setStates == that.setStates + } + + override fun hashCode(): Int + { + return Objects.hash(owner, setStates) + } + + override fun test(blockState: BlockState): Boolean + { + if (blockState.block !== owner) + { + return false + } + for ((key, value) in setStates) + { + if (blockState.getValue(key) !== value) + { + return false + } + } + return true + } + + override fun toString(): String + { + val ret = StringBuilder() + for ((key, value) in setStates) + { + if (ret.isNotEmpty()) + { + ret.append(',') + } + @Suppress("UNCHECKED_CAST") + ret.append(key.name) + .append('=') + .append( + (key as Property>).getName( + value as Comparable + ) + ) + } + return ret.toString() + } + + companion object + { + /** Comparator ordering states by property values, approximating vanilla's blockstate JSON ordering. */ + fun comparingByProperties(): Comparator + { + // Sort variants inversely by property values, to approximate vanilla style + return Comparator { s1: PartialBlockstate, s2: PartialBlockstate -> + val propUniverse: SortedSet> = + TreeSet( + s1.setStates.comparator().reversed() + ) + propUniverse.addAll(s1.setStates.keys) + propUniverse.addAll(s2.setStates.keys) + for (prop in propUniverse) + { + val val1 = s1.setStates[prop] + val val2 = s2.setStates[prop] + if (val1 == null && val2 != null) + { + return@Comparator -1 + } else if (val2 == null && val1 != null) + { + return@Comparator 1 + } else if (val1 != null && val2 != null) + { + @Suppress("UNCHECKED_CAST") val cmp = (val1 as Comparable).compareTo(val2) + if (cmp != 0) + { + return@Comparator cmp + } + } + } + 0 + } + } + } + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/IAGeneratedBlockState.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/IAGeneratedBlockState.kt new file mode 100644 index 000000000..dc1bc95da --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/IAGeneratedBlockState.kt @@ -0,0 +1,10 @@ +package net.kernelpanicsoft.archie.data.client.model + +import com.google.gson.JsonObject + +/** Something that can be serialized as a complete `blockstates` JSON document, e.g. [AVariantBlockStateBuilder]/[AMultiPartBlockStateBuilder]. */ +interface IAGeneratedBlockState +{ + /** Serializes this blockstate to its JSON representation. */ + fun toJson(): JsonObject +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt new file mode 100644 index 000000000..f16f848c2 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt @@ -0,0 +1,62 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import net.minecraft.core.Registry +import net.minecraft.resources.ResourceKey +import net.minecraft.resources.ResourceLocation + +/** + * DSL for building [IACondition] trees with infix/operator combinators (`and`, `or`, `xor`, + * `eql`, their negated `n*` counterparts, and `!` operator aliases) plus factory + * functions for the leaf conditions ([mod], [registry], [platform], [TRUE], [FALSE]). + * + * Import the members (`import ...AConditionBuilder.*`) to write conditions like + * `mod("architectury") and platform(FABRIC)`. + */ +object AConditionBuilder +{ + /** [AAndCondition] of `this` and [other]. */ + + + fun and(vararg values: IACondition): IACondition = AAndCondition(*values) + fun or(vararg values: IACondition): IACondition = AOrCondition(*values) + fun xor(vararg values: IACondition): IACondition = AXorCondition(*values) + fun eql(vararg values: IACondition): IACondition = AEqualsCondition(*values) + + fun nand(vararg values: IACondition): IACondition = !and(*values) + fun nor(vararg values: IACondition): IACondition = !or(*values) + fun xnor(vararg values: IACondition): IACondition = !xor(*values) + fun neql(vararg values: IACondition): IACondition = !eql(*values) + + infix fun IACondition.and(other: IACondition): IACondition = and(this, other) + infix fun IACondition.or(other: IACondition): IACondition = or(this, other) + infix fun IACondition.xor(other: IACondition): IACondition = xor(this, other) + infix fun IACondition.eql(other: IACondition): IACondition = eql(this, other) + + infix fun IACondition.nand(other: IACondition): IACondition = !(this and other) + infix fun IACondition.nor(other: IACondition): IACondition = !(this or other) + infix fun IACondition.xnor(other: IACondition): IACondition = !(this xor other) + infix fun IACondition.neql(other: IACondition): IACondition = !(this eql other) + + operator fun IACondition.not(): IACondition = ANotCondition(this) + + /** Always-true condition; see [ATrueCondition]. */ + val TRUE = ATrueCondition + + /** Always-false condition; see [AFalseCondition]. */ + val FALSE = AFalseCondition + + /** Condition that holds when every mod id in [mods] is loaded. */ + fun mod(vararg mods: String): IACondition = AModLoadedCondition(*mods) + + /** Condition that holds when every one of [entries] is registered in [registry]. */ + fun registry(registry: ResourceKey>, vararg entries: ResourceLocation): IACondition = ARegistryCondition(registry.location(), *entries) + + /** Condition that holds when every one of [entries] is registered in [registry]. */ + fun registry(registry: Registry<*>, vararg entries: ResourceLocation): IACondition = ARegistryCondition(registry.key().location(), *entries) + + /** Condition that holds when the running loader's platform id equals [platform]; see [FABRIC]/[NEOFORGE]. */ + fun platform(platform: String): IACondition = APlatformCondition(platform) + + const val FABRIC = "fabric" + const val NEOFORGE = "neoforge" +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.common.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.common.kt new file mode 100644 index 000000000..a5ae75998 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.common.kt @@ -0,0 +1,20 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider +import net.minecraft.core.HolderLookup +import net.minecraft.data.recipes.RecipeOutput +import net.minecraft.data.recipes.RecipeProvider +import java.util.concurrent.CompletableFuture + +/** + * Datagen-only half of [AConditionsPlatform] - attaching conditions to a *generated* recipe. + * Split out from [AConditionsPlatform] itself since neither member has any runtime presence. + */ +expect object ADatagenConditionsPlatform +{ + /** Attaches [condition] to the next recipe written to [output] via the loader's native mechanism. */ + fun withCondition(output: RecipeOutput, condition: IACondition): RecipeOutput + + /** Wraps [child] in a loader-specific [RecipeProvider] so [withCondition] can attach conditions on Fabric; `null` where not needed. */ + fun fabricRecipeProvider(child: ARecipeProvider, registries: CompletableFuture): RecipeProvider? +} diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt new file mode 100644 index 000000000..62c67d288 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt @@ -0,0 +1,18 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import net.minecraft.data.recipes.RecipeOutput + +/** Attaches [condition] to the next recipe written to this [RecipeOutput]. */ +fun RecipeOutput.withCondition(condition: IACondition): RecipeOutput +{ + return withCondition { condition } +} + +/** Attaches the [IACondition] built by [block] (with [AConditionBuilder] in scope) to the next recipe written to this [RecipeOutput]. */ +fun RecipeOutput.withCondition(block: AConditionBuilder.() -> IACondition): RecipeOutput +{ + return ADatagenConditionsPlatform.withCondition(this, AConditionBuilder.block()) +} + +/** Builds an [IACondition] with [AConditionBuilder] in scope, e.g. `buildCondition { mod("architectury") }`. */ +inline fun buildCondition(block: AConditionBuilder.() -> IACondition): IACondition = AConditionBuilder.block() \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt new file mode 100644 index 000000000..e1eff9942 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt @@ -0,0 +1,109 @@ +package net.kernelpanicsoft.archie.data.common.crafting + +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.IADataProvider +import net.kernelpanicsoft.archie.data.common.conditions.ADatagenConditionsPlatform +import net.kernelpanicsoft.archie.data.common.crafting.recipies.ArchieCookingRecipeBuilder +import net.kernelpanicsoft.archie.data.common.crafting.recipies.ArchieShapedRecipeBuilder +import net.kernelpanicsoft.archie.data.common.crafting.recipies.ArchieShapelessRecipeBuilder +import dev.architectury.platform.Mod +import net.minecraft.core.HolderLookup +import net.minecraft.data.CachedOutput +import net.minecraft.data.PackOutput +import net.minecraft.data.recipes.* +import net.minecraft.tags.TagKey +import net.minecraft.world.item.Item +import net.minecraft.world.item.crafting.BlastingRecipe +import net.minecraft.world.item.crafting.CampfireCookingRecipe +import net.minecraft.world.item.crafting.SmeltingRecipe +import net.minecraft.world.item.crafting.SmokingRecipe +import net.minecraft.world.level.ItemLike +import java.util.concurrent.CompletableFuture +import kotlin.system.exitProcess + +/** + * Datagen provider that builds recipe JSONs on top of vanilla's [RecipeProvider]. Implement + * [generate] and use the `shaped`/`shapeless`/`smelting`/`blasting`/`smoking`/`cooking` DSL + * helpers, or vanilla's [RecipeBuilder]s directly, to register recipes into the given + * [RecipeOutput]. Use via [net.kernelpanicsoft.archie.data.ADataGenerator.Common.recipes]. + */ +@Suppress("unused") +abstract class ARecipeProvider( + override val output: PackOutput, + override val mod: Mod, + registries: CompletableFuture, + override val exitOnError: Boolean +) : RecipeProvider(output, registries), + IADataProvider +{ + + /** On Fabric, a wrapping [RecipeProvider] needed for [ADatagenConditionsPlatform.withCondition] support; `null` elsewhere. */ + private val fabricParent: RecipeProvider? = ADatagenConditionsPlatform.fabricRecipeProvider(this, registries) + + override fun run(cachedOutput: CachedOutput): CompletableFuture<*> + { + return if (fabricParent != null) + fabricParent.run(cachedOutput) + else + super.run(cachedOutput) + } + + final override fun buildRecipes(recipeOutput: RecipeOutput) + { + runCatching { + generate(recipeOutput) + }.onFailure { + Archie.LOGGER.error( + "Data Provider $name failed with exception: ${it.message}\n" + + "Stacktrace: ${it.stackTraceToString()}" + ) + if (exitOnError) exitProcess(-1) + } + } + + /** Called once during [buildRecipes] to register recipes into [recipeOutput]. */ + abstract fun generate(recipeOutput: RecipeOutput) + + override fun getName(): String = format("Recipes") + + /** Builds a shaped crafting recipe declared in [block]. */ + fun shaped( + block: ArchieShapedRecipeBuilder.() -> Unit + ): ArchieShapedRecipeBuilder = ArchieShapedRecipeBuilder.shaped(block) + + /** Builds a shapeless crafting recipe declared in [block]. */ + fun shapeless( + block: ArchieShapelessRecipeBuilder.() -> Unit + ): ArchieShapelessRecipeBuilder = ArchieShapelessRecipeBuilder.shapeless(block) + + /** Builds a furnace smelting recipe declared in [block]. */ + fun smelting( + block: ArchieCookingRecipeBuilder.() -> Unit + ): ArchieCookingRecipeBuilder = ArchieCookingRecipeBuilder.smelting(block) + + /** Builds a blast furnace recipe declared in [block]. */ + fun blasting( + block: ArchieCookingRecipeBuilder.() -> Unit + ): ArchieCookingRecipeBuilder = ArchieCookingRecipeBuilder.blasting(block) + + /** Builds a smoker recipe declared in [block]. */ + fun smoking( + block: ArchieCookingRecipeBuilder.() -> Unit + ): ArchieCookingRecipeBuilder = ArchieCookingRecipeBuilder.smoking(block) + + /** Builds a campfire cooking recipe declared in [block]. */ + fun cooking( + block: ArchieCookingRecipeBuilder.() -> Unit + ): ArchieCookingRecipeBuilder = ArchieCookingRecipeBuilder.cooking(block) + + /** Adds an unlock criterion requiring [ingredient] to have been obtained, named after it. */ + @Suppress("UNCHECKED_CAST") + fun T.unlockedBy(ingredient: ItemLike): T = + unlockedBy(getHasName(ingredient), has(ingredient)) as T + + /** Adds an unlock criterion requiring any item in [tag] to have been obtained, named `has_`. */ + @Suppress("UNCHECKED_CAST") + fun T.unlockedBy(tag: TagKey): T = + unlockedBy("has_${tag.location.path}", has(tag)) as T + +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieCookingRecipeBuilder.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieCookingRecipeBuilder.kt new file mode 100644 index 000000000..98bda5872 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieCookingRecipeBuilder.kt @@ -0,0 +1,109 @@ +package net.kernelpanicsoft.archie.data.common.crafting.recipies + +import net.minecraft.advancements.Criterion +import net.minecraft.data.recipes.RecipeCategory +import net.minecraft.data.recipes.RecipeOutput +import net.minecraft.data.recipes.SimpleCookingRecipeBuilder +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.Item +import net.minecraft.world.item.crafting.AbstractCookingRecipe +import net.minecraft.world.item.crafting.BlastingRecipe +import net.minecraft.world.item.crafting.CampfireCookingRecipe +import net.minecraft.world.item.crafting.Ingredient +import net.minecraft.world.item.crafting.RecipeSerializer +import net.minecraft.world.item.crafting.SmeltingRecipe +import net.minecraft.world.item.crafting.SmokingRecipe +import net.minecraft.world.level.ItemLike +import kotlin.properties.Delegates + +/** + * DSL builder for a [T] cooking recipe (smelting/blasting/smoking/campfire), wrapping vanilla's + * [SimpleCookingRecipeBuilder]. Set [category], [result], [ingredient], [experience], and + * [cookingTime], then save with [IARecipeBuilder.save]/[net.minecraft.data.recipes.RecipeBuilder.save]. + * Build via the type-specific [smelting]/[blasting]/[smoking]/[cooking] factories, or the + * corresponding methods on [net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider]. + */ +class ArchieCookingRecipeBuilder( + private val factory: AbstractCookingRecipe.Factory, + private val serializer: RecipeSerializer +) : IARecipeBuilder +{ + private val builder: SimpleCookingRecipeBuilder by lazy { + SimpleCookingRecipeBuilder.generic( + ingredient, + category, + result, + experience, + cookingTime, + serializer, + factory + ) + } + private val criteria: MutableMap> = mutableMapOf() + + lateinit var category: RecipeCategory + lateinit var result: ItemLike + lateinit var ingredient: Ingredient + var experience by Delegates.notNull() + var cookingTime by Delegates.notNull() + + var group: String? = null + + private fun checkVars() + { + check(::category.isInitialized) { "You must specify a recipe category dumbass!" } + check(::result.isInitialized) { "You must specify a recipe result dumbass!" } + check(::ingredient.isInitialized) { "You must specify a recipe ingredient dumbass!" } + check(runCatching { experience }.isSuccess) { "You must specify an experience amount dumbass!" } + check(runCatching { cookingTime }.isSuccess) { "You must specify a cooking time dumbass!" } + } + + override fun unlockedBy(name: String, criterion: Criterion<*>): ArchieCookingRecipeBuilder + { + criteria[name] = criterion + return this + } + + override fun group(groupName: String?): ArchieCookingRecipeBuilder + { + group = groupName + return this + } + + override fun getResult(): Item + { + checkVars() + return builder.result + } + + override fun save(recipeOutput: RecipeOutput, id: ResourceLocation) + { + checkVars() + criteria.forEach { (k, v) -> + builder.unlockedBy(k, v) + } + builder.group(group) + + builder.save(recipeOutput, id) + } + + companion object + { + /** Builds a furnace smelting recipe. */ + fun smelting(block: ArchieCookingRecipeBuilder.() -> Unit): ArchieCookingRecipeBuilder = + ArchieCookingRecipeBuilder(::SmeltingRecipe, RecipeSerializer.SMELTING_RECIPE).apply(block) + + /** Builds a blast furnace recipe. */ + fun blasting(block: ArchieCookingRecipeBuilder.() -> Unit): ArchieCookingRecipeBuilder = + ArchieCookingRecipeBuilder(::BlastingRecipe, RecipeSerializer.BLASTING_RECIPE).apply(block) + + /** Builds a smoker recipe. */ + fun smoking(block: ArchieCookingRecipeBuilder.() -> Unit): ArchieCookingRecipeBuilder = + ArchieCookingRecipeBuilder(::SmokingRecipe, RecipeSerializer.SMOKING_RECIPE).apply(block) + + /** Builds a campfire cooking recipe. */ + fun cooking(block: ArchieCookingRecipeBuilder.() -> Unit): ArchieCookingRecipeBuilder = + ArchieCookingRecipeBuilder(::CampfireCookingRecipe, RecipeSerializer.CAMPFIRE_COOKING_RECIPE).apply(block) + } + +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapedRecipeBuilder.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapedRecipeBuilder.kt new file mode 100644 index 000000000..f484ec6d3 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapedRecipeBuilder.kt @@ -0,0 +1,133 @@ +package net.kernelpanicsoft.archie.data.common.crafting.recipies + +import net.minecraft.advancements.Criterion +import net.minecraft.data.recipes.RecipeCategory +import net.minecraft.data.recipes.RecipeOutput +import net.minecraft.data.recipes.ShapedRecipeBuilder +import net.minecraft.resources.ResourceLocation +import net.minecraft.tags.TagKey +import net.minecraft.world.item.Item +import net.minecraft.world.item.crafting.Ingredient +import net.minecraft.world.level.ItemLike + +/** + * DSL builder for a shaped crafting recipe, wrapping vanilla's [ShapedRecipeBuilder]. Set + * [category], [result], and (optionally) [count], declare the grid with [pattern] and [key], and + * save with [IARecipeBuilder.save]/[net.minecraft.data.recipes.RecipeBuilder.save]. Build via + * [net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider.shaped]. + */ +class ArchieShapedRecipeBuilder : IARecipeBuilder +{ + + + private val builder: ShapedRecipeBuilder by lazy { ShapedRecipeBuilder(category, result, count) } + + private val rows: MutableList = mutableListOf() + private val key: MutableMap = mutableMapOf() + private val criteria: MutableMap> = mutableMapOf() + + lateinit var category: RecipeCategory + lateinit var result: ItemLike + var count = 1 + + var group: String? = null + var showNotification = true + + /** Declares the recipe's shape via [Pattern.unaryPlus] on each row string, e.g. `+"XXX"`. */ + fun pattern(block: Pattern.() -> Unit) + { + Pattern().apply(block) + } + + /** Declares the recipe's shape as a sequence of row strings, top to bottom. */ + fun pattern( + vararg lines: String, + ) + { + rows.addAll(lines) + } + + /** DSL scope for declaring pattern rows one at a time; see [pattern]. */ + inner class Pattern + { + /** Adds this string as the next pattern row. */ + operator fun String.unaryPlus() + { + rows.add(this) + } + } + + /** Declares which [Ingredient] each pattern symbol maps to via [Key.to]. */ + fun key(block: Key.() -> Unit) + { + Key().apply(block) + } + + /** DSL scope for mapping pattern symbols to ingredients; see [key]. */ + inner class Key + { + infix fun Char.to(tag: TagKey) + { + this to Ingredient.of(tag) + } + + infix fun Char.to(item: ItemLike) + { + this to Ingredient.of(item) + } + + infix fun Char.to(ingredient: Ingredient) + { + require(!key.containsKey(this)) { "Symbol '$this' is already defined!" } + require(this != ' ') { "Symbol ' ' (whitespace) is reserved and cannot be defined" } + key[this] = ingredient + } + } + + + override fun unlockedBy(name: String, criterion: Criterion<*>): ArchieShapedRecipeBuilder + { + criteria[name] = criterion + return this + } + + override fun group(groupName: String?): ArchieShapedRecipeBuilder + { + group = groupName + return this + } + + override fun getResult(): Item + { + check(::category.isInitialized) { "You must specify a recipe category dumbass!" } + check(::result.isInitialized) { "You must specify a recipe result dumbass!" } + return builder.result + } + + override fun save(recipeOutput: RecipeOutput, id: ResourceLocation) + { + check(::category.isInitialized) { "You must specify a recipe category dumbass!" } + check(::result.isInitialized) { "You must specify a recipe result dumbass!" } + rows.forEach { + builder.pattern(it) + } + key.forEach { (k, v) -> + builder.define(k, v) + } + criteria.forEach { (k, v) -> + builder.unlockedBy(k, v) + } + builder.group(group) + builder.showNotification(showNotification) + + builder.save(recipeOutput, id) + } + + companion object + { + fun shaped(block: ArchieShapedRecipeBuilder.() -> Unit): ArchieShapedRecipeBuilder + { + return ArchieShapedRecipeBuilder().apply(block) + } + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapelessRecipeBuilder.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapelessRecipeBuilder.kt new file mode 100644 index 000000000..99cad6deb --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapelessRecipeBuilder.kt @@ -0,0 +1,111 @@ +package net.kernelpanicsoft.archie.data.common.crafting.recipies + +import net.minecraft.advancements.Criterion +import net.minecraft.core.NonNullList +import net.minecraft.data.recipes.RecipeCategory +import net.minecraft.data.recipes.RecipeOutput +import net.minecraft.data.recipes.ShapelessRecipeBuilder +import net.minecraft.resources.ResourceLocation +import net.minecraft.tags.TagKey +import net.minecraft.world.item.Item +import net.minecraft.world.item.crafting.Ingredient +import net.minecraft.world.level.ItemLike + +/** + * DSL builder for a shapeless crafting recipe, wrapping vanilla's [ShapelessRecipeBuilder]. Set + * [category], [result], and (optionally) [count], declare inputs with [ingredients], and save + * with [IARecipeBuilder.save]/[net.minecraft.data.recipes.RecipeBuilder.save]. Build via + * [net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider.shapeless]. + */ +class ArchieShapelessRecipeBuilder : IARecipeBuilder +{ + private val builder: ShapelessRecipeBuilder by lazy { ShapelessRecipeBuilder(category, result, count) } + + private val ingredients: NonNullList = NonNullList.create() + private val criteria: MutableMap> = mutableMapOf() + + lateinit var category: RecipeCategory + lateinit var result: ItemLike + var count = 1 + + var group: String? = null + + /** Declares input ingredients via [Ingredients.of], e.g. `2 of Items.STICK`. */ + fun ingredients(block: Ingredients.() -> Unit) + { + Ingredients().apply(block) + } + + /** DSL scope for adding input ingredients by quantity; see [ingredients]. */ + inner class Ingredients + { + infix fun Int.of(tag: TagKey) + { + check(this >= 1) { "Cannot add less than 1 of ingredient" } + repeat(this) + { + ingredients.add(Ingredient.of(tag)) + } + } + + infix fun Int.of(item: ItemLike) + { + check(this >= 1) { "Cannot add less than 1 of ingredient" } + repeat(this) + { + ingredients.add(Ingredient.of(item)) + } + } + + infix fun Int.of(ingredient: Ingredient) + { + check(this >= 1) { "Cannot add less than 1 of ingredient" } + repeat(this) + { + ingredients.add(ingredient) + } + } + } + + override fun unlockedBy(name: String, criterion: Criterion<*>): ArchieShapelessRecipeBuilder + { + criteria[name] = criterion + return this + } + + override fun group(groupName: String?): ArchieShapelessRecipeBuilder + { + group = groupName + return this + } + + override fun getResult(): Item + { + check(::category.isInitialized) { "You must specify a recipe category dumbass!" } + check(::result.isInitialized) { "You must specify a recipe result dumbass!" } + return builder.result + } + + override fun save(recipeOutput: RecipeOutput, id: ResourceLocation) + { + check(::category.isInitialized) { "You must specify a recipe category dumbass!" } + check(::result.isInitialized) { "You must specify a recipe result dumbass!" } + ingredients.forEach { + builder.requires(it) + } + criteria.forEach { (k, v) -> + builder.unlockedBy(k, v) + } + builder.group(group) + + builder.save(recipeOutput, id) + } + + companion object + { + fun shapeless(block: ArchieShapelessRecipeBuilder.() -> Unit): ArchieShapelessRecipeBuilder + { + return ArchieShapelessRecipeBuilder().apply(block) + } + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt new file mode 100644 index 000000000..f9f4a4040 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt @@ -0,0 +1,21 @@ +package net.kernelpanicsoft.archie.data.common.crafting.recipies + +import net.kernelpanicsoft.archie.data.common.conditions.AConditionBuilder +import net.kernelpanicsoft.archie.data.common.conditions.IACondition +import net.kernelpanicsoft.archie.data.common.conditions.withCondition +import net.minecraft.data.recipes.RecipeBuilder +import net.minecraft.data.recipes.RecipeOutput +import net.minecraft.resources.ResourceLocation + +/** [RecipeBuilder] extension adding a condition-aware [save] overload. */ +interface IARecipeBuilder : RecipeBuilder +{ + /** Saves this recipe to [recipeOutput] under [id] (or the default id if `null`), gated by the [IACondition] built from [condition]. */ + fun save(recipeOutput: RecipeOutput, id: ResourceLocation? = null, condition: AConditionBuilder.() -> IACondition) + { + if (id != null) + save(recipeOutput.withCondition(condition), id) + else + save(recipeOutput.withCondition(condition)) + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilder.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilder.kt new file mode 100644 index 000000000..ba7e01c70 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilder.kt @@ -0,0 +1,169 @@ +package net.kernelpanicsoft.archie.data.common.tags + +import net.minecraft.data.tags.TagsProvider +import net.minecraft.resources.ResourceKey +import net.minecraft.resources.ResourceLocation +import net.minecraft.tags.TagEntry +import net.minecraft.tags.TagKey +import java.util.function.Consumer +import java.util.function.Predicate +import java.util.stream.Stream + +class ATagBuilder(private val parent: TagsProvider.TagAppender, private val provider: ATagsProvider) : + TagsProvider.TagAppender(parent.builder), IATagBuilder +{ + override fun setReplace(replace: Boolean): ATagBuilder + { + ATagBuilderPlatform.setTagReplace(builder, replace) + return this + } + + override fun replace(): ATagBuilder + { + return setReplace(true) + } + + override fun add(element: T): ATagBuilder + { + add(provider.reverseLookup(element)) + return this + } + + @SafeVarargs + override fun add(vararg elements: T): ATagBuilder + { + Stream.of(*elements).map { element: T -> + provider.reverseLookup( + element + ) + }.forEach { registryKey: ResourceKey -> + this.add( + registryKey + ) + } + return this + } + + override fun add(registryKey: ResourceKey): ATagBuilder + { + parent.add(registryKey) + return this + } + + override fun add(id: ResourceLocation): ATagBuilder + { + builder.addElement(id) + return this + } + + override fun addOptional(id: ResourceLocation): ATagBuilder + { + parent.addOptional(id) + return this + } + + override fun addOptional(registryKey: ResourceKey): ATagBuilder + { + return addOptional(registryKey.location()) + } + + override fun addOptionals(vararg ids: ResourceLocation): ATagBuilder + { + ids.forEach(this::addOptional) + return this + } + + override fun addOptionals(vararg keys: ResourceKey): ATagBuilder + { + keys.forEach(this::addOptional) + return this + } + + override fun addTag(tag: TagKey): ATagBuilder + { + builder.add(ForcedTagEntry(TagEntry.tag(tag.location()))) + return this + } + + override fun addOptionalTag(id: ResourceLocation): ATagBuilder + { + parent.addOptionalTag(id) + return this + } + + override fun addOptionalTag(tag: TagKey): ATagBuilder + { + return addOptionalTag(tag.location()) + } + + override fun addOptionalTags(vararg ids: ResourceLocation): ATagBuilder + { + ids.forEach(this::addOptionalTag) + return this + } + + override fun addOptionalTags(vararg tags: TagKey): ATagBuilder + { + tags.forEach(this::addOptionalTag) + return this + } + + override fun add(vararg ids: ResourceLocation): ATagBuilder + { + for (id in ids) + { + add(id) + } + + return this + } + + @SafeVarargs + override fun add(vararg registryKeys: ResourceKey): ATagBuilder + { + for (registryKey in registryKeys) + { + add(registryKey) + } + + return this + } + + override fun addTags(vararg ids: ResourceLocation): ATagBuilder + { + for (id in ids) + { + builder.addTag(id) + } + + return this + } + + @SafeVarargs + override fun addTags(vararg tagKeys: TagKey): ATagBuilder + { + for (tagKey in tagKeys) + { + addTag(tagKey) + } + + return this + } + + class ForcedTagEntry(private val delegate: TagEntry) : + TagEntry(delegate.id, true, delegate.required) + { + override fun build(arg: Lookup, consumer: Consumer): Boolean + { + return delegate.build(arg, consumer) + } + + override fun verifyIfPresent( + objectExistsTest: Predicate, + tagExistsTest: Predicate + ): Boolean + { + return true + } + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.common.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.common.kt new file mode 100644 index 000000000..6847e897b --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.common.kt @@ -0,0 +1,14 @@ +package net.kernelpanicsoft.archie.data.common.tags + +import net.minecraft.data.tags.TagsProvider.TagAppender +import net.minecraft.tags.TagBuilder + +/** Cross-loader hooks into vanilla/loader-specific tag builder internals not otherwise exposed uniformly. */ +expect object ATagBuilderPlatform +{ + /** Sets whether a tag file [replace]s (rather than merges with) tags from lower-priority datapacks. */ + fun setTagReplace(builder: TagBuilder, replace: Boolean) + + /** Creates an [IATagBuilder] wrapping [parent], the loader's native tag appender for [provider]. */ + fun createTagBuilder(parent: TagAppender, provider: ATagsProvider): IATagBuilder +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagsProvider.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagsProvider.kt new file mode 100644 index 000000000..649401016 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagsProvider.kt @@ -0,0 +1,423 @@ +package net.kernelpanicsoft.archie.data.common.tags + +import dev.architectury.extensions.injected.InjectedRegistryEntryExtension +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.IADataProvider +import net.kernelpanicsoft.archie.registries.holder +import dev.architectury.platform.Mod +import net.minecraft.core.Holder +import net.minecraft.core.HolderLookup +import net.minecraft.core.Registry +import net.minecraft.core.RegistryAccess +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.core.registries.Registries +import net.minecraft.data.PackOutput +import net.minecraft.data.tags.TagsProvider +import net.minecraft.resources.ResourceKey +import net.minecraft.resources.ResourceLocation +import net.minecraft.tags.* +import net.minecraft.world.entity.EntityType +import net.minecraft.world.item.Item +import net.minecraft.world.level.biome.Biome +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.material.Fluid +import java.util.* +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer +import java.util.function.Function +import kotlin.jvm.optionals.getOrNull +import kotlin.system.exitProcess + +abstract class ATagsProvider( + override val output: PackOutput, override val mod: Mod, registryKey: ResourceKey>, + registries: CompletableFuture, override val exitOnError: Boolean +) : TagsProvider(output, registryKey, registries), IADataProvider +{ + final override fun addTags(registries: HolderLookup.Provider) + { + runCatching { + generate(registries) + }.onFailure { + Archie.LOGGER.error( + "Data Provider $name failed with exception: ${it.message}\n" + + "Stacktrace: ${it.stackTraceToString()}" + ) + if (exitOnError) exitProcess(-1) + } + } + + /** + * Implement this method and then use [invoke] to get and register new tag builders. + */ + abstract fun generate(registries: HolderLookup.Provider) + + /** + * Looks up a registry entry for a specific [ResourceKey]. + * Only works if the resource key corresponds to the provider's [registryKey] + * @param registries The [HolderLookup.Provider] received from [addTags] + * @param key The [ResourceKey] to look up + * @return The looked up registry entry + * @throws IllegalStateException If either the registry or the entry cannot be looked up + */ + protected open fun lookup(registries: HolderLookup.Provider, key: ResourceKey): T + { + val registryLookup = registries.lookupOrThrow(registryKey) + return registryLookup.getOrThrow(key).value() + } + + /** + * Looks up a registry entry for a specific [ResourceLocation]. + * Uses the provider's [registryKey] to create a [ResourceKey] and delegates + * to the overload that takes a [ResourceKey] + * @param registries The [HolderLookup.Provider] received from [addTags] + * @param key The [ResourceKey] to look up + * @return The looked up registry entry + * @throws IllegalStateException If either the registry or the entry cannot be looked up + */ + protected open fun lookup(registries: HolderLookup.Provider, key: ResourceLocation): T + { + return lookup(registries, ResourceKey.create(registryKey, key)) + } + + /** + * Override to enable adding objects to the tag builder directly. + */ + open fun reverseLookup(element: T): ResourceKey + { + val registry = + RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY).registry(registryKey).getOrNull() + + if (registry != null) + { + val key: Optional> = registry.getResourceKey(element) + + if (key.isPresent) + { + return key.get() + } + } + + throw UnsupportedOperationException("Adding objects is not supported by $javaClass") + } + + @Suppress("UNCHECKED_CAST") + protected fun reverseLookupInjected(element: E): ResourceKey + { + return ((element as InjectedRegistryEntryExtension).holder as Holder.Reference).key() + } + + /** + * Creates a new instance of [IATagBuilder] for the given [TagKey]. + * + * @receiver The [TagKey] tag to create the builder for + * @return The [IATagBuilder] instance + */ + operator fun TagKey.invoke(): IATagBuilder + { + return ATagBuilderPlatform.createTagBuilder(super.tag(this), this@ATagsProvider) + } + + /** + * Creates a new instance of [IATagBuilder] for the given [TagKey] + * and applies a lambda over it. + * + * @receiver The [TagKey] tag to create the builder for + * @param block the lamda to apply over the [TagKey] + */ + operator fun TagKey.invoke(block: IATagBuilder.() -> Unit) + { + this().apply(block) + } + + /** + * Creates a new instance of [IATagBuilder] for the given [TagKey] + * and adds an element of type [T] to it. + * + * @receiver The [TagKey] tag to create the builder for + * @param tag the element to add to the [TagKey] + */ + operator fun TagKey.plusAssign(tag: T) + { + this().add(tag) + } + + /** + * Creates a new instance of [IATagBuilder] for the given [TagKey] + * and adds an element of type [ResourceKey] to it. + * + * @receiver The [TagKey] tag to create the builder for + * @param tag the element to add to the [TagKey] + */ + operator fun TagKey.plusAssign(tag: ResourceKey) + { + this().add(tag) + } + + /** + * Creates a new instance of [IATagBuilder] for the given [TagKey] + * and adds an element of type [ResourceLocation] to it. + * + * @receiver The [TagKey] tag to create the builder for + * @param tag the element to add to the [TagKey] + */ + operator fun TagKey.plusAssign(tag: ResourceLocation) + { + this().add(tag) + } + + /** + * Creates a new instance of [IATagBuilder] for the given [TagKey] + * and adds a tag of type [TagKey] to it. + * + * @receiver The [TagKey] tag to create the builder for + * @param tag the element to add to the [TagKey] + */ + operator fun TagKey.plusAssign(tag: TagKey) + { + this().addTag(tag) + } + + /** + * Creates a new instance of [IATagBuilder] for the given [TagKey] + * and adds a list of elements of type [T] to it. + * + * @receiver The [TagKey] tag to create the builder for + * @param tags the list of elements to add to the [TagKey] + */ + operator fun TagKey.plusAssign(tags: List) + { + this().apply { tags.forEach(this::add) } + } + + /** + * Creates a new instance of [IATagBuilder] for the given [TagKey] + * and adds a list of elements of type [ResourceKey] to it. + * + * @receiver The [TagKey] tag to create the builder for + * @param tags the list of elements to add to the [TagKey] + */ + @JvmName("plusAssignKeyList") + operator fun TagKey.plusAssign(tags: List>) + { + this().apply { tags.forEach(this::add) } + } + + /** + * Creates a new instance of [IATagBuilder] for the given [TagKey] + * and adds a list of elements of type [ResourceLocation] to it. + * + * @receiver The [TagKey] tag to create the builder for + * @param tags the list of elements to add to the [TagKey] + */ + @JvmName("plusAssignLocList") + operator fun TagKey.plusAssign(tags: List) + { + this().apply { tags.forEach(this::add) } + } + + /** + * Creates a new instance of [IATagBuilder] for the given [TagKey] + * and adds a list of tags of type [TagKey] to it. + * + * @receiver The [TagKey] tag to create the builder for + * @param tags the list of elements to add to the [TagKey] + */ + @JvmName("plusAssignTagList") + operator fun TagKey.plusAssign(tags: List>) + { + this().apply { tags.forEach(this::addTag) } + } + + operator fun TagKey.timesAssign(tag: ResourceKey) + { + this().addOptional(tag) + } + + operator fun TagKey.timesAssign(tag: ResourceLocation) + { + this().addOptional(tag) + } + + operator fun TagKey.timesAssign(tag: TagKey) + { + this().addOptionalTag(tag) + } + + @JvmName("timesAssignKeyList") + operator fun TagKey.timesAssign(tags: List>) + { + this().apply { tags.forEach(this::addOptional) } + } + + @JvmName("timesAssignLocList") + operator fun TagKey.timesAssign(tags: List) + { + this().apply { tags.forEach(this::addOptional) } + } + + @JvmName("timesAssignTagList") + operator fun TagKey.timesAssign(tags: List>) + { + this().apply { tags.forEach(this::addOptionalTag) } + + } + + @Deprecated("Don't use this, use the platform agnostic version", ReplaceWith("tag()")) + override fun tag(tag: TagKey): TagAppender + { + throw IllegalStateException("Usage of vanilla \"tag\" method in an ArchieTagsProvider is prohibited. use the platform agnostic \"invoke\" operator instead") + } + + override fun getName(): String = format("${ + registryKey.location().path.split("/").last().split("_") + .joinToString(" ") { it.replaceFirstChar(Char::uppercase) } + } Tags") + + /** + * Extend this class to create [Block] tags in the "/blocks" tag directory. + */ + abstract class BlockTagsProvider( + output: PackOutput, + mod: Mod, + registriesFuture: CompletableFuture, + exitOnError: Boolean + ) : + ATagsProvider(output, mod, Registries.BLOCK, registriesFuture, exitOnError) + { + override fun reverseLookup(element: Block): ResourceKey + { + return reverseLookupInjected(element) + } + } + + /** + * Extend this class to create [Item] tags in the "/items" tag directory. + */ + abstract class ItemTagsProvider : + ATagsProvider + { + /** + * Construct an [ItemTagsProvider] tag provider **with** an associated [BlockTagsProvider] tag provider. + * + * @param output The [PackOutput] instance + * @param mod The architectury [Mod] instance + * @param registriesFuture The [HolderLookup.Provider] future + * @param blockTagsProvider The parent [BlockTagsProvider] + */ + constructor( + output: PackOutput, + mod: Mod, + registriesFuture: CompletableFuture, + blockTagsProvider: BlockTagsProvider?, + exitOnError: Boolean + + ) : super(output, mod, Registries.ITEM, registriesFuture, exitOnError) + { + this.blockTagBuilderProvider = + if (blockTagsProvider == null) null else Function, TagBuilder> { tag: TagKey -> + blockTagsProvider.getOrCreateRawBuilder( + tag + ) + } + } + + /** + * Construct an [ItemTagsProvider] tag provider **without** an associated [BlockTagsProvider] tag provider. + * + * @param output The [PackOutput] instance + * @param mod The architectury [Mod] instance + * @param registriesFuture The [HolderLookup.Provider] future + */ + constructor( + output: PackOutput, + mod: Mod, + registriesFuture: CompletableFuture, + exitOnError: Boolean + + ) : this(output, mod, registriesFuture, null, exitOnError) + + + private val blockTagBuilderProvider: Function, TagBuilder>? + + /** + * Copy the entries from a tag with the [Block] type into this item tag. + * + * + * The [ItemTagsProvider] tag provider must be constructed with an associated [BlockTagsProvider] tag provider to use this method. + * + * @param blockTag The block tag to copy from. + * @param itemTag The item tag to copy to. + */ + fun copy(blockTag: TagKey, itemTag: TagKey) + { + val blockTagBuilder = Objects.requireNonNull( + this.blockTagBuilderProvider, + "Pass Block tag provider via constructor to use copy" + )!!.apply(blockTag) + val itemTagBuilder: TagBuilder = this.getOrCreateRawBuilder(itemTag) + blockTagBuilder.build().forEach(Consumer { entry: TagEntry -> + itemTagBuilder.add( + entry + ) + }) + } + + override fun reverseLookup(element: Item): ResourceKey + { + return reverseLookupInjected(element) + } + } + + /** + * Extend this class to create [Fluid] tags in the "/fluids" tag directory. + */ + abstract class FluidTagsProvider( + output: PackOutput, + mod: Mod, + registriesFuture: CompletableFuture, + exitOnError: Boolean + ) : + ATagsProvider(output, mod, Registries.FLUID, registriesFuture, exitOnError) + { + override fun reverseLookup(element: Fluid): ResourceKey + { + return reverseLookupInjected(element) + } + } + + /** + * Extend this class to create [EntityType] tags in the "/entity_types" tag directory. + */ + abstract class EntityTypeTagsProvider( + output: PackOutput, + mod: Mod, + registriesFuture: CompletableFuture, + exitOnError: Boolean + ) : + ATagsProvider>(output, mod, Registries.ENTITY_TYPE, registriesFuture, exitOnError) + { + override fun reverseLookup(element: EntityType<*>): ResourceKey> + { + return reverseLookupInjected(element) + } + } + + /** + * Extend this class to create [Biome] tags in the "/worldgen/biome" tag directory. + * + * **Note:** Minecraft does not have a biome registry, so only [ResourceKey] and [TagKey] are allowed as tag entries + */ + abstract class BiomeTagsProvider( + output: PackOutput, + mod: Mod, + registriesFuture: CompletableFuture, exitOnError: Boolean + ) : + ATagsProvider(output, mod, Registries.BIOME, registriesFuture, exitOnError) + { + override fun reverseLookup(element: Biome): ResourceKey + { + throw UnsupportedOperationException("You can't look up a biome in the registry since Minecraft doesn't have a biome registry") + } + } + +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/IATagBuilder.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/IATagBuilder.kt new file mode 100644 index 000000000..264b75798 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/IATagBuilder.kt @@ -0,0 +1,155 @@ +package net.kernelpanicsoft.archie.data.common.tags + +import net.minecraft.data.tags.TagsProvider +import net.minecraft.resources.ResourceKey +import net.minecraft.resources.ResourceLocation +import net.minecraft.tags.BiomeTags +import net.minecraft.tags.BlockTags +import net.minecraft.tags.EntityTypeTags +import net.minecraft.tags.FluidTags +import net.minecraft.tags.ItemTags +import net.minecraft.tags.TagKey + + + +/** + * An extension to [TagsProvider.TagAppender] that provides additional functionality. + */ +interface IATagBuilder +{ + /** + * Set the value of the `replace` flag in a Tag. + * + * + * When set to true the tag will replace any existing tag entries. + * + * @return the [IATagBuilder] instance + */ + fun setReplace(replace: Boolean): IATagBuilder + + /** + * Set the value of the `replace` flag to true in a Tag. + * + * + * The tag will replace any existing tag entries. + * + * @return the [IATagBuilder] instance + */ + fun replace(): IATagBuilder + + /** + * Add an element to the tag. + * + * @return the [IATagBuilder] instance + */ + fun add(element: T): IATagBuilder + + /** + * Add multiple elements to the tag. + * + * @return the [IATagBuilder] instance + */ + @SafeVarargs + fun add(vararg elements: T): IATagBuilder + + /** + * Add an element to the tag. + * + * @return the [IATagBuilder] instance + */ + fun add(registryKey: ResourceKey): IATagBuilder + + /** + * Add a single element to the tag. + * + * @return the [IATagBuilder] instance + */ + fun add(id: ResourceLocation): IATagBuilder + + /** + * Add an optional [ResourceLocation] to the tag. + * + * @return the [IATagBuilder] instance + */ + fun addOptional(id: ResourceLocation): IATagBuilder + + /** + * Add an optional [ResourceKey] to the tag. + * + * @return the [IATagBuilder] instance + */ + fun addOptional(registryKey: ResourceKey): IATagBuilder + + /** Add multiple optional [ResourceLocation]s to the tag. */ + fun addOptionals(vararg ids: ResourceLocation): IATagBuilder + + /** Add multiple optional [ResourceKey]s to the tag. */ + fun addOptionals(vararg keys: ResourceKey): IATagBuilder + + /** + * Add all elements of [tag] to this tag, unconditionally (unlike [addTags], this does not + * require [tag] to be defined by a known builder or vanilla tag). + * + * @return the [IATagBuilder] instance + * @see BlockTags + * + * @see EntityTypeTags + * + * @see FluidTags + * + * @see BiomeTags + * + * @see ItemTags + */ + fun addTag(tag: TagKey): IATagBuilder + + /** + * Add another optional tag to this tag. + * + * @return the [IATagBuilder] instance + */ + fun addOptionalTag(id: ResourceLocation): IATagBuilder + + /** + * Add another optional tag to this tag. + * + * @return the [IATagBuilder] instance + */ + fun addOptionalTag(tag: TagKey): IATagBuilder + + /** Add multiple optional tags, by id, to this tag. */ + fun addOptionalTags(vararg ids: ResourceLocation): IATagBuilder + + /** Add multiple optional tags to this tag. */ + fun addOptionalTags(vararg tags: TagKey): IATagBuilder + + /** + * Add multiple elements to this tag. + * + * @return the [IATagBuilder] instance + */ + fun add(vararg ids: ResourceLocation): IATagBuilder + + /** + * Add multiple elements to this tag. + * + * @return the [IATagBuilder] instance + */ + @SafeVarargs + fun add(vararg registryKeys: ResourceKey): IATagBuilder + + /** + * Add multiple tags to this tag. + * + * @return the [IATagBuilder] instance + */ + fun addTags(vararg ids: ResourceLocation): IATagBuilder + + /** + * Add multiple tags to this tag. + * + * @return the [IATagBuilder] instance + */ + @SafeVarargs + fun addTags(vararg tagKeys: TagKey): IATagBuilder +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt new file mode 100644 index 000000000..91a3573ce --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt @@ -0,0 +1,39 @@ +package net.kernelpanicsoft.archie.data.internal + +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.ADataGenerator +import net.kernelpanicsoft.archie.data.ADatagenEventObject +import net.kernelpanicsoft.archie.data.common.conditions.withCondition +import net.kernelpanicsoft.archie.data.common.crafting.ingredients.AComponentsIngredient +import net.kernelpanicsoft.archie.data.common.tags.ACommonTags +import net.kernelpanicsoft.archie.data.internal.common.tags.* +import net.minecraft.core.component.DataComponents +import net.minecraft.data.recipes.RecipeCategory +import net.minecraft.network.chat.Component +import net.minecraft.world.item.Items +import net.minecraft.world.item.crafting.Ingredient +import net.minecraft.world.level.block.Blocks + +/** + * Archie's own datagen registration, used both to populate the vanilla-derived common ("c") tags + * ([AInternalBlockTagsProvider] and friends) that ship with the library and as a smoke test for + * the datagen DSL itself (e.g. the emerald-from-diamond shapeless recipe below). + */ +internal object ArchieDatagen : ADatagenEventObject(Archie.MOD) +{ + override fun ADataGenerator.handler() + { + client { + languages { + add("archie.networking.config.no_permissions", "You do not have the required permissions to edit the server config") + } + } + common { + blockTags(::AInternalBlockTagsProvider) + itemTags(::AInternalItemTagsProvider) + biomeTags(::AInternalBiomeTagsProvider) + entityTags(::AInternalEntityTypeTagsProvider) + fluidTags(::AInternalFluidTagsProvider) + } + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/DatagenArchieExtension.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/DatagenArchieExtension.kt new file mode 100644 index 000000000..d36e7ca36 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/DatagenArchieExtension.kt @@ -0,0 +1,12 @@ +package net.kernelpanicsoft.archie.data.internal + +import net.kernelpanicsoft.archie.ArchieExtension + +/** [ArchieExtension] hook that activates [ArchieDatagen] when `archie-datagen` is on the classpath. */ +internal class DatagenArchieExtension : ArchieExtension +{ + override fun onDataGen() + { + ArchieDatagen.init() + } +} diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt new file mode 100644 index 000000000..52e0f7ee2 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt @@ -0,0 +1,349 @@ +package net.kernelpanicsoft.archie.data.internal.common.tags + +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider +import net.kernelpanicsoft.archie.data.common.tags.ACommonTags +import net.minecraft.core.HolderLookup +import net.minecraft.data.PackOutput +import net.minecraft.tags.BiomeTags +import net.minecraft.world.level.biome.Biomes +import java.util.concurrent.CompletableFuture + +/** + * Populates Archie's vanilla-derived common ("c") biome tags (see [ACommonTags.Biomes]) with + * their vanilla biome members, so downstream mods can depend on the `c` tag convention without + * every mod having to redeclare it. + */ +class AInternalBiomeTagsProvider( + output: PackOutput, registriesFuture: CompletableFuture +) : ATagsProvider.BiomeTagsProvider(output, Archie.MOD, registriesFuture, false) +{ + override fun generate(registries: HolderLookup.Provider) + { + ACommonTags.Biomes.NO_DEFAULT_MONSTERS += listOf( + Biomes.MUSHROOM_FIELDS, Biomes.DEEP_DARK + ) + ACommonTags.Biomes.HIDDEN_FROM_LOCATOR_SELECTION() // Create tag file for visibility + + ACommonTags.Biomes.IS_VOID += Biomes.THE_VOID + + ACommonTags.Biomes.IS_END += BiomeTags.IS_END + ACommonTags.Biomes.IS_NETHER += BiomeTags.IS_NETHER + ACommonTags.Biomes.IS_OVERWORLD += BiomeTags.IS_OVERWORLD + + ACommonTags.Biomes.IS_HOT_OVERWORLD += listOf( + Biomes.SWAMP, + Biomes.MANGROVE_SWAMP, + Biomes.JUNGLE, + Biomes.BAMBOO_JUNGLE, + Biomes.SPARSE_JUNGLE, + Biomes.DESERT, + Biomes.ERODED_BADLANDS, + Biomes.SAVANNA, + Biomes.SAVANNA_PLATEAU, + Biomes.WINDSWEPT_SAVANNA, + Biomes.STONY_PEAKS, + Biomes.WARM_OCEAN + ) + ACommonTags.Biomes.IS_HOT_NETHER += listOf( + Biomes.NETHER_WASTES, + Biomes.CRIMSON_FOREST, + Biomes.WARPED_FOREST, + Biomes.SOUL_SAND_VALLEY, + Biomes.BASALT_DELTAS + ) + ACommonTags.Biomes.IS_HOT_END() + ACommonTags.Biomes.IS_HOT { + addTags( + ACommonTags.Biomes.IS_HOT_OVERWORLD, + ACommonTags.Biomes.IS_HOT_NETHER + ) + addOptionalTag(ACommonTags.Biomes.IS_HOT_END) + } + + ACommonTags.Biomes.IS_COLD_OVERWORLD += listOf( + Biomes.TAIGA, + Biomes.OLD_GROWTH_PINE_TAIGA, + Biomes.SNOWY_PLAINS, + Biomes.ICE_SPIKES, + Biomes.GROVE, + Biomes.SNOWY_SLOPES, + Biomes.JAGGED_PEAKS, + Biomes.FROZEN_PEAKS, + Biomes.SNOWY_BEACH, + Biomes.SNOWY_TAIGA, + Biomes.FROZEN_RIVER, + Biomes.COLD_OCEAN, + Biomes.FROZEN_OCEAN, + Biomes.DEEP_COLD_OCEAN, + Biomes.DEEP_FROZEN_OCEAN + ) + ACommonTags.Biomes.IS_COLD_NETHER() + ACommonTags.Biomes.IS_COLD_END += listOf( + Biomes.THE_END, + Biomes.SMALL_END_ISLANDS, + Biomes.END_MIDLANDS, + Biomes.END_HIGHLANDS, + Biomes.END_BARRENS + ) + ACommonTags.Biomes.IS_COLD { + addTags( + ACommonTags.Biomes.IS_COLD_OVERWORLD, + ACommonTags.Biomes.IS_COLD_END + ) + addOptionalTag(ACommonTags.Biomes.IS_COLD_NETHER.location()) + } + + ACommonTags.Biomes.IS_SPARSE_VEGETATION_OVERWORLD += listOf( + Biomes.WOODED_BADLANDS, + Biomes.ERODED_BADLANDS, + Biomes.SAVANNA, + Biomes.SAVANNA_PLATEAU, + Biomes.WINDSWEPT_SAVANNA, + Biomes.WINDSWEPT_FOREST, + Biomes.WINDSWEPT_HILLS, + Biomes.WINDSWEPT_GRAVELLY_HILLS, + Biomes.SNOWY_SLOPES, + Biomes.JAGGED_PEAKS, + Biomes.FROZEN_PEAKS + ) + ACommonTags.Biomes.IS_SPARSE_VEGETATION_NETHER() + ACommonTags.Biomes.IS_SPARSE_VEGETATION_END() + ACommonTags.Biomes.IS_SPARSE_VEGETATION { + addTag(ACommonTags.Biomes.IS_SPARSE_VEGETATION_OVERWORLD) + addOptionalTags( + ACommonTags.Biomes.IS_SPARSE_VEGETATION_NETHER, + ACommonTags.Biomes.IS_SPARSE_VEGETATION_END + ) + } + + ACommonTags.Biomes.IS_DENSE_VEGETATION_OVERWORLD += listOf( + Biomes.DARK_FOREST, + Biomes.OLD_GROWTH_BIRCH_FOREST, + Biomes.OLD_GROWTH_SPRUCE_TAIGA, + Biomes.JUNGLE + ) + ACommonTags.Biomes.IS_DENSE_VEGETATION_NETHER() + ACommonTags.Biomes.IS_DENSE_VEGETATION_END() + ACommonTags.Biomes.IS_DENSE_VEGETATION { + addTag(ACommonTags.Biomes.IS_DENSE_VEGETATION_OVERWORLD) + addOptionalTags( + ACommonTags.Biomes.IS_DENSE_VEGETATION_NETHER, + ACommonTags.Biomes.IS_DENSE_VEGETATION_END + ) + } + + ACommonTags.Biomes.IS_WET_OVERWORLD += listOf( + Biomes.SWAMP, + Biomes.MANGROVE_SWAMP, + Biomes.JUNGLE, + Biomes.BAMBOO_JUNGLE, + Biomes.SPARSE_JUNGLE, + Biomes.BEACH, + Biomes.LUSH_CAVES, + Biomes.DRIPSTONE_CAVES + ) + ACommonTags.Biomes.IS_WET_NETHER() + ACommonTags.Biomes.IS_WET_END() + ACommonTags.Biomes.IS_WET { + addTag(ACommonTags.Biomes.IS_WET_OVERWORLD) + addOptionalTags( + ACommonTags.Biomes.IS_WET_NETHER, + ACommonTags.Biomes.IS_WET_END + ) + } + + ACommonTags.Biomes.IS_DRY_OVERWORLD += listOf( + Biomes.DESERT, + Biomes.BADLANDS, + Biomes.WOODED_BADLANDS, + Biomes.ERODED_BADLANDS, + Biomes.SAVANNA, + Biomes.SAVANNA_PLATEAU, + Biomes.WINDSWEPT_SAVANNA + ) + ACommonTags.Biomes.IS_DRY_NETHER += listOf( + Biomes.NETHER_WASTES, + Biomes.CRIMSON_FOREST, + Biomes.WARPED_FOREST, + Biomes.SOUL_SAND_VALLEY, + Biomes.BASALT_DELTAS + ) + ACommonTags.Biomes.IS_DRY_END += listOf( + Biomes.THE_END, + Biomes.SMALL_END_ISLANDS, + Biomes.END_MIDLANDS, + Biomes.END_HIGHLANDS, + Biomes.END_BARRENS + ) + ACommonTags.Biomes.IS_DRY += listOf( + ACommonTags.Biomes.IS_DRY_OVERWORLD, + ACommonTags.Biomes.IS_DRY_NETHER, + ACommonTags.Biomes.IS_DRY_END + ) + + ACommonTags.Biomes.IS_CONIFEROUS_TREE += ACommonTags.Biomes.IS_TAIGA + ACommonTags.Biomes.IS_CONIFEROUS_TREE += Biomes.GROVE + + ACommonTags.Biomes.IS_SAVANNA_TREE += ACommonTags.Biomes.IS_SAVANNA + ACommonTags.Biomes.IS_JUNGLE_TREE += ACommonTags.Biomes.IS_JUNGLE + ACommonTags.Biomes.IS_DECIDUOUS_TREE += listOf( + Biomes.FOREST, + Biomes.FLOWER_FOREST, + Biomes.BIRCH_FOREST, + Biomes.DARK_FOREST, + Biomes.OLD_GROWTH_BIRCH_FOREST, + Biomes.WINDSWEPT_FOREST + ) + + ACommonTags.Biomes.IS_MOUNTAIN_SLOPE += listOf( + Biomes.SNOWY_SLOPES, + Biomes.MEADOW, + Biomes.GROVE, + Biomes.CHERRY_GROVE + ) + ACommonTags.Biomes.IS_MOUNTAIN_PEAK += listOf( + Biomes.JAGGED_PEAKS, + Biomes.FROZEN_PEAKS, + Biomes.STONY_PEAKS + ) + ACommonTags.Biomes.IS_MOUNTAIN += listOf( + BiomeTags.IS_MOUNTAIN, + ACommonTags.Biomes.IS_MOUNTAIN_PEAK, + ACommonTags.Biomes.IS_MOUNTAIN_SLOPE + ) + + ACommonTags.Biomes.IS_FOREST += BiomeTags.IS_FOREST + ACommonTags.Biomes.IS_BIRCH_FOREST += listOf( + Biomes.BIRCH_FOREST, + Biomes.OLD_GROWTH_BIRCH_FOREST + ) + ACommonTags.Biomes.IS_FLOWER_FOREST += Biomes.FLOWER_FOREST + ACommonTags.Biomes.IS_FLORAL += ACommonTags.Biomes.IS_FLOWER_FOREST + ACommonTags.Biomes.IS_FLORAL += listOf( + Biomes.SUNFLOWER_PLAINS, + Biomes.CHERRY_GROVE, + Biomes.MEADOW + ) + ACommonTags.Biomes.IS_BEACH += BiomeTags.IS_BEACH + ACommonTags.Biomes.IS_STONY_SHORES += Biomes.STONY_SHORE + ACommonTags.Biomes.IS_DESERT += Biomes.DESERT + ACommonTags.Biomes.IS_BADLANDS += BiomeTags.IS_BADLANDS + ACommonTags.Biomes.IS_PLAINS += listOf( + Biomes.PLAINS, + Biomes.SUNFLOWER_PLAINS + ) + ACommonTags.Biomes.IS_SNOWY_PLAINS += Biomes.SNOWY_PLAINS + ACommonTags.Biomes.IS_TAIGA += BiomeTags.IS_TAIGA + ACommonTags.Biomes.IS_HILL += BiomeTags.IS_HILL + ACommonTags.Biomes.IS_WINDSWEPT += listOf( + Biomes.WINDSWEPT_HILLS, + Biomes.WINDSWEPT_GRAVELLY_HILLS, + Biomes.WINDSWEPT_FOREST, + Biomes.WINDSWEPT_SAVANNA + ) + ACommonTags.Biomes.IS_SAVANNA += BiomeTags.IS_SAVANNA + ACommonTags.Biomes.IS_JUNGLE += BiomeTags.IS_JUNGLE + ACommonTags.Biomes.IS_SNOWY += listOf( + Biomes.SNOWY_BEACH, + Biomes.SNOWY_PLAINS, + Biomes.ICE_SPIKES, + Biomes.SNOWY_TAIGA, + Biomes.GROVE, + Biomes.SNOWY_SLOPES, + Biomes.JAGGED_PEAKS, + Biomes.FROZEN_PEAKS + ) + ACommonTags.Biomes.IS_ICY += listOf( + Biomes.ICE_SPIKES, + Biomes.FROZEN_PEAKS + ) + ACommonTags.Biomes.IS_SWAMP += listOf( + Biomes.SWAMP, + Biomes.MANGROVE_SWAMP + ) + ACommonTags.Biomes.IS_OLD_GROWTH += listOf( + Biomes.OLD_GROWTH_BIRCH_FOREST, + Biomes.OLD_GROWTH_PINE_TAIGA, + Biomes.OLD_GROWTH_SPRUCE_TAIGA + ) + ACommonTags.Biomes.IS_LUSH += Biomes.LUSH_CAVES + ACommonTags.Biomes.IS_SANDY += listOf( + Biomes.DESERT, + Biomes.BADLANDS, + Biomes.WOODED_BADLANDS, + Biomes.ERODED_BADLANDS, + Biomes.BEACH + ) + ACommonTags.Biomes.IS_MUSHROOM += Biomes.MUSHROOM_FIELDS + ACommonTags.Biomes.IS_PLATEAU += listOf( + Biomes.WOODED_BADLANDS, + Biomes.SAVANNA_PLATEAU, + Biomes.CHERRY_GROVE, + Biomes.MEADOW + ) + ACommonTags.Biomes.IS_SPOOKY += listOf( + Biomes.DARK_FOREST, + Biomes.DEEP_DARK + ) + ACommonTags.Biomes.IS_WASTELAND() + ACommonTags.Biomes.IS_RARE += listOf( + Biomes.SUNFLOWER_PLAINS, + Biomes.FLOWER_FOREST, + Biomes.OLD_GROWTH_BIRCH_FOREST, + Biomes.OLD_GROWTH_SPRUCE_TAIGA, + Biomes.BAMBOO_JUNGLE, + Biomes.SPARSE_JUNGLE, + Biomes.ERODED_BADLANDS, + Biomes.SAVANNA_PLATEAU, + Biomes.WINDSWEPT_SAVANNA, + Biomes.ICE_SPIKES, + Biomes.WINDSWEPT_GRAVELLY_HILLS, + Biomes.MUSHROOM_FIELDS, + Biomes.DEEP_DARK + ) + + ACommonTags.Biomes.IS_RIVER += BiomeTags.IS_RIVER + ACommonTags.Biomes.IS_SHALLOW_OCEAN += listOf( + Biomes.OCEAN, + Biomes.LUKEWARM_OCEAN, + Biomes.WARM_OCEAN, + Biomes.COLD_OCEAN, + Biomes.FROZEN_OCEAN + ) + ACommonTags.Biomes.IS_DEEP_OCEAN += BiomeTags.IS_DEEP_OCEAN + ACommonTags.Biomes.IS_OCEAN += listOf( + BiomeTags.IS_OCEAN, + ACommonTags.Biomes.IS_SHALLOW_OCEAN, + ACommonTags.Biomes.IS_DEEP_OCEAN + ) + ACommonTags.Biomes.IS_AQUATIC_ICY += listOf( + Biomes.FROZEN_RIVER, + Biomes.DEEP_FROZEN_OCEAN, + Biomes.FROZEN_OCEAN + ) + ACommonTags.Biomes.IS_AQUATIC += listOf( + ACommonTags.Biomes.IS_OCEAN, + ACommonTags.Biomes.IS_RIVER + ) + + ACommonTags.Biomes.IS_CAVE += listOf( + Biomes.LUSH_CAVES, + Biomes.DRIPSTONE_CAVES, + Biomes.DEEP_DARK + ) + ACommonTags.Biomes.IS_UNDERGROUND += ACommonTags.Biomes.IS_CAVE + + ACommonTags.Biomes.IS_NETHER_FOREST += listOf( + Biomes.CRIMSON_FOREST, + Biomes.WARPED_FOREST + ) + ACommonTags.Biomes.IS_OUTER_END_ISLAND += listOf( + Biomes.END_HIGHLANDS, + Biomes.END_MIDLANDS, + Biomes.END_BARRENS + ) + + } + +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt new file mode 100644 index 000000000..53bf4cd82 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt @@ -0,0 +1,382 @@ +package net.kernelpanicsoft.archie.data.internal.common.tags + +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider +import net.kernelpanicsoft.archie.data.common.tags.ACommonTags +import dev.architectury.platform.Platform +import net.minecraft.core.HolderLookup +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.data.PackOutput +import net.minecraft.resources.ResourceLocation +import net.minecraft.tags.BlockTags +import net.minecraft.tags.TagKey +import net.minecraft.world.item.DyeColor +import net.minecraft.world.level.block.Block +import net.minecraft.world.level.block.Blocks +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer + +/** + * Populates Archie's vanilla-derived common ("c") block tags (see [ACommonTags.Blocks]) with + * their vanilla block members, so downstream mods can depend on the `c` tag convention without + * every mod having to redeclare it. + */ +class AInternalBlockTagsProvider( + output: PackOutput, + lookupProvider: CompletableFuture +) : + ATagsProvider.BlockTagsProvider( + output, Archie.MOD, + lookupProvider, + false + ) +{ + override fun generate(registries: HolderLookup.Provider) + { + if (Platform.isNeoForge()) + { + ACommonTags.Blocks.ENDERMAN_PLACE_ON_BLACKLIST() + } + ACommonTags.Blocks.BARRELS += ACommonTags.Blocks.BARRELS_WOODEN + ACommonTags.Blocks.BARRELS_WOODEN += Blocks.BARREL + ACommonTags.Blocks.BOOKSHELVES += Blocks.BOOKSHELF + ACommonTags.Blocks.BUDDING_BLOCKS += Blocks.BUDDING_AMETHYST + ACommonTags.Blocks.BUDS += listOf( + Blocks.SMALL_AMETHYST_BUD, + Blocks.MEDIUM_AMETHYST_BUD, + Blocks.LARGE_AMETHYST_BUD + ) + ACommonTags.Blocks.CHAINS += Blocks.CHAIN + ACommonTags.Blocks.CHESTS += listOf( + ACommonTags.Blocks.CHESTS_ENDER, + ACommonTags.Blocks.CHESTS_TRAPPED, + ACommonTags.Blocks.CHESTS_WOODEN + ) + ACommonTags.Blocks.CHESTS_ENDER += Blocks.ENDER_CHEST + ACommonTags.Blocks.CHESTS_TRAPPED += Blocks.TRAPPED_CHEST + ACommonTags.Blocks.CHESTS_WOODEN += listOf( + Blocks.CHEST, + Blocks.TRAPPED_CHEST + ) + ACommonTags.Blocks.CLUSTERS += Blocks.AMETHYST_CLUSTER + ACommonTags.Blocks.SHULKER_BOXES += listOf( + Blocks.SHULKER_BOX, + Blocks.BLUE_SHULKER_BOX, + Blocks.BROWN_SHULKER_BOX, + Blocks.CYAN_SHULKER_BOX, + Blocks.GRAY_SHULKER_BOX, + Blocks.GREEN_SHULKER_BOX, + Blocks.LIGHT_BLUE_SHULKER_BOX, + Blocks.LIGHT_GRAY_SHULKER_BOX, + Blocks.LIME_SHULKER_BOX, + Blocks.MAGENTA_SHULKER_BOX, + Blocks.ORANGE_SHULKER_BOX, + Blocks.PINK_SHULKER_BOX, + Blocks.PURPLE_SHULKER_BOX, + Blocks.RED_SHULKER_BOX, + Blocks.WHITE_SHULKER_BOX, + Blocks.YELLOW_SHULKER_BOX, + Blocks.BLACK_SHULKER_BOX + ) + ACommonTags.Blocks.COBBLESTONES += listOf( + ACommonTags.Blocks.COBBLESTONES_NORMAL, + ACommonTags.Blocks.COBBLESTONES_INFESTED, + ACommonTags.Blocks.COBBLESTONES_MOSSY, + ACommonTags.Blocks.COBBLESTONES_DEEPSLATE + ) + ACommonTags.Blocks.COBBLESTONES_NORMAL += Blocks.COBBLESTONE + ACommonTags.Blocks.COBBLESTONES_INFESTED += Blocks.INFESTED_COBBLESTONE + ACommonTags.Blocks.COBBLESTONES_MOSSY += Blocks.MOSSY_COBBLESTONE + ACommonTags.Blocks.COBBLESTONES_DEEPSLATE += Blocks.COBBLED_DEEPSLATE + ACommonTags.Blocks.END_STONES += Blocks.END_STONE + ACommonTags.Blocks.FENCE_GATES += ACommonTags.Blocks.FENCE_GATES_WOODEN + ACommonTags.Blocks.FENCE_GATES_WOODEN += listOf( + Blocks.OAK_FENCE_GATE, + Blocks.SPRUCE_FENCE_GATE, + Blocks.BIRCH_FENCE_GATE, + Blocks.JUNGLE_FENCE_GATE, + Blocks.ACACIA_FENCE_GATE, + Blocks.DARK_OAK_FENCE_GATE, + Blocks.CRIMSON_FENCE_GATE, + Blocks.WARPED_FENCE_GATE, + Blocks.MANGROVE_FENCE_GATE, + Blocks.BAMBOO_FENCE_GATE, + Blocks.CHERRY_FENCE_GATE + ) + ACommonTags.Blocks.FENCES += listOf( + ACommonTags.Blocks.FENCES_NETHER_BRICK, + ACommonTags.Blocks.FENCES_WOODEN + ) + ACommonTags.Blocks.FENCES_NETHER_BRICK += Blocks.NETHER_BRICK_FENCE + ACommonTags.Blocks.FENCES_WOODEN += BlockTags.WOODEN_FENCES + ACommonTags.Blocks.GLASS_BLOCKS += listOf( + ACommonTags.Blocks.GLASS_BLOCKS_COLORLESS, + ACommonTags.Blocks.GLASS_BLOCKS_STAINED, + ACommonTags.Blocks.GLASS_BLOCKS_TINTED + ) + ACommonTags.Blocks.GLASS_BLOCKS_COLORLESS += Blocks.GLASS + ACommonTags.Blocks.GLASS_BLOCKS_CHEAP += listOf( + ACommonTags.Blocks.GLASS_BLOCKS_COLORLESS, + ACommonTags.Blocks.GLASS_BLOCKS_STAINED + ) + ACommonTags.Blocks.GLASS_BLOCKS_TINTED += Blocks.TINTED_GLASS + ACommonTags.Blocks.GLASS_PANES += listOf( + ACommonTags.Blocks.GLASS_PANES_COLORLESS, + ACommonTags.Blocks.GLASS_PANES_STAINED + ) + ACommonTags.Blocks.GLASS_PANES_COLORLESS += Blocks.GLASS_PANE + addColoredFlat(ACommonTags.Blocks.GLASS_BLOCKS_STAINED, "{color}_stained_glass") + addColoredFlat(ACommonTags.Blocks.GLASS_PANES_STAINED, "{color}_stained_glass_pane") + addColored(ACommonTags.Blocks.DYED, "{color}_banner") + addColored(ACommonTags.Blocks.DYED, "{color}_bed") + addColored(ACommonTags.Blocks.DYED, "{color}_candle") + addColored(ACommonTags.Blocks.DYED, "{color}_carpet") + addColored(ACommonTags.Blocks.DYED, "{color}_concrete") + addColored(ACommonTags.Blocks.DYED, "{color}_concrete_powder") + addColored(ACommonTags.Blocks.DYED, "{color}_glazed_terracotta") + addColored(ACommonTags.Blocks.DYED, "{color}_shulker_box") + addColored(ACommonTags.Blocks.DYED, "{color}_stained_glass") + addColored(ACommonTags.Blocks.DYED, "{color}_stained_glass_pane") + addColored(ACommonTags.Blocks.DYED, "{color}_terracotta") + addColored(ACommonTags.Blocks.DYED, "{color}_wall_banner") + addColored(ACommonTags.Blocks.DYED, "{color}_wool") + ACommonTags.Blocks.HIDDEN_FROM_RECIPE_VIEWERS() + ACommonTags.Blocks.GRAVELS += Blocks.GRAVEL + ACommonTags.Blocks.SKULLS += listOf( + Blocks.SKELETON_SKULL, + Blocks.SKELETON_WALL_SKULL, + Blocks.WITHER_SKELETON_SKULL, + Blocks.WITHER_SKELETON_WALL_SKULL, + Blocks.PLAYER_HEAD, + Blocks.PLAYER_WALL_HEAD, + Blocks.ZOMBIE_HEAD, + Blocks.ZOMBIE_WALL_HEAD, + Blocks.CREEPER_HEAD, + Blocks.CREEPER_WALL_HEAD, + Blocks.PIGLIN_HEAD, + Blocks.PIGLIN_WALL_HEAD, + Blocks.DRAGON_HEAD, + Blocks.DRAGON_WALL_HEAD + ) + + ACommonTags.Blocks.NETHERRACKS += Blocks.NETHERRACK + ACommonTags.Blocks.OBSIDIANS += Blocks.OBSIDIAN + ACommonTags.Blocks.ORE_BEARING_GROUND_DEEPSLATE += Blocks.DEEPSLATE + ACommonTags.Blocks.ORE_BEARING_GROUND_NETHERRACK += Blocks.NETHERRACK + ACommonTags.Blocks.ORE_BEARING_GROUND_STONE += Blocks.STONE + ACommonTags.Blocks.ORE_RATES_DENSE += listOf( + Blocks.COPPER_ORE, + Blocks.DEEPSLATE_COPPER_ORE, + Blocks.DEEPSLATE_LAPIS_ORE, + Blocks.DEEPSLATE_REDSTONE_ORE, + Blocks.LAPIS_ORE, + Blocks.REDSTONE_ORE + ) + ACommonTags.Blocks.ORE_RATES_SINGULAR += listOf( + Blocks.ANCIENT_DEBRIS, + Blocks.COAL_ORE, + Blocks.DEEPSLATE_COAL_ORE, + Blocks.DEEPSLATE_DIAMOND_ORE, + Blocks.DEEPSLATE_EMERALD_ORE, + Blocks.DEEPSLATE_GOLD_ORE, + Blocks.DEEPSLATE_IRON_ORE, + Blocks.DIAMOND_ORE, + Blocks.EMERALD_ORE, + Blocks.GOLD_ORE, + Blocks.IRON_ORE, + Blocks.NETHER_QUARTZ_ORE + ) + ACommonTags.Blocks.ORE_RATES_SPARSE += Blocks.NETHER_GOLD_ORE + ACommonTags.Blocks.ORES += listOf( + ACommonTags.Blocks.ORES_COAL, + ACommonTags.Blocks.ORES_COPPER, + ACommonTags.Blocks.ORES_DIAMOND, + ACommonTags.Blocks.ORES_EMERALD, + ACommonTags.Blocks.ORES_GOLD, + ACommonTags.Blocks.ORES_IRON, + ACommonTags.Blocks.ORES_LAPIS, + ACommonTags.Blocks.ORES_REDSTONE, + ACommonTags.Blocks.ORES_QUARTZ, + ACommonTags.Blocks.ORES_NETHERITE_SCRAP + ) + ACommonTags.Blocks.ORES_COAL += BlockTags.COAL_ORES + ACommonTags.Blocks.ORES_COPPER += BlockTags.COPPER_ORES + ACommonTags.Blocks.ORES_DIAMOND += BlockTags.DIAMOND_ORES + ACommonTags.Blocks.ORES_EMERALD += BlockTags.EMERALD_ORES + ACommonTags.Blocks.ORES_GOLD += BlockTags.GOLD_ORES + ACommonTags.Blocks.ORES_IRON += BlockTags.IRON_ORES + ACommonTags.Blocks.ORES_LAPIS += BlockTags.LAPIS_ORES + ACommonTags.Blocks.ORES_QUARTZ += Blocks.NETHER_QUARTZ_ORE + ACommonTags.Blocks.ORES_REDSTONE += BlockTags.REDSTONE_ORES + ACommonTags.Blocks.ORES_NETHERITE_SCRAP += Blocks.ANCIENT_DEBRIS + ACommonTags.Blocks.ORES_IN_GROUND_DEEPSLATE += listOf( + Blocks.DEEPSLATE_COAL_ORE, + Blocks.DEEPSLATE_COPPER_ORE, + Blocks.DEEPSLATE_DIAMOND_ORE, + Blocks.DEEPSLATE_EMERALD_ORE, + Blocks.DEEPSLATE_GOLD_ORE, + Blocks.DEEPSLATE_IRON_ORE, + Blocks.DEEPSLATE_LAPIS_ORE, + Blocks.DEEPSLATE_REDSTONE_ORE + ) + ACommonTags.Blocks.ORES_IN_GROUND_NETHERRACK += listOf( + Blocks.NETHER_GOLD_ORE, + Blocks.NETHER_QUARTZ_ORE + ) + ACommonTags.Blocks.ORES_IN_GROUND_STONE += listOf( + Blocks.COAL_ORE, + Blocks.COPPER_ORE, + Blocks.DIAMOND_ORE, + Blocks.EMERALD_ORE, + Blocks.GOLD_ORE, + Blocks.IRON_ORE, + Blocks.LAPIS_ORE, + Blocks.REDSTONE_ORE + ) + ACommonTags.Blocks.PLAYER_WORKSTATIONS_CRAFTING_TABLES += Blocks.CRAFTING_TABLE + ACommonTags.Blocks.PLAYER_WORKSTATIONS_FURNACES += Blocks.FURNACE + ACommonTags.Blocks.RELOCATION_NOT_SUPPORTED() + ACommonTags.Blocks.ROPES() + ACommonTags.Blocks.SANDS += listOf( + ACommonTags.Blocks.SANDS_COLORLESS, + ACommonTags.Blocks.SANDS_RED + ) + ACommonTags.Blocks.SANDS_COLORLESS += Blocks.SAND + ACommonTags.Blocks.SANDS_RED += Blocks.RED_SAND + ACommonTags.Blocks.SANDSTONE_RED_BLOCKS += listOf( + Blocks.RED_SANDSTONE, + Blocks.CUT_RED_SANDSTONE, + Blocks.CHISELED_RED_SANDSTONE, + Blocks.SMOOTH_RED_SANDSTONE + ) + ACommonTags.Blocks.SANDSTONE_UNCOLORED_BLOCKS += listOf( + Blocks.SANDSTONE, + Blocks.CUT_SANDSTONE, + Blocks.CHISELED_SANDSTONE, + Blocks.SMOOTH_SANDSTONE + ) + ACommonTags.Blocks.SANDSTONE_BLOCKS += listOf( + ACommonTags.Blocks.SANDSTONE_RED_BLOCKS, + ACommonTags.Blocks.SANDSTONE_UNCOLORED_BLOCKS + ) + ACommonTags.Blocks.SANDSTONE_RED_SLABS += listOf( + Blocks.RED_SANDSTONE_SLAB, + Blocks.CUT_RED_SANDSTONE_SLAB, + Blocks.SMOOTH_RED_SANDSTONE_SLAB + ) + ACommonTags.Blocks.SANDSTONE_UNCOLORED_SLABS += listOf( + Blocks.SANDSTONE_SLAB, + Blocks.CUT_SANDSTONE_SLAB, + Blocks.SMOOTH_SANDSTONE_SLAB + ) + ACommonTags.Blocks.SANDSTONE_SLABS += listOf( + ACommonTags.Blocks.SANDSTONE_RED_SLABS, + ACommonTags.Blocks.SANDSTONE_UNCOLORED_SLABS + ) + ACommonTags.Blocks.SANDSTONE_RED_STAIRS += listOf( + Blocks.RED_SANDSTONE_STAIRS, + Blocks.SMOOTH_RED_SANDSTONE_STAIRS + ) + ACommonTags.Blocks.SANDSTONE_UNCOLORED_STAIRS += listOf( + Blocks.SANDSTONE_STAIRS, + Blocks.SMOOTH_SANDSTONE_STAIRS + ) + ACommonTags.Blocks.SANDSTONE_STAIRS += listOf( + ACommonTags.Blocks.SANDSTONE_RED_STAIRS, + ACommonTags.Blocks.SANDSTONE_UNCOLORED_STAIRS + ) + ACommonTags.Blocks.STONES += listOf( + Blocks.ANDESITE, + Blocks.DIORITE, + Blocks.GRANITE, + Blocks.STONE, + Blocks.DEEPSLATE, + Blocks.TUFF + ) + ACommonTags.Blocks.STORAGE_BLOCKS += listOf( + ACommonTags.Blocks.STORAGE_BLOCKS_BONE_MEAL, + ACommonTags.Blocks.STORAGE_BLOCKS_AMETHYST, + ACommonTags.Blocks.STORAGE_BLOCKS_COAL, + ACommonTags.Blocks.STORAGE_BLOCKS_COPPER, + ACommonTags.Blocks.STORAGE_BLOCKS_DIAMOND, + ACommonTags.Blocks.STORAGE_BLOCKS_DRIED_KELP, + ACommonTags.Blocks.STORAGE_BLOCKS_EMERALD, + ACommonTags.Blocks.STORAGE_BLOCKS_GOLD, + ACommonTags.Blocks.STORAGE_BLOCKS_IRON, + ACommonTags.Blocks.STORAGE_BLOCKS_LAPIS, + ACommonTags.Blocks.STORAGE_BLOCKS_QUARTZ, + ACommonTags.Blocks.STORAGE_BLOCKS_RAW_COPPER, + ACommonTags.Blocks.STORAGE_BLOCKS_RAW_GOLD, + ACommonTags.Blocks.STORAGE_BLOCKS_RAW_IRON, + ACommonTags.Blocks.STORAGE_BLOCKS_REDSTONE, + ACommonTags.Blocks.STORAGE_BLOCKS_NETHERITE, + ACommonTags.Blocks.STORAGE_BLOCKS_SLIME, + ACommonTags.Blocks.STORAGE_BLOCKS_WHEAT + ) + ACommonTags.Blocks.STORAGE_BLOCKS_BONE_MEAL += Blocks.BONE_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_AMETHYST += Blocks.AMETHYST_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_COAL += Blocks.COAL_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_COPPER += Blocks.COPPER_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_DIAMOND += Blocks.DIAMOND_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_DRIED_KELP += Blocks.DRIED_KELP_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_EMERALD += Blocks.EMERALD_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_GOLD += Blocks.GOLD_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_IRON += Blocks.IRON_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_LAPIS += Blocks.LAPIS_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_QUARTZ += Blocks.QUARTZ_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_RAW_COPPER += Blocks.RAW_COPPER_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_RAW_GOLD += Blocks.RAW_GOLD_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_RAW_IRON += Blocks.RAW_IRON_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_REDSTONE += Blocks.REDSTONE_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_NETHERITE += Blocks.NETHERITE_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_SLIME += Blocks.SLIME_BLOCK + ACommonTags.Blocks.STORAGE_BLOCKS_WHEAT += Blocks.HAY_BLOCK + ACommonTags.Blocks.VILLAGER_JOB_SITES += listOf( + Blocks.BARREL, Blocks.BLAST_FURNACE, Blocks.BREWING_STAND, Blocks.CARTOGRAPHY_TABLE, + Blocks.CAULDRON, Blocks.WATER_CAULDRON, Blocks.LAVA_CAULDRON, Blocks.POWDER_SNOW_CAULDRON, + Blocks.COMPOSTER, Blocks.FLETCHING_TABLE, Blocks.GRINDSTONE, Blocks.LECTERN, + Blocks.LOOM, Blocks.SMITHING_TABLE, Blocks.SMOKER, Blocks.STONECUTTER + ) + } + + /** + * For each [DyeColor], resolves the vanilla block named by substituting `{color}` into + * `pattern` and adds it to the per-color common tag `c:{group path}/{color}` (via [getCommonTag]). + */ + private fun addColored(group: TagKey, pattern: String, consumer: Consumer = Consumer {}) + { + val prefix = group.location().path.lowercase() + '/' + for (color in DyeColor.entries) + { + val key = ResourceLocation.fromNamespaceAndPath("minecraft", pattern.replace("{color}", color.getName().lowercase())) + val tag = getCommonTag(prefix + color.getName().lowercase()) + val block = BuiltInRegistries.BLOCK[key] + check(block !== Blocks.AIR) { "Unknown vanilla block: $key" } + tag += block + consumer.accept(block) + } + } + + /** + * For each [DyeColor], resolves the vanilla block named by substituting `{color}` into + * `pattern` and adds it directly to [tag] (unlike [addColored], all colors share one tag). + */ + private fun addColoredFlat(tag: TagKey, pattern: String, consumer: Consumer = Consumer {}) + { + for (color in DyeColor.entries) + { + val key = ResourceLocation.fromNamespaceAndPath("minecraft", pattern.replace("{color}", color.getName().lowercase())) + val block = BuiltInRegistries.BLOCK[key] + check(block !== Blocks.AIR) { "Unknown vanilla block: $key" } + tag += block + consumer.accept(block) + } + } + + /** Looks up a common ("c") block tag by [name], throwing if it isn't declared in [ACommonTags.Blocks]. */ + private fun getCommonTag(name: String): TagKey + { + return ACommonTags.Blocks[ResourceLocation.fromNamespaceAndPath("c", name)] + ?: throw IllegalStateException(ACommonTags.Blocks::class.java.name + " is missing tag name: " + name) + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt new file mode 100644 index 000000000..8b492d8bd --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt @@ -0,0 +1,43 @@ +package net.kernelpanicsoft.archie.data.internal.common.tags + +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider +import net.kernelpanicsoft.archie.data.common.tags.ACommonTags +import net.minecraft.core.HolderLookup +import net.minecraft.data.PackOutput +import net.minecraft.world.entity.EntityType +import java.util.concurrent.CompletableFuture + +/** + * Populates Archie's vanilla-derived common ("c") entity type tags (see [ACommonTags.EntityTypes]) + * with their vanilla entity type members, so downstream mods can depend on the `c` tag convention + * without every mod having to redeclare it. + */ +class AInternalEntityTypeTagsProvider( + output: PackOutput, + registriesFuture: CompletableFuture +) : ATagsProvider.EntityTypeTagsProvider(output, Archie.MOD, registriesFuture, false) +{ + override fun generate(registries: HolderLookup.Provider) + { + ACommonTags.EntityTypes.BOSSES += listOf( + EntityType.ENDER_DRAGON, + EntityType.WITHER + ) + ACommonTags.EntityTypes.MINECARTS += listOf( + EntityType.MINECART, + EntityType.CHEST_MINECART, + EntityType.FURNACE_MINECART, + EntityType.HOPPER_MINECART, + EntityType.SPAWNER_MINECART, + EntityType.TNT_MINECART, + EntityType.COMMAND_BLOCK_MINECART + ) + ACommonTags.EntityTypes.BOATS += listOf( + EntityType.BOAT, + EntityType.CHEST_BOAT + ) + ACommonTags.EntityTypes.CAPTURING_NOT_SUPPORTED() + ACommonTags.EntityTypes.TELEPORTING_NOT_SUPPORTED() + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt new file mode 100644 index 000000000..7dd19cfef --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt @@ -0,0 +1,46 @@ +package net.kernelpanicsoft.archie.data.internal.common.tags + +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider +import net.kernelpanicsoft.archie.data.common.tags.ACommonTags +import net.minecraft.core.HolderLookup +import net.minecraft.data.PackOutput +import net.minecraft.world.level.material.Fluids +import java.util.concurrent.CompletableFuture + +/** + * Populates Archie's vanilla-derived common ("c") fluid tags (see [ACommonTags.Fluids]) with + * their vanilla fluid members, so downstream mods can depend on the `c` tag convention without + * every mod having to redeclare it. + */ +class AInternalFluidTagsProvider(output: PackOutput, registriesFuture: CompletableFuture) : + ATagsProvider.FluidTagsProvider( + output, Archie.MOD, + registriesFuture, + false + ) +{ + override fun generate(registries: HolderLookup.Provider) + { + ACommonTags.Fluids.WATER += listOf( + Fluids.WATER, + Fluids.FLOWING_WATER + ) + ACommonTags.Fluids.LAVA += listOf( + Fluids.LAVA, + Fluids.FLOWING_LAVA + ) + ACommonTags.Fluids.MILK *= listOf( + mcLoc("milk"), + mcLoc("flowing_milk") + ) + ACommonTags.Fluids.GASEOUS() + ACommonTags.Fluids.HONEY() + ACommonTags.Fluids.POTION() + ACommonTags.Fluids.SUSPICIOUS_STEW() + ACommonTags.Fluids.MUSHROOM_STEW() + ACommonTags.Fluids.RABBIT_STEW() + ACommonTags.Fluids.BEETROOT_SOUP() + ACommonTags.Fluids.HIDDEN_FROM_RECIPE_VIEWERS() + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt new file mode 100644 index 000000000..00ed7dfb7 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt @@ -0,0 +1,654 @@ +package net.kernelpanicsoft.archie.data.internal.common.tags + +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider +import net.kernelpanicsoft.archie.data.common.tags.ACommonTags +import dev.architectury.platform.Platform +import net.minecraft.core.HolderLookup +import net.minecraft.core.registries.BuiltInRegistries +import net.minecraft.data.PackOutput +import net.minecraft.resources.ResourceLocation +import net.minecraft.tags.ItemTags +import net.minecraft.tags.TagKey +import net.minecraft.world.item.DyeColor +import net.minecraft.world.item.Item +import net.minecraft.world.item.Items +import java.util.concurrent.CompletableFuture +import java.util.function.Consumer + +/** + * Populates Archie's vanilla-derived common ("c") item tags (see [ACommonTags.Items]) with + * their vanilla item members, so downstream mods can depend on the `c` tag convention without + * every mod having to redeclare it. + */ +class AInternalItemTagsProvider( + output: PackOutput, + lookupProvider: CompletableFuture, + blockTagsProvider: BlockTagsProvider +) : ATagsProvider.ItemTagsProvider(output, Archie.MOD, lookupProvider, blockTagsProvider, false) +{ + override fun generate(registries: HolderLookup.Provider) + { + if (Platform.isNeoForge()) + { + ACommonTags.Items.ENCHANTING_FUELS += ACommonTags.Items.GEMS_LAPIS + } + copy(ACommonTags.Blocks.BARRELS, ACommonTags.Items.BARRELS) + copy( + ACommonTags.Blocks.BARRELS_WOODEN, + ACommonTags.Items.BARRELS_WOODEN + ) + ACommonTags.Items.BONES += Items.BONE + copy( + ACommonTags.Blocks.BOOKSHELVES, + ACommonTags.Items.BOOKSHELVES + ) + ACommonTags.Items.BRICKS += listOf( + ACommonTags.Items.BRICKS_NORMAL, + ACommonTags.Items.BRICKS_NETHER + ) + ACommonTags.Items.BRICKS_NORMAL += Items.BRICK + ACommonTags.Items.BRICKS_NETHER += Items.NETHER_BRICK + ACommonTags.Items.BUCKETS_EMPTY += Items.BUCKET + ACommonTags.Items.BUCKETS_WATER += Items.WATER_BUCKET + ACommonTags.Items.BUCKETS_LAVA += Items.LAVA_BUCKET + ACommonTags.Items.BUCKETS_MILK += Items.MILK_BUCKET + ACommonTags.Items.BUCKETS_POWDER_SNOW += Items.POWDER_SNOW_BUCKET + ACommonTags.Items.BUCKETS_ENTITY_WATER += listOf( + Items.AXOLOTL_BUCKET, + Items.COD_BUCKET, + Items.PUFFERFISH_BUCKET, + Items.TADPOLE_BUCKET, + Items.TROPICAL_FISH_BUCKET, + Items.SALMON_BUCKET + ) + ACommonTags.Items.BUCKETS += listOf( + ACommonTags.Items.BUCKETS_EMPTY, + ACommonTags.Items.BUCKETS_WATER, + ACommonTags.Items.BUCKETS_LAVA, + ACommonTags.Items.BUCKETS_MILK, + ACommonTags.Items.BUCKETS_POWDER_SNOW, + ACommonTags.Items.BUCKETS_ENTITY_WATER + ) + copy( + ACommonTags.Blocks.BUDDING_BLOCKS, + ACommonTags.Items.BUDDING_BLOCKS + ) + copy(ACommonTags.Blocks.BUDS, ACommonTags.Items.BUDS) + copy(ACommonTags.Blocks.CHAINS, ACommonTags.Items.CHAINS) + copy(ACommonTags.Blocks.CHESTS, ACommonTags.Items.CHESTS) + copy( + ACommonTags.Blocks.CHESTS_ENDER, + ACommonTags.Items.CHESTS_ENDER + ) + copy( + ACommonTags.Blocks.CHESTS_TRAPPED, + ACommonTags.Items.CHESTS_TRAPPED + ) + copy( + ACommonTags.Blocks.CHESTS_WOODEN, + ACommonTags.Items.CHESTS_WOODEN + ) + copy(ACommonTags.Blocks.CLUSTERS, ACommonTags.Items.CLUSTERS) + copy( + ACommonTags.Blocks.COBBLESTONES, + ACommonTags.Items.COBBLESTONES + ) + copy( + ACommonTags.Blocks.COBBLESTONES_NORMAL, + ACommonTags.Items.COBBLESTONES_NORMAL + ) + copy( + ACommonTags.Blocks.COBBLESTONES_INFESTED, + ACommonTags.Items.COBBLESTONES_INFESTED + ) + copy( + ACommonTags.Blocks.COBBLESTONES_MOSSY, + ACommonTags.Items.COBBLESTONES_MOSSY + ) + copy( + ACommonTags.Blocks.COBBLESTONES_DEEPSLATE, + ACommonTags.Items.COBBLESTONES_DEEPSLATE + ) + ACommonTags.Items.CROPS += listOf( + ACommonTags.Items.CROPS_BEETROOT, + ACommonTags.Items.CROPS_CARROT, + ACommonTags.Items.CROPS_NETHER_WART, + ACommonTags.Items.CROPS_POTATO, + ACommonTags.Items.CROPS_WHEAT + ) + ACommonTags.Items.CROPS_BEETROOT += Items.BEETROOT + ACommonTags.Items.CROPS_CARROT += Items.CARROT + ACommonTags.Items.CROPS_NETHER_WART += Items.NETHER_WART + ACommonTags.Items.CROPS_POTATO += Items.POTATO + ACommonTags.Items.CROPS_WHEAT += Items.WHEAT + addColored(ACommonTags.Items.DYED, "{color}_banner") + addColored(ACommonTags.Items.DYED, "{color}_bed") + addColored(ACommonTags.Items.DYED, "{color}_candle") + addColored(ACommonTags.Items.DYED, "{color}_carpet") + addColored(ACommonTags.Items.DYED, "{color}_concrete") + addColored(ACommonTags.Items.DYED, "{color}_concrete_powder") + addColored(ACommonTags.Items.DYED, "{color}_glazed_terracotta") + addColored(ACommonTags.Items.DYED, "{color}_shulker_box") + addColored(ACommonTags.Items.DYED, "{color}_stained_glass") + addColored(ACommonTags.Items.DYED, "{color}_stained_glass_pane") + addColored(ACommonTags.Items.DYED, "{color}_terracotta") + addColored(ACommonTags.Items.DYED, "{color}_wool") + addColoredTags(ACommonTags.Items.DYED) { values: TagKey -> + ACommonTags.Items.DYED += values + } + ACommonTags.Items.DUSTS += listOf( + ACommonTags.Items.DUSTS_GLOWSTONE, + ACommonTags.Items.DUSTS_REDSTONE, + ACommonTags.Items.DUSTS_PRISMARINE + ) + ACommonTags.Items.DUSTS_GLOWSTONE += Items.GLOWSTONE_DUST + ACommonTags.Items.DUSTS_REDSTONE += Items.REDSTONE + ACommonTags.Items.DUSTS_PRISMARINE += Items.PRISMARINE_SHARD + addColored(ACommonTags.Items.DYES, "{color}_dye") + addColoredTags(ACommonTags.Items.DYES) { values: TagKey -> + ACommonTags.Items.DYES += listOf(values) + } + ACommonTags.Items.EGGS += Items.EGG + copy(ACommonTags.Blocks.END_STONES, ACommonTags.Items.END_STONES) + ACommonTags.Items.ENDER_PEARLS += Items.ENDER_PEARL + ACommonTags.Items.FEATHERS += Items.FEATHER + copy( + ACommonTags.Blocks.FENCE_GATES, + ACommonTags.Items.FENCE_GATES + ) + copy( + ACommonTags.Blocks.FENCE_GATES_WOODEN, + ACommonTags.Items.FENCE_GATES_WOODEN + ) + copy(ACommonTags.Blocks.FENCES, ACommonTags.Items.FENCES) + copy( + ACommonTags.Blocks.FENCES_NETHER_BRICK, + ACommonTags.Items.FENCES_NETHER_BRICK + ) + copy( + ACommonTags.Blocks.FENCES_WOODEN, + ACommonTags.Items.FENCES_WOODEN + ) + ACommonTags.Items.FOODS_FRUITS += listOf( + Items.APPLE, + Items.GOLDEN_APPLE, + Items.ENCHANTED_GOLDEN_APPLE + ) + ACommonTags.Items.FOODS_VEGETABLES += listOf( + Items.CARROT, + Items.GOLDEN_CARROT, + Items.POTATO, + Items.MELON_SLICE, + Items.BEETROOT + ) + ACommonTags.Items.FOODS_BERRIES += listOf( + Items.SWEET_BERRIES, + Items.GLOW_BERRIES + ) + ACommonTags.Items.FOODS_BREADS += Items.BREAD + ACommonTags.Items.FOODS_COOKIES += Items.COOKIE + ACommonTags.Items.FOODS_RAW_MEATS += listOf( + Items.BEEF, + Items.PORKCHOP, + Items.CHICKEN, + Items.RABBIT, + Items.MUTTON + ) + ACommonTags.Items.FOODS_RAW_FISHES += listOf( + Items.COD, + Items.SALMON, + Items.TROPICAL_FISH, + Items.PUFFERFISH + ) + ACommonTags.Items.FOODS_COOKED_MEATS += listOf( + Items.COOKED_BEEF, + Items.COOKED_PORKCHOP, + Items.COOKED_CHICKEN, + Items.COOKED_RABBIT, + Items.COOKED_MUTTON + ) + ACommonTags.Items.FOODS_COOKED_FISHES += listOf( + Items.COOKED_COD, + Items.COOKED_SALMON + ) + ACommonTags.Items.FOODS_SOUPS += listOf( + Items.BEETROOT_SOUP, + Items.MUSHROOM_STEW, + Items.RABBIT_STEW, + Items.SUSPICIOUS_STEW + ) + ACommonTags.Items.FOODS_CANDIES() + ACommonTags.Items.FOODS_EDIBLE_WHEN_PLACED += Items.CAKE + ACommonTags.Items.FOODS_FOOD_POISONING += listOf( + Items.POISONOUS_POTATO, + Items.PUFFERFISH, + Items.SPIDER_EYE, + Items.CHICKEN, + Items.ROTTEN_FLESH + ) + ACommonTags.Items.FOODS { + add( + Items.BAKED_POTATO, + Items.PUMPKIN_PIE, + Items.HONEY_BOTTLE, + Items.OMINOUS_BOTTLE, + Items.DRIED_KELP + ) + addTags( + ACommonTags.Items.FOODS_FRUITS, + ACommonTags.Items.FOODS_VEGETABLES, + ACommonTags.Items.FOODS_BERRIES, + ACommonTags.Items.FOODS_BREADS, + ACommonTags.Items.FOODS_COOKIES, + ACommonTags.Items.FOODS_RAW_MEATS, + ACommonTags.Items.FOODS_RAW_FISHES, + ACommonTags.Items.FOODS_COOKED_MEATS, + ACommonTags.Items.FOODS_COOKED_FISHES, + ACommonTags.Items.FOODS_SOUPS, + ACommonTags.Items.FOODS_CANDIES, + ACommonTags.Items.FOODS_EDIBLE_WHEN_PLACED, + ACommonTags.Items.FOODS_FOOD_POISONING + ) + } + ACommonTags.Items.GEMS += listOf( + ACommonTags.Items.GEMS_AMETHYST, + ACommonTags.Items.GEMS_DIAMOND, + ACommonTags.Items.GEMS_EMERALD, + ACommonTags.Items.GEMS_LAPIS, + ACommonTags.Items.GEMS_PRISMARINE, + ACommonTags.Items.GEMS_QUARTZ + ) + ACommonTags.Items.GEMS_AMETHYST += Items.AMETHYST_SHARD + ACommonTags.Items.GEMS_DIAMOND += Items.DIAMOND + ACommonTags.Items.GEMS_EMERALD += Items.EMERALD + ACommonTags.Items.GEMS_LAPIS += Items.LAPIS_LAZULI + ACommonTags.Items.GEMS_PRISMARINE += Items.PRISMARINE_CRYSTALS + ACommonTags.Items.GEMS_QUARTZ += Items.QUARTZ + copy( + ACommonTags.Blocks.GLASS_BLOCKS, + ACommonTags.Items.GLASS_BLOCKS + ) + copy( + ACommonTags.Blocks.GLASS_BLOCKS_COLORLESS, + ACommonTags.Items.GLASS_BLOCKS_COLORLESS + ) + copy( + ACommonTags.Blocks.GLASS_BLOCKS_TINTED, + ACommonTags.Items.GLASS_BLOCKS_TINTED + ) + copy( + ACommonTags.Blocks.GLASS_BLOCKS_CHEAP, + ACommonTags.Items.GLASS_BLOCKS_CHEAP + ) + copy( + ACommonTags.Blocks.GLASS_BLOCKS_STAINED, + ACommonTags.Items.GLASS_BLOCKS_STAINED + ) + copy( + ACommonTags.Blocks.GLASS_PANES, + ACommonTags.Items.GLASS_PANES + ) + copy( + ACommonTags.Blocks.GLASS_PANES_COLORLESS, + ACommonTags.Items.GLASS_PANES_COLORLESS + ) + copy( + ACommonTags.Blocks.GLASS_PANES_STAINED, + ACommonTags.Items.GLASS_PANES_STAINED + ) + copy(ACommonTags.Blocks.GRAVELS, ACommonTags.Items.GRAVELS) + ACommonTags.Items.GUNPOWDERS += Items.GUNPOWDER + ACommonTags.Items.HIDDEN_FROM_RECIPE_VIEWERS() + ACommonTags.Items.INGOTS += listOf( + ACommonTags.Items.INGOTS_COPPER, + ACommonTags.Items.INGOTS_GOLD, + ACommonTags.Items.INGOTS_IRON, + ACommonTags.Items.INGOTS_NETHERITE + ) + ACommonTags.Items.INGOTS_COPPER += Items.COPPER_INGOT + ACommonTags.Items.INGOTS_GOLD += Items.GOLD_INGOT + ACommonTags.Items.INGOTS_IRON += Items.IRON_INGOT + ACommonTags.Items.INGOTS_NETHERITE += Items.NETHERITE_INGOT + ACommonTags.Items.LEATHERS += Items.LEATHER + ACommonTags.Items.MUSHROOMS += listOf( + Items.BROWN_MUSHROOM, + Items.RED_MUSHROOM + ) + ACommonTags.Items.NETHER_STARS += Items.NETHER_STAR + copy( + ACommonTags.Blocks.NETHERRACKS, + ACommonTags.Items.NETHERRACKS + ) + ACommonTags.Items.NUGGETS += listOf( + ACommonTags.Items.NUGGETS_GOLD, + ACommonTags.Items.NUGGETS_IRON + ) + ACommonTags.Items.NUGGETS_IRON += Items.IRON_NUGGET + ACommonTags.Items.NUGGETS_GOLD += Items.GOLD_NUGGET + copy(ACommonTags.Blocks.OBSIDIANS, ACommonTags.Items.OBSIDIANS) + copy( + ACommonTags.Blocks.ORE_BEARING_GROUND_DEEPSLATE, + ACommonTags.Items.ORE_BEARING_GROUND_DEEPSLATE + ) + copy( + ACommonTags.Blocks.ORE_BEARING_GROUND_NETHERRACK, + ACommonTags.Items.ORE_BEARING_GROUND_NETHERRACK + ) + copy( + ACommonTags.Blocks.ORE_BEARING_GROUND_STONE, + ACommonTags.Items.ORE_BEARING_GROUND_STONE + ) + copy( + ACommonTags.Blocks.ORE_RATES_DENSE, + ACommonTags.Items.ORE_RATES_DENSE + ) + copy( + ACommonTags.Blocks.ORE_RATES_SINGULAR, + ACommonTags.Items.ORE_RATES_SINGULAR + ) + copy( + ACommonTags.Blocks.ORE_RATES_SPARSE, + ACommonTags.Items.ORE_RATES_SPARSE + ) + copy(ACommonTags.Blocks.ORES, ACommonTags.Items.ORES) + copy(ACommonTags.Blocks.ORES_COAL, ACommonTags.Items.ORES_COAL) + copy( + ACommonTags.Blocks.ORES_COPPER, + ACommonTags.Items.ORES_COPPER + ) + copy( + ACommonTags.Blocks.ORES_DIAMOND, + ACommonTags.Items.ORES_DIAMOND + ) + copy( + ACommonTags.Blocks.ORES_EMERALD, + ACommonTags.Items.ORES_EMERALD + ) + copy(ACommonTags.Blocks.ORES_GOLD, ACommonTags.Items.ORES_GOLD) + copy(ACommonTags.Blocks.ORES_IRON, ACommonTags.Items.ORES_IRON) + copy(ACommonTags.Blocks.ORES_LAPIS, ACommonTags.Items.ORES_LAPIS) + copy( + ACommonTags.Blocks.ORES_QUARTZ, + ACommonTags.Items.ORES_QUARTZ + ) + copy( + ACommonTags.Blocks.ORES_REDSTONE, + ACommonTags.Items.ORES_REDSTONE + ) + copy( + ACommonTags.Blocks.ORES_NETHERITE_SCRAP, + ACommonTags.Items.ORES_NETHERITE_SCRAP + ) + copy( + ACommonTags.Blocks.ORES_IN_GROUND_DEEPSLATE, + ACommonTags.Items.ORES_IN_GROUND_DEEPSLATE + ) + copy( + ACommonTags.Blocks.ORES_IN_GROUND_NETHERRACK, + ACommonTags.Items.ORES_IN_GROUND_NETHERRACK + ) + copy( + ACommonTags.Blocks.ORES_IN_GROUND_STONE, + ACommonTags.Items.ORES_IN_GROUND_STONE + ) + copy( + ACommonTags.Blocks.PLAYER_WORKSTATIONS_CRAFTING_TABLES, + ACommonTags.Items.PLAYER_WORKSTATIONS_CRAFTING_TABLES + ) + copy( + ACommonTags.Blocks.PLAYER_WORKSTATIONS_FURNACES, + ACommonTags.Items.PLAYER_WORKSTATIONS_FURNACES + ) + ACommonTags.Items.RAW_BLOCKS += listOf( + ACommonTags.Items.RAW_BLOCKS_COPPER, + ACommonTags.Items.RAW_BLOCKS_GOLD, + ACommonTags.Items.RAW_BLOCKS_IRON + ) + ACommonTags.Items.RAW_BLOCKS_COPPER += Items.RAW_COPPER_BLOCK + ACommonTags.Items.RAW_BLOCKS_GOLD += Items.RAW_GOLD_BLOCK + ACommonTags.Items.RAW_BLOCKS_IRON += Items.RAW_IRON_BLOCK + ACommonTags.Items.RAW_MATERIALS += listOf( + ACommonTags.Items.RAW_MATERIALS_COPPER, + ACommonTags.Items.RAW_MATERIALS_GOLD, + ACommonTags.Items.RAW_MATERIALS_IRON + ) + ACommonTags.Items.RAW_MATERIALS_COPPER += Items.RAW_COPPER + ACommonTags.Items.RAW_MATERIALS_GOLD += Items.RAW_GOLD + ACommonTags.Items.RAW_MATERIALS_IRON += Items.RAW_IRON + ACommonTags.Items.RODS += listOf( + ACommonTags.Items.RODS_WOODEN, + ACommonTags.Items.RODS_BLAZE, + ACommonTags.Items.RODS_BREEZE + ) + ACommonTags.Items.RODS_BLAZE += Items.BLAZE_ROD + ACommonTags.Items.RODS_BREEZE += Items.BREEZE_ROD + ACommonTags.Items.RODS_WOODEN += Items.STICK + copy(ACommonTags.Blocks.ROPES, ACommonTags.Items.ROPES) + copy(ACommonTags.Blocks.SANDS, ACommonTags.Items.SANDS) + copy( + ACommonTags.Blocks.SANDS_COLORLESS, + ACommonTags.Items.SANDS_COLORLESS + ) + copy(ACommonTags.Blocks.SANDS_RED, ACommonTags.Items.SANDS_RED) + copy( + ACommonTags.Blocks.SANDSTONE_BLOCKS, + ACommonTags.Items.SANDSTONE_BLOCKS + ) + copy( + ACommonTags.Blocks.SANDSTONE_SLABS, + ACommonTags.Items.SANDSTONE_SLABS + ) + copy( + ACommonTags.Blocks.SANDSTONE_STAIRS, + ACommonTags.Items.SANDSTONE_STAIRS + ) + copy( + ACommonTags.Blocks.SANDSTONE_RED_BLOCKS, + ACommonTags.Items.SANDSTONE_RED_BLOCKS + ) + copy( + ACommonTags.Blocks.SANDSTONE_RED_SLABS, + ACommonTags.Items.SANDSTONE_RED_SLABS + ) + copy( + ACommonTags.Blocks.SANDSTONE_RED_STAIRS, + ACommonTags.Items.SANDSTONE_RED_STAIRS + ) + copy( + ACommonTags.Blocks.SANDSTONE_UNCOLORED_BLOCKS, + ACommonTags.Items.SANDSTONE_UNCOLORED_BLOCKS + ) + copy( + ACommonTags.Blocks.SANDSTONE_UNCOLORED_SLABS, + ACommonTags.Items.SANDSTONE_UNCOLORED_SLABS + ) + copy( + ACommonTags.Blocks.SANDSTONE_UNCOLORED_STAIRS, + ACommonTags.Items.SANDSTONE_UNCOLORED_STAIRS + ) + ACommonTags.Items.SEEDS += listOf( + ACommonTags.Items.SEEDS_BEETROOT, + ACommonTags.Items.SEEDS_MELON, + ACommonTags.Items.SEEDS_PUMPKIN, + ACommonTags.Items.SEEDS_WHEAT + ) + ACommonTags.Items.SEEDS_BEETROOT += Items.BEETROOT_SEEDS + ACommonTags.Items.SEEDS_MELON += Items.MELON_SEEDS + ACommonTags.Items.SEEDS_PUMPKIN += Items.PUMPKIN_SEEDS + ACommonTags.Items.SEEDS_WHEAT += Items.WHEAT_SEEDS + copy( + ACommonTags.Blocks.SHULKER_BOXES, + ACommonTags.Items.SHULKER_BOXES + ) + ACommonTags.Items.SLIMEBALLS += Items.SLIME_BALL + copy(ACommonTags.Blocks.STONES, ACommonTags.Items.STONES) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS, + ACommonTags.Items.STORAGE_BLOCKS + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_AMETHYST, + ACommonTags.Items.STORAGE_BLOCKS_AMETHYST + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_BONE_MEAL, + ACommonTags.Items.STORAGE_BLOCKS_BONE_MEAL + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_COAL, + ACommonTags.Items.STORAGE_BLOCKS_COAL + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_COPPER, + ACommonTags.Items.STORAGE_BLOCKS_COPPER + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_DIAMOND, + ACommonTags.Items.STORAGE_BLOCKS_DIAMOND + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_DRIED_KELP, + ACommonTags.Items.STORAGE_BLOCKS_DRIED_KELP + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_EMERALD, + ACommonTags.Items.STORAGE_BLOCKS_EMERALD + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_GOLD, + ACommonTags.Items.STORAGE_BLOCKS_GOLD + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_IRON, + ACommonTags.Items.STORAGE_BLOCKS_IRON + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_LAPIS, + ACommonTags.Items.STORAGE_BLOCKS_LAPIS + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_NETHERITE, + ACommonTags.Items.STORAGE_BLOCKS_NETHERITE + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_QUARTZ, + ACommonTags.Items.STORAGE_BLOCKS_QUARTZ + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_RAW_COPPER, + ACommonTags.Items.STORAGE_BLOCKS_RAW_COPPER + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_RAW_GOLD, + ACommonTags.Items.STORAGE_BLOCKS_RAW_GOLD + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_RAW_IRON, + ACommonTags.Items.STORAGE_BLOCKS_RAW_IRON + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_REDSTONE, + ACommonTags.Items.STORAGE_BLOCKS_REDSTONE + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_SLIME, + ACommonTags.Items.STORAGE_BLOCKS_SLIME + ) + copy( + ACommonTags.Blocks.STORAGE_BLOCKS_WHEAT, + ACommonTags.Items.STORAGE_BLOCKS_WHEAT + ) + ACommonTags.Items.STRINGS += Items.STRING + ACommonTags.Items.VILLAGER_JOB_SITES += listOf( + Items.BARREL, Items.BLAST_FURNACE, Items.BREWING_STAND, Items.CARTOGRAPHY_TABLE, + Items.CAULDRON, Items.COMPOSTER, Items.FLETCHING_TABLE, Items.GRINDSTONE, + Items.LECTERN, Items.LOOM, Items.SMITHING_TABLE, Items.SMOKER, Items.STONECUTTER + ) + + + // Tools and Armors + ACommonTags.Items.TOOLS_SHIELDS += Items.SHIELD + ACommonTags.Items.TOOLS_BOWS += Items.BOW + ACommonTags.Items.TOOLS_BRUSHES += Items.BRUSH + ACommonTags.Items.TOOLS_CROSSBOWS += Items.CROSSBOW + ACommonTags.Items.TOOLS_FISHING_RODS += Items.FISHING_ROD + ACommonTags.Items.TOOLS_SHEARS += Items.SHEARS + ACommonTags.Items.TOOLS_SPEARS += Items.TRIDENT + ACommonTags.Items.TOOLS += listOf( + ACommonTags.Items.TOOLS_AXES, + ACommonTags.Items.TOOLS_HOES, + ACommonTags.Items.TOOLS_PICKAXES, + ACommonTags.Items.TOOLS_SHOVELS, + ACommonTags.Items.TOOLS_SWORDS, + + ACommonTags.Items.TOOLS_BOWS, + ACommonTags.Items.TOOLS_BRUSHES, + ACommonTags.Items.TOOLS_CROSSBOWS, + ACommonTags.Items.TOOLS_FISHING_RODS, + ACommonTags.Items.TOOLS_SHEARS, + ACommonTags.Items.TOOLS_SHIELDS, + ACommonTags.Items.TOOLS_SPEARS + ) + ACommonTags.Items.ARMORS += listOf( + ACommonTags.Items.ARMORS_HELMETS, + ACommonTags.Items.ARMORS_CHESTPLATES, + ACommonTags.Items.ARMORS_LEGGINGS, + ACommonTags.Items.ARMORS_BOOTS + ) + ACommonTags.Items.ENCHANTABLES += listOf( + ItemTags.ARMOR_ENCHANTABLE, + ItemTags.EQUIPPABLE_ENCHANTABLE, + ItemTags.WEAPON_ENCHANTABLE, + ItemTags.SWORD_ENCHANTABLE, + ItemTags.MINING_ENCHANTABLE, + ItemTags.MINING_LOOT_ENCHANTABLE, + ItemTags.FISHING_ENCHANTABLE, + ItemTags.TRIDENT_ENCHANTABLE, + ItemTags.BOW_ENCHANTABLE, + ItemTags.CROSSBOW_ENCHANTABLE, + ItemTags.FIRE_ASPECT_ENCHANTABLE, + ItemTags.DURABILITY_ENCHANTABLE + ) + + ACommonTags.Items.ENCHANTABLES *= ItemTags.MACE_ENCHANTABLE + + + } + + /** + * For each [DyeColor], resolves the vanilla item named by substituting `{color}` into + * `pattern` and adds it to the per-color common tag `c:{group path}/{color}` (via [getCommonItemTag]). + */ + private fun addColored(group: TagKey, pattern: String) + { + val prefix = group.location().path.lowercase() + '/' + for (color in DyeColor.entries) + { + val key = ResourceLocation.fromNamespaceAndPath("minecraft", pattern.replace("{color}", color.getName())) + val tag = getCommonItemTag(prefix + color.getName()) + val item = BuiltInRegistries.ITEM[key] + check(item !== Items.AIR) { "Unknown vanilla item: $key" } + tag += item + } + } + + /** Passes each of [group]'s per-color common tags (`c:{group path}/{color}`) to [consumer]. */ + private fun addColoredTags(group: TagKey, consumer: Consumer>) + { + val prefix = group.location().path.lowercase() + '/' + for (color in DyeColor.entries) + { + val tag = getCommonItemTag(prefix + color.getName()) + consumer.accept(tag) + } + } + + /** Looks up a common ("c") item tag by [name], throwing if it isn't declared in [ACommonTags.Items]. */ + private fun getCommonItemTag(name: String): TagKey + { + return ACommonTags.Items[ResourceLocation.fromNamespaceAndPath("c", name)] + ?: throw IllegalStateException(ACommonTags.Items::class.java.name + " is missing tag name: " + name) + } + +} \ No newline at end of file diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt new file mode 100644 index 000000000..7bc877bc8 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt @@ -0,0 +1,396 @@ +package net.kernelpanicsoft.archie.data.util + +import com.google.gson.* +import com.mojang.math.Axis +import com.mojang.math.Transformation +import net.minecraft.util.Mth +import net.minecraft.util.StringRepresentable +import org.joml.* +import java.lang.Math +import java.lang.reflect.Type +import kotlin.math.acos + +/** + * Math and JSON-parsing helpers for [Transformation] (translate/rotate/scale/rotate model + * transforms), including [Deserializer] for the "TRSR" JSON format used by custom item/block + * model transforms. + */ +object TransformationHelper +{ + /** Builds a quaternion from Euler [xyz] angles (in [degrees] if `true`, else radians). */ + fun quatFromXYZ(xyz: Vector3f, degrees: Boolean): Quaternionf + { + return quatFromXYZ(xyz.x, xyz.y, xyz.z, degrees) + } + + fun quatFromXYZ(xyz: FloatArray, degrees: Boolean): Quaternionf + { + return quatFromXYZ(xyz[0], xyz[1], xyz[2], degrees) + } + + fun quatFromXYZ(x: Float, y: Float, z: Float, degrees: Boolean): Quaternionf + { + val conversionFactor = if (degrees) Math.PI.toFloat() / 180 else 1f + return Quaternionf().rotationXYZ(x * conversionFactor, y * conversionFactor, z * conversionFactor) + } + + /** Builds a quaternion directly from `[x, y, z, w]` components in [values]. */ + fun makeQuaternion(values: FloatArray): Quaternionf + { + return Quaternionf(values[0], values[1], values[2], values[3]) + } + + /** Linearly interpolates from [from] to [to] by [progress] (0..1). */ + fun lerp(from: Vector3f?, to: Vector3f?, progress: Float): Vector3f + { + val res = Vector3f(from) + res.lerp(to, progress) + return res + } + + private const val THRESHOLD = 0.9995 + + /** Spherically interpolates from [v0] to [v1] by [t] (0..1), falling back to lerp when the inputs are nearly parallel. */ + fun slerp(v0: Quaternionfc, v1: Quaternionfc, t: Float): Quaternionf + { + // From https://en.wikipedia.org/w/index.php?title=Slerp&oldid=928959428 + // License: CC BY-SA 3.0 https://creativecommons.org/licenses/by-sa/3.0/ + + // Compute the cosine of the angle between the two vectors. + // If the dot product is negative, slerp won't take + // the shorter path. Note that v1 and -v1 are equivalent when + // the negation is applied to all four components. Fix by + // reversing one quaternion. + + var v1 = v1 + var dot = v0.x() * v1.x() + v0.y() * v1.y() + v0.z() * v1.z() + v0.w() * v1.w() + if (dot < 0.0f) + { + v1 = Quaternionf(-v1.x(), -v1.y(), -v1.z(), -v1.w()) + dot = -dot + } + + // If the inputs are too close for comfort, linearly interpolate + // and normalize the result. + if (dot > THRESHOLD) + { + val x = Mth.lerp(t, v0.x(), v1.x()) + val y = Mth.lerp(t, v0.y(), v1.y()) + val z = Mth.lerp(t, v0.z(), v1.z()) + val w = Mth.lerp(t, v0.w(), v1.w()) + return Quaternionf(x, y, z, w) + } + + // Since dot is in range [0, DOT_THRESHOLD], acos is safe + val angle01 = acos(dot.toDouble()).toFloat() + val angle0t = angle01 * t + val sin0t = Mth.sin(angle0t) + val sin01 = Mth.sin(angle01) + val sin1t = Mth.sin(angle01 - angle0t) + + val s1 = sin0t / sin01 + val s0 = sin1t / sin01 + + return Quaternionf( + s0 * v0.x() + s1 * v1.x(), + s0 * v0.y() + s1 * v1.y(), + s0 * v0.z() + s1 * v1.z(), + s0 * v0.w() + s1 * v1.w() + ) + } + + /** Interpolates every component of a [Transformation] (translation/scale linearly, rotations spherically) from [one] to [that] by [progress]. */ + fun slerp(one: Transformation, that: Transformation, progress: Float): Transformation + { + return Transformation( + lerp(one.translation, that.translation, progress), + slerp(one.leftRotation, that.leftRotation, progress), + lerp(one.scale, that.scale, progress), + slerp(one.rightRotation, that.rightRotation, progress) + ) + } + + /** Whether [v1] and [v2] are componentwise equal within [epsilon]. */ + fun epsilonEquals(v1: Vector4f, v2: Vector4f, epsilon: Float): Boolean + { + return Mth.abs(v1.x() - v2.x()) < epsilon && Mth.abs(v1.y() - v2.y()) < epsilon && Mth.abs( + v1.z() - v2.z() + ) < epsilon && Mth.abs(v1.w() - v2.w()) < epsilon + } + + /** Gson deserializer for the "TRSR" [Transformation] JSON format: `"identity"`, a raw 3x4 matrix, or an object with `translation`/`rotation`(`left_rotation`)/`scale`/`right_rotation`(`post-rotation`)/`origin`. */ + class Deserializer : JsonDeserializer + { + @Throws(JsonParseException::class) + override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Transformation + { + if (json.isJsonPrimitive && json.asJsonPrimitive.isString) + { + val transform = json.asString + if (transform == "identity") + { + return Transformation.identity() + } else + { + throw JsonParseException("TRSR: unknown default string: $transform") + } + } + if (json.isJsonArray) + { + // direct matrix array + return Transformation(parseMatrix(json)) + } + if (!json.isJsonObject) throw JsonParseException("TRSR: expected array or object, got: $json") + val obj = json.asJsonObject + val ret: Transformation + if (obj.has("matrix")) + { + // matrix as a sole key + ret = Transformation(parseMatrix(obj["matrix"])) + if (obj.entrySet().size > 1) + { + throw JsonParseException("TRSR: can't combine matrix and other keys") + } + return ret + } + var translation: Vector3f? = null + var leftRot: Quaternionf? = null + var scale: Vector3f? = null + var rightRot: Quaternionf? = null + // TODO: Default origin is opposing corner, due to a mistake. + // This should probably be replaced with center in future versions. + var origin: Vector3f? = + TransformOrigin.OPPOSING_CORNER.vector // TODO: Changing this to ORIGIN_CENTER breaks models, function content needs changing too -C + val elements: MutableSet = HashSet(obj.keySet()) + if (obj.has("translation")) + { + translation = Vector3f(parseFloatArray(obj["translation"], 3, "Translation")) + elements.remove("translation") + } + if (obj.has("rotation")) + { + leftRot = parseRotation(obj["rotation"]) + elements.remove("rotation") + } else if (obj.has("left_rotation")) + { + leftRot = parseRotation(obj["left_rotation"]) + elements.remove("left_rotation") + } + if (obj.has("scale")) + { + if (!obj["scale"].isJsonArray) + { + try + { + val s = obj["scale"].asNumber.toFloat() + scale = Vector3f(s, s, s) + } catch (ex: ClassCastException) + { + throw JsonParseException("TRSR scale: expected number or array, got: " + obj["scale"]) + } + } else + { + scale = Vector3f(parseFloatArray(obj["scale"], 3, "Scale")) + } + elements.remove("scale") + } + if (obj.has("right_rotation")) + { + rightRot = parseRotation(obj["right_rotation"]) + elements.remove("right_rotation") + } else if (obj.has("post-rotation")) + { + rightRot = parseRotation(obj["post-rotation"]) + elements.remove("post-rotation") + } + if (obj.has("origin")) + { + origin = parseOrigin(obj) + elements.remove("origin") + } + if (!elements.isEmpty()) throw JsonParseException( + "TRSR: can either have single 'matrix' key, or a combination of 'translation', 'rotation' OR 'left_rotation', 'scale', 'post-rotation' (legacy) OR 'right_rotation', 'origin'. Found: " + java.lang.String.join( + ", ", + elements + ) + ) + + val matrix = Transformation(translation, leftRot, scale, rightRot) + return matrix.applyOriginLocal(Vector3f(origin)) + } + + fun Transformation.isIdentityLocal(): Boolean + { + return this@isIdentityLocal == Transformation.identity() + } + + fun Transformation.applyOriginLocal(origin: Vector3f): Transformation + { + val transform: Transformation = this@applyOriginLocal + if (transform.isIdentityLocal()) return Transformation.identity() + + val ret = transform.matrix + val tmp = Matrix4f().translation(origin.x(), origin.y(), origin.z()) + tmp.mul(ret, ret) + tmp.translation(-origin.x(), -origin.y(), -origin.z()) + ret.mul(tmp) + return Transformation(ret) + } + + companion object + { + private fun parseOrigin(obj: JsonObject): Vector3f? + { + var origin: Vector3f? = null + + // Two types supported: string ("center", "corner", "opposing-corner") and array ([x, y, z]) + val originElement = obj["origin"] + if (originElement.isJsonArray) + { + origin = Vector3f(parseFloatArray(originElement, 3, "Origin")) + } else if (originElement.isJsonPrimitive) + { + val originString = originElement.asString + val originEnum = TransformOrigin.fromString(originString) + ?: throw JsonParseException("Origin: expected one of 'center', 'corner', 'opposing-corner'") + origin = originEnum.vector + } else + { + throw JsonParseException("Origin: expected an array or one of 'center', 'corner', 'opposing-corner'") + } + return origin + } + + fun parseMatrix(e: JsonElement): Matrix4f + { + if (!e.isJsonArray) throw JsonParseException("Matrix: expected an array, got: $e") + val m = e.asJsonArray + if (m.size() != 3) throw JsonParseException("Matrix: expected an array of length 3, got: " + m.size()) + val matrix = Matrix4f() + for (rowIdx in 0..2) + { + if (!m[rowIdx].isJsonArray) throw JsonParseException("Matrix row: expected an array, got: " + m[rowIdx]) + val r = m[rowIdx].asJsonArray + if (r.size() != 4) throw JsonParseException("Matrix row: expected an array of length 4, got: " + r.size()) + for (columnIdx in 0..3) + { + try + { + matrix[columnIdx, rowIdx] = r[columnIdx].asNumber.toFloat() + } catch (ex: ClassCastException) + { + throw JsonParseException("Matrix element: expected number, got: " + r[columnIdx]) + } + } + } + // JOML's unsafe matrix component setter does not recalculate these properties, so the matrix would stay marked as identity + matrix.determineProperties() + return matrix + } + + fun parseFloatArray(e: JsonElement, length: Int, prefix: String): FloatArray + { + if (!e.isJsonArray) throw JsonParseException("$prefix: expected an array, got: $e") + val t = e.asJsonArray + if (t.size() != length) throw JsonParseException(prefix + ": expected an array of length " + length + ", got: " + t.size()) + val ret = FloatArray(length) + for (i in 0 until length) + { + try + { + ret[i] = t[i].asNumber.toFloat() + } catch (ex: ClassCastException) + { + throw JsonParseException(prefix + " element: expected number, got: " + t[i]) + } + } + return ret + } + + fun parseAxisRotation(e: JsonElement): Quaternionf + { + if (!e.isJsonObject) throw JsonParseException("Axis rotation: object expected, got: $e") + val obj = e.asJsonObject + if (obj.entrySet().size != 1) throw JsonParseException("Axis rotation: expected single axis object, got: $e") + val entry = obj.entrySet().iterator().next() + val ret: Quaternionf + try + { + ret = if (entry.key == "x") + { + Axis.XP.rotationDegrees(entry.value.asNumber.toFloat()) + } else if (entry.key == "y") + { + Axis.YP.rotationDegrees(entry.value.asNumber.toFloat()) + } else if (entry.key == "z") + { + Axis.ZP.rotationDegrees(entry.value.asNumber.toFloat()) + } else throw JsonParseException("Axis rotation: expected single axis key, got: " + entry.key) + } catch (ex: ClassCastException) + { + throw JsonParseException("Axis rotation value: expected number, got: " + entry.value) + } + return ret + } + + fun parseRotation(e: JsonElement): Quaternionf + { + if (e.isJsonArray) + { + if (e.asJsonArray[0].isJsonObject) + { + val ret = Quaternionf() + for (a in e.asJsonArray) + { + ret.mul(parseAxisRotation(a)) + } + return ret + } else if (e.isJsonArray) + { + val array = e.asJsonArray + return if (array.size() == 3) //Vanilla rotation + quatFromXYZ(parseFloatArray(e, 3, "Rotation"), true) + else // quaternion + makeQuaternion(parseFloatArray(e, 4, "Rotation")) + } else throw JsonParseException("Rotation: expected array or object, got: $e") + } else if (e.isJsonObject) + { + return parseAxisRotation(e) + } else throw JsonParseException("Rotation: expected array or object, got: $e") + } + } + } + + /** Named reference points a [Deserializer]-parsed transform's rotation/scale can pivot around. */ + enum class TransformOrigin(val vector: Vector3f, private val serialName: String) : StringRepresentable + { + CENTER(Vector3f(.5f, .5f, .5f), "center"), + CORNER(Vector3f(), "corner"), + OPPOSING_CORNER(Vector3f(1f, 1f, 1f), "opposing-corner"); + + override fun getSerializedName(): String + { + return serialName + } + + companion object + { + fun fromString(originName: String): TransformOrigin? + { + if (CENTER.serializedName == originName) + { + return CENTER + } + if (CORNER.serializedName == originName) + { + return CORNER + } + if (OPPOSING_CORNER.serializedName == originName) + { + return OPPOSING_CORNER + } + return null + } + } + } +} diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ADatagenEvents.kt b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ADatagenEvents.kt new file mode 100644 index 000000000..3b822a2c2 --- /dev/null +++ b/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ADatagenEvents.kt @@ -0,0 +1,68 @@ +package net.kernelpanicsoft.archie.events + +import dev.architectury.event.Event +import dev.architectury.event.EventFactory +import dev.architectury.event.EventResult +import dev.architectury.platform.Mod +import net.kernelpanicsoft.archie.data.ADataGenerator + +/** + * `archie-datagen`'s central, mod-scoped event registry, built on top of Architectury's event + * system - the datagen half of what used to be `archie-core`'s combined `AEvents`. + * + * A downstream mod opts in with `ADatagenEvents += MOD` (its own [Mod] descriptor), then listens + * for [GATHER_DATA] via [GatherDataHandler.Companion.create]. Handlers are mod-scoped: they check + * the invoking [Mod] and no-op (via [EventResult.pass]) for any mod other than the one they were + * created for. + */ +object ADatagenEvents +{ + /** Fired during datagen runs; handlers should gate by owning [Mod]. */ + val GATHER_DATA: Event = EventFactory.createEventResult() + + private val mods: MutableList = mutableListOf() + + /** Mods that opted into `archie-datagen`'s event plumbing via `ADatagenEvents += MOD`. */ + val MODS: List + get() = mods + + fun register(mod: Mod) + { + mods.add(mod) + } + + operator fun plusAssign(mod: Mod) = register(mod) + + /** + * Handler for [GATHER_DATA]. Implementations are produced via [HandlerConstructor.create] + * and forward to the registered `block` only when the firing [ADataGenerator.mod] matches + * the [Mod] the handler was created for. + */ + interface GatherDataHandler : Handler + { + operator fun invoke(dataGenerator: ADataGenerator): EventResult + + companion object : HandlerConstructor + { + override fun create(mod: Mod, block: ADataGenerator.() -> Unit): GatherDataHandler + { + return GatherDataHandlerImpl(mod, block) + } + + class GatherDataHandlerImpl internal constructor( + private val mod: Mod, + private val gatherData: ADataGenerator.() -> Unit + ) : + GatherDataHandler + { + override operator fun invoke(dataGenerator: ADataGenerator): EventResult + { + if (this.mod != dataGenerator.mod) + return EventResult.pass() + dataGenerator.gatherData() + return EventResult.interruptDefault() + } + } + } + } +} diff --git a/Archie-Core/datagen/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension b/Archie-Core/datagen/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension new file mode 100644 index 000000000..66911e85a --- /dev/null +++ b/Archie-Core/datagen/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension @@ -0,0 +1 @@ +net.kernelpanicsoft.archie.data.internal.DatagenArchieExtension diff --git a/Archie-Core/datagen/fabric/build.gradle.kts b/Archie-Core/datagen/fabric/build.gradle.kts new file mode 100644 index 000000000..fbda67cf8 --- /dev/null +++ b/Archie-Core/datagen/fabric/build.gradle.kts @@ -0,0 +1,105 @@ +plugins { + alias(libs.plugins.archie) +} + +architectury { + platformSetupLoomIde() + fabric() +} + +actualizer { + actualizes(project(":archie-datagen-common")) +} + +configurations { + create("common") + compileClasspath.get().extendsFrom(configurations["common"]) + runtimeClasspath.get().extendsFrom(configurations["common"]) + testCompileClasspath.get().extendsFrom(compileClasspath.get()) + testRuntimeClasspath.get().extendsFrom(runtimeClasspath.get()) +} + +loom { + accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) + + mods { + maybeCreate("main").apply { + sourceSet(sourceSets.main.get()) + } + } + + runs { + getByName("client") { + name = "Minecraft Client" + source(sourceSets.main.get()) + vmArg("-XX:+AllowEnhancedClassRedefinition") + } + getByName("server") { + name = "Minecraft Server" + source(sourceSets.main.get()) + vmArgs("-XX:+AllowEnhancedClassRedefinition") + } + // This adds a new gradle task that runs the datagen API: "gradlew runDatagen" + create("datagen") { + client() + name = "Minecraft Datagen" + property("archie.datagen", "true") + property("archie.datagen.client", providers.gradleProperty("client_datagen").orElse("true").get()) + property("archie.datagen.server", providers.gradleProperty("server_datagen").orElse("true").get()) + property("fabric-api.datagen") + property("fabric-api.datagen.modid", "archie_datagen") + property("fabric-api.datagen.output-dir", file("src/main/generated").absolutePath) + + runDir = "build/datagen" + } + } +} + +fabricApi.configureDataGeneration { + createRunConfiguration = false + outputDirectory.set(file("src/main/generated")) +} + +dependencies { + modImplementation(libs.fabric.loader) + modApi(libs.fabric.api) + modImplementation(libs.kotlin.fabric) + compileOnly(libs.kotlinx.serialization) + + implementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.api) + testRuntimeOnly(libs.junit.jupiter.engine) + + "common"(project(":archie-datagen-common", "namedElements")) { isTransitive = false } + modApi(project(":archie-core-fabric")) +} + +modResources { + filesMatching.add("fabric.mod.json") +} + +tasks { + base.archivesName.set(base.archivesName.get() + "-datagen-fabric") + + test { + useJUnitPlatform() + } + + processResources { + dependsOn(processTestResources) + } + + processTestResources { + } + + classes { + finalizedBy(testClasses) + } + + sourcesJar { + val commonSources = project(":archie-datagen-common").tasks.sourcesJar + dependsOn(commonSources) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(commonSources.get().archiveFile.map { zipTree(it) }) + } +} diff --git a/Archie-Core/datagen/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java b/Archie-Core/datagen/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java new file mode 100644 index 000000000..033370dc0 --- /dev/null +++ b/Archie-Core/datagen/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java @@ -0,0 +1,32 @@ +package net.kernelpanicsoft.archie.mixin.fabric; + +import com.llamalad7.mixinextras.sugar.Local; +import com.llamalad7.mixinextras.sugar.ref.LocalRef; +import net.kernelpanicsoft.archie.Archie; +import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform; +import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint; +import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper; +import net.fabricmc.loader.api.entrypoint.EntrypointContainer; +import net.kernelpanicsoft.archie.data.ADataGeneratorPlatformInternal; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.List; + +@SuppressWarnings("UnstableApiUsage") +@Mixin(FabricDataGenHelper.class) +class FabricDataGenHelperMixin +{ + @SuppressWarnings("UnresolvedLocalCapture") + @Inject(remap = false, method = "runInternal()V", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/fabricmc/loader/api/FabricLoader;getEntrypointContainers(Ljava/lang/String;Ljava/lang/Class;)Ljava/util/List;")) + private static void addEntrypoints(CallbackInfo ci, @Local(name = "dataGeneratorInitializers") LocalRef>> dataGeneratorInitializers) + { + if (ADataGeneratorPlatform.INSTANCE.isDataGen()) + { + Archie.LOGGER.info("Registering DataGen Handlers"); + ADataGeneratorPlatformInternal.addEntrypoints(dataGeneratorInitializers); + } + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorFabric.kt b/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorFabric.kt new file mode 100644 index 000000000..13a96d563 --- /dev/null +++ b/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorFabric.kt @@ -0,0 +1,22 @@ +package net.kernelpanicsoft.archie.data + +import dev.architectury.platform.Mod +import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator +import net.minecraft.data.DataGenerator +import net.minecraft.data.DataProvider + +/** Fabric [ADataGenerator], registering providers with a Fabric [FabricDataGenerator] pack. */ +class ADataGeneratorFabric(private val fabricDataGenerator: FabricDataGenerator, override val mod: Mod) : ADataGenerator() +{ + /** Reflectively forces the created provider's `toRun` flag since Fabric's pack API always runs a provider once added. */ + override fun addProvider(run: Boolean, factory: ARegistryAwareDataProviderFactory): T + { + val pack = fabricDataGenerator.createPack() + val toRun = DataGenerator.PackGenerator::class.java.getDeclaredField("toRun") + toRun.isAccessible = true + toRun.setBoolean(pack, run) + return pack.addProvider { output, registriesFuture -> + factory(output, registriesFuture) + } + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt b/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt new file mode 100644 index 000000000..4ee285d5a --- /dev/null +++ b/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt @@ -0,0 +1,66 @@ +package net.kernelpanicsoft.archie.data + +import com.llamalad7.mixinextras.sugar.ref.LocalRef +import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint +import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator +import net.fabricmc.loader.api.FabricLoader +import net.fabricmc.loader.api.ModContainer +import net.fabricmc.loader.api.entrypoint.EntrypointContainer +import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform.isDataGen +import net.kernelpanicsoft.archie.events.ADatagenEvents +import net.minecraft.core.RegistrySetBuilder + +/** + * Backs `FabricDataGenHelperMixin`, which calls [addEntrypoints] mid-way through + * `FabricDataGenHelper.runInternal()` to splice in synthetic datagen entrypoints. + * + * Archie mods don't declare a `fabric-datagen` entrypoint in `fabric.mod.json`; instead they + * register with [ADatagenEvents.MODS] at init time. This object bridges that registration into + * the `EntrypointContainer` list Fabric's data generator actually + * consumes. + */ +internal object ADataGeneratorPlatformInternal +{ + /** + * Appends one [EntrypointContainer] per mod in [ADatagenEvents.MODS] to + * [dataGeneratorInitializers], each of which fires [ADatagenEvents.GATHER_DATA] with an + * [ADataGeneratorFabric] for that mod. No-op outside a datagen run + * ([ADataGeneratorPlatform.isDataGen] false). + */ + @JvmStatic + @JvmName("addEntrypoints") + internal fun addEntrypoints(dataGeneratorInitializers: LocalRef>>) + { + if (!isDataGen) return + + // Fabric expects datagen entrypoints; inject one per registered Archie mod. + val result = dataGeneratorInitializers.get().toMutableList() + for (mod in ADatagenEvents.MODS) + { + result.add(object : EntrypointContainer + { + override fun getEntrypoint(): DataGeneratorEntrypoint + { + return object : DataGeneratorEntrypoint + { + override fun onInitializeDataGenerator(fabricDataGenerator: FabricDataGenerator) + { + ADatagenEvents.GATHER_DATA.invoker()(ADataGeneratorFabric(fabricDataGenerator, mod)) + } + + override fun buildRegistry(registryBuilder: RegistrySetBuilder?) + { + super.buildRegistry(registryBuilder) + } + } + } + + override fun getProvider(): ModContainer + { + return FabricLoader.getInstance().getModContainer(mod.modId).orElse(null) + } + }) + } + dataGeneratorInitializers.set(result) + } +} diff --git a/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.fabric.kt b/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.fabric.kt new file mode 100644 index 000000000..9669b94ac --- /dev/null +++ b/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.fabric.kt @@ -0,0 +1,58 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import kotlinx.serialization.json.Json +import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput +import net.fabricmc.fabric.api.datagen.v1.provider.FabricRecipeProvider +import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition +import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.common.conditions.fabric +import net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider +import net.kernelpanicsoft.archie.serialization.kSerializer +import net.minecraft.advancements.Advancement +import net.minecraft.advancements.AdvancementHolder +import net.minecraft.core.HolderLookup +import net.minecraft.data.recipes.RecipeOutput +import net.minecraft.data.recipes.RecipeProvider +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.crafting.Recipe +import java.util.concurrent.CompletableFuture + +/** Fabric implementation of [ADatagenConditionsPlatform]. */ +actual object ADatagenConditionsPlatform +{ + /** Attaches [condition] to whatever [output] accepts next, via [FabricDataGenHelper.addConditions]. */ + actual fun withCondition(output: RecipeOutput, condition: IACondition): RecipeOutput + { + return object : RecipeOutput + { + @Suppress("UnstableApiUsage") + override fun accept(identifier: ResourceLocation, recipe: Recipe<*>, advancementEntry: AdvancementHolder?) + { + FabricDataGenHelper.addConditions(recipe, arrayOf(condition.fabric)) + Archie.LOGGER.info(Json.encodeToString(ResourceCondition.CODEC.kSerializer, condition.fabric)) + output.accept(identifier, recipe, advancementEntry) + } + + override fun advancement(): Advancement.Builder + { + return output.advancement() + } + } + } + + /** Wraps [child] in a [FabricRecipeProvider] so its recipes go through Fabric's condition-aware output. */ + actual fun fabricRecipeProvider( + child: ARecipeProvider, + registries: CompletableFuture + ): RecipeProvider? + { + return object : FabricRecipeProvider(child.output as FabricDataOutput, registries) + { + override fun buildRecipes(exporter: RecipeOutput) + { + child.buildRecipes(exporter) + } + } + } +} diff --git a/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt b/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt new file mode 100644 index 000000000..c8207a19d --- /dev/null +++ b/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt @@ -0,0 +1,23 @@ +package net.kernelpanicsoft.archie.data.common.tags + +import net.fabricmc.fabric.impl.datagen.FabricTagBuilder +import net.minecraft.data.tags.TagsProvider +import net.minecraft.tags.* + +/** Fabric implementation of [ATagBuilderPlatform]. */ +actual object ATagBuilderPlatform +{ + /** Sets the tag's `replace` flag via Fabric's `FabricTagBuilder` extension on [TagBuilder]. */ + @Suppress("UnstableApiUsage") + actual fun setTagReplace(builder: TagBuilder, replace: Boolean) + { + (builder as FabricTagBuilder).fabric_setReplace(replace) + } + + actual fun createTagBuilder(parent: TagsProvider.TagAppender, provider: ATagsProvider): IATagBuilder + { + return ATagBuilder(parent, provider) + } + + +} \ No newline at end of file diff --git a/Archie-Core/datagen/fabric/src/main/resources/archie_datagen.mixins.json b/Archie-Core/datagen/fabric/src/main/resources/archie_datagen.mixins.json new file mode 100644 index 000000000..80473e525 --- /dev/null +++ b/Archie-Core/datagen/fabric/src/main/resources/archie_datagen.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "package": "net.kernelpanicsoft.archie.mixin.fabric", + "compatibilityLevel": "JAVA_17", + "minVersion": "0.8", + "mixins": [ + "FabricDataGenHelperMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/Archie-Core/datagen/fabric/src/main/resources/fabric.mod.json b/Archie-Core/datagen/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 000000000..ec3006de0 --- /dev/null +++ b/Archie-Core/datagen/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}_datagen", + "version": "${mod_version}", + "name": "${mod_display_name} Datagen", + "description": "${mod_display_name}'s datagen DSL - dev-time only, never shipped in a production jar.", + "authors": [ + "${mod_authors}" + ], + "contact": { + "homepage": "${mod_url}", + "sources": "${mod_source}" + }, + "license": "${mod_license}", + "environment": "*", + "mixins": [ + "${mod_id}_datagen.mixins.json" + ], + "depends": { + "minecraft": "${versions.minecraft}", + "fabricloader": ">=${versions.fabric_loader}", + "fabric-api": ">=${versions.fabric_api}", + "fabric-language-kotlin": ">=${versions.kotlin_fabric}", + "archie": ">=${mod_version}" + } +} diff --git a/Archie-Core/datagen/neoforge/build.gradle.kts b/Archie-Core/datagen/neoforge/build.gradle.kts new file mode 100644 index 000000000..e0f97e66c --- /dev/null +++ b/Archie-Core/datagen/neoforge/build.gradle.kts @@ -0,0 +1,102 @@ +plugins { + alias(libs.plugins.archie) +} + +architectury { + platformSetupLoomIde() + neoForge() +} + +actualizer { + actualizes(project(":archie-datagen-common")) +} + +configurations { + create("common") + configureEach { + exclude(group = "thedarkcolour", module = "kotlinforforge-neoforge") + exclude(group = "remapped.thedarkcolour", module = "kotlinforforge-neoforge-1d1bcbf2") + } + compileClasspath.get().extendsFrom(configurations["common"]) + runtimeClasspath.get().extendsFrom(configurations["common"]) + testCompileClasspath.get().extendsFrom(compileClasspath.get()) + testRuntimeClasspath.get().extendsFrom(runtimeClasspath.get()) +} + +loom { + accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) + + mods { + maybeCreate("main").apply { + sourceSet(sourceSets.main.get()) + } + } + + runs { + getByName("client") { + name = "Minecraft Client" + source(sourceSets.main.get()) + vmArgs("-XX:+AllowEnhancedClassRedefinition") + property("kotlinx.coroutines.debug", "off") + } + getByName("server") { + name = "Minecraft Server" + source(sourceSets.main.get()) + vmArgs("-XX:+AllowEnhancedClassRedefinition") + property("kotlinx.coroutines.debug", "off") + } + create("datagen") { + data() + name = "Minecraft Datagen" + property("archie.datagen", "true") + property("archie.datagen.client", providers.gradleProperty("client_datagen").orElse("true").get()) + property("archie.datagen.server", providers.gradleProperty("server_datagen").orElse("true").get()) + property("kotlinx.coroutines.debug", "off") + programArgs("--all", "--mod", "archie_datagen") + programArgs("--output", file("src/main/generated").absolutePath) + } + } +} + +dependencies { + "neoForge"(libs.neoforge) + implementation(libs.kotlin.neoforge) + compileOnly(libs.kotlinx.serialization) + + implementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.api) + testRuntimeOnly(libs.junit.jupiter.engine) + + "common"(project(":archie-datagen-common", "namedElements")) { isTransitive = false } + modApi(project(":archie-core-neoforge")) +} + +modResources { + filesMatching.add("META-INF/neoforge.mods.toml") +} + +tasks { + base.archivesName.set(base.archivesName.get() + "-datagen-neoforge") + + test { + useJUnitPlatform() + } + + processResources { + dependsOn(processTestResources) + } + + processTestResources { + } + + classes { + finalizedBy(testClasses) + } + + sourcesJar { + val commonSources = project(":archie-datagen-common").tasks.sourcesJar + dependsOn(commonSources) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(commonSources.get().archiveFile.map { zipTree(it) }) + } +} diff --git a/Archie-Core/datagen/neoforge/gradle.properties b/Archie-Core/datagen/neoforge/gradle.properties new file mode 100644 index 000000000..2914393db --- /dev/null +++ b/Archie-Core/datagen/neoforge/gradle.properties @@ -0,0 +1 @@ +loom.platform=neoforge \ No newline at end of file diff --git a/Archie-Core/datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java b/Archie-Core/datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java new file mode 100644 index 000000000..0b6ecc37e --- /dev/null +++ b/Archie-Core/datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java @@ -0,0 +1,31 @@ +package net.kernelpanicsoft.archie.mixin.neoforge; + +import net.kernelpanicsoft.archie.Archie; +import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform; +import net.kernelpanicsoft.archie.data.ADataGeneratorPlatformInternal; +import net.neoforged.fml.ModList; +import net.neoforged.neoforge.data.event.GatherDataEvent; +import net.neoforged.neoforge.data.loading.DatagenModLoader; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.io.File; +import java.nio.file.Path; +import java.util.Collection; +import java.util.Set; + +@Mixin(DatagenModLoader.class) +class DatagenModLoaderMixin +{ + @Inject(method = "begin(Ljava/util/Set;Ljava/nio/file/Path;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Set;ZZZZZZLjava/lang/String;Ljava/io/File;)V", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/ModLoader;runEventGenerator(Ljava/util/function/Function;)V")) + private static void addEventHandlers(Set mods, Path path, Collection inputs, Collection existingPacks, Set existingMods, boolean serverGenerators, boolean clientGenerators, boolean devToolGenerators, boolean reportsGenerator, boolean structureValidator, boolean flat, String assetIndex, File assetsDir, CallbackInfo ci) + { + if (ADataGeneratorPlatform.INSTANCE.isDataGen()) + { + Archie.LOGGER.info("Registering DataGen Handlers"); + ADataGeneratorPlatformInternal.addEventHandlers(); + } + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorNeoForge.kt b/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorNeoForge.kt new file mode 100644 index 000000000..62fc48480 --- /dev/null +++ b/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorNeoForge.kt @@ -0,0 +1,17 @@ +package net.kernelpanicsoft.archie.data + +import dev.architectury.platform.Mod +import net.minecraft.data.DataProvider +import net.minecraft.data.PackOutput +import net.neoforged.neoforge.data.event.GatherDataEvent + +/** NeoForge [ADataGenerator], registering providers with the [GatherDataEvent]'s underlying generator. */ +class ADataGeneratorNeoForge(private val forgeDataGenerator: GatherDataEvent, override val mod: Mod) : ADataGenerator() +{ + override fun addProvider(run: Boolean, factory: ARegistryAwareDataProviderFactory): T + { + return forgeDataGenerator.generator.addProvider(run, DataProvider.Factory { output: PackOutput -> + factory(output, forgeDataGenerator.lookupProvider) + }) + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt b/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt new file mode 100644 index 000000000..006872eac --- /dev/null +++ b/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt @@ -0,0 +1,31 @@ +package net.kernelpanicsoft.archie.data + +import net.kernelpanicsoft.archie.events.ADatagenEvents +import net.neoforged.fml.ModList +import net.neoforged.neoforge.data.event.GatherDataEvent + +/** Backs [ADataGeneratorPlatform] on NeoForge: wires each registered mod's [GatherDataEvent] listener into [ADatagenEvents.GATHER_DATA]. */ +internal object ADataGeneratorPlatformInternal +{ + /** + * Called from `DatagenModLoaderMixin` mid-way through NeoForge's datagen bootstrap. No-ops + * outside a datagen run; otherwise, for every mod in [ADatagenEvents.MODS], subscribes to + * that mod's [GatherDataEvent] and fires [ADatagenEvents.GATHER_DATA] with an + * [ADataGeneratorNeoForge] wrapping it. + */ + @JvmStatic + @JvmName("addEventHandlers") + fun addEventHandlers() + { + if (!ADataGeneratorPlatform.isDataGen) return + + for (mod in ADatagenEvents.MODS) + { + ModList.get().getModContainerById(mod.modId).ifPresent { + it.eventBus?.addListener { event -> + ADatagenEvents.GATHER_DATA.invoker()(ADataGeneratorNeoForge(event, mod)) + } + } + } + } +} diff --git a/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.neoforge.kt b/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.neoforge.kt new file mode 100644 index 000000000..8e34ba7ba --- /dev/null +++ b/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.neoforge.kt @@ -0,0 +1,47 @@ +package net.kernelpanicsoft.archie.data.common.conditions + +import net.kernelpanicsoft.archie.data.common.conditions.neoforge +import net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider +import net.minecraft.advancements.Advancement +import net.minecraft.advancements.AdvancementHolder +import net.minecraft.core.HolderLookup +import net.minecraft.data.recipes.RecipeOutput +import net.minecraft.data.recipes.RecipeProvider +import net.minecraft.resources.ResourceLocation +import net.minecraft.world.item.crafting.Recipe +import net.neoforged.neoforge.common.conditions.ICondition +import java.util.concurrent.CompletableFuture + +/** NeoForge implementation of [ADatagenConditionsPlatform]. */ +actual object ADatagenConditionsPlatform +{ + /** Wraps [output] so every entry accepted through it also carries [condition]. */ + actual fun withCondition(output: RecipeOutput, condition: IACondition): RecipeOutput + { + return NeoForgeConditionalRecipeOutput(output, condition.neoforge) + } + + /** Fabric-only concern; always `null` on NeoForge. */ + actual fun fabricRecipeProvider(child: ARecipeProvider, registries: CompletableFuture): RecipeProvider? = null + + /** [RecipeOutput] wrapper that always attaches [condition] to whatever [inner] accepts. */ + class NeoForgeConditionalRecipeOutput(private val inner: RecipeOutput, private val condition: ICondition) : + RecipeOutput + { + override fun advancement(): Advancement.Builder + { + return inner.advancement() + } + + /** Forwards to [inner], attaching [condition]; any [iConditions] passed by the caller are not currently applied. */ + override fun accept( + id: ResourceLocation, + recipe: Recipe<*>, + adv: AdvancementHolder?, + vararg iConditions: ICondition + ) + { + inner.accept(id, recipe, adv, this.condition) + } + } +} diff --git a/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt b/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt new file mode 100644 index 000000000..23692eeee --- /dev/null +++ b/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt @@ -0,0 +1,19 @@ +package net.kernelpanicsoft.archie.data.common.tags + +import net.minecraft.data.tags.TagsProvider +import net.minecraft.tags.TagBuilder + +/** NeoForge implementation of [ATagBuilderPlatform]. */ +actual object ATagBuilderPlatform +{ + /** Sets the tag's `replace` flag directly via vanilla's [TagBuilder.replace], which NeoForge doesn't restrict. */ + actual fun setTagReplace(builder: TagBuilder, replace: Boolean) + { + builder.replace(replace) + } + + actual fun createTagBuilder(parent: TagsProvider.TagAppender, provider: ATagsProvider): IATagBuilder + { + return ATagBuilder(parent, provider) + } +} \ No newline at end of file diff --git a/Archie-Core/datagen/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/Archie-Core/datagen/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 000000000..ef4a72c4b --- /dev/null +++ b/Archie-Core/datagen/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,38 @@ +modLoader = "klf" +loaderVersion = "[${versions.kotlin_neoforge_range},)" +issueTrackerURL = "" +license = "${mod_license}" + +[[mods]] +modId = "${mod_id}_datagen" +version = "${mod_version}" +displayName = "${mod_display_name} Datagen" +authors = "${mod_authors}" +description = ''' +${mod_display_name}'s datagen DSL - dev-time only, never shipped in a production jar. +''' +displayURL = "${mod_url}" + +[[mixins]] +config = "${mod_id}_datagen.mixins.json" + +[[dependencies."${mod_id}_datagen"]] +modId = "neoforge" +type = "required" +versionRange = "[${versions.neoforge_range},)" +ordering = "NONE" +side = "BOTH" + +[[dependencies."${mod_id}_datagen"]] +modId = "minecraft" +type = "required" +versionRange = "[${versions.minecraft}]" +ordering = "NONE" +side = "BOTH" + +[[dependencies."${mod_id}_datagen"]] +modId = "archie" +type = "required" +versionRange = "[${mod_version},)" +ordering = "AFTER" +side = "BOTH" diff --git a/Archie-Core/datagen/neoforge/src/main/resources/archie_datagen.mixins.json b/Archie-Core/datagen/neoforge/src/main/resources/archie_datagen.mixins.json new file mode 100644 index 000000000..65dd2eae8 --- /dev/null +++ b/Archie-Core/datagen/neoforge/src/main/resources/archie_datagen.mixins.json @@ -0,0 +1,12 @@ +{ + "required": true, + "package": "net.kernelpanicsoft.archie.mixin.neoforge", + "compatibilityLevel": "JAVA_17", + "minVersion": "0.8", + "mixins": [ + "DatagenModLoaderMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/Archie-Core/gametest/common/build.gradle.kts b/Archie-Core/gametest/common/build.gradle.kts new file mode 100644 index 000000000..ccfc6be89 --- /dev/null +++ b/Archie-Core/gametest/common/build.gradle.kts @@ -0,0 +1,40 @@ +architectury { + common("fabric", "neoforge") +} + +actualizer { + stubUnfulfilledExpects() +} + +loom { + accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) +} + +dependencies { + modApi(project(":archie-core-common")) + modApi(libs.architectury.common) + modApi(libs.storage.common) + modApi(libs.storage.resources.common) + + compileOnly(kotlin("reflect")) + implementation(libs.junit.jupiter.api) + // Gives ComposeScreen a virtual clock/dispatcher during tests - never on a real player's + // classpath (archie-gametest is dev/test-only, never shipped in a production jar). + implementation(libs.kotlinx.coroutines.test) + testImplementation(libs.junit.jupiter.api) + testImplementation(kotlin("reflect")) + testRuntimeOnly(libs.junit.jupiter.engine) +} + +tasks { + base.archivesName.set(base.archivesName.get() + "-gametest-common") + + jar { + from(sourceSets.main.get().output) + exclude("**/*StubKt.class") + } + + sourcesJar { + exclude("**/*Stub.kt") + } +} diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEvents.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AGametestEvents.kt similarity index 64% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEvents.kt rename to Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AGametestEvents.kt index 2ca7be7da..d27594dc9 100644 --- a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEvents.kt +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AGametestEvents.kt @@ -1,35 +1,29 @@ package net.kernelpanicsoft.archie.events -import net.kernelpanicsoft.archie.data.ADataGenerator -import net.kernelpanicsoft.archie.gametest.AGameTestPlatform import dev.architectury.event.Event import dev.architectury.event.EventFactory import dev.architectury.event.EventResult import dev.architectury.platform.Mod -import dev.architectury.platform.Platform -import dev.architectury.utils.Env +import net.kernelpanicsoft.archie.gametest.AGameTestPlatform import net.kernelpanicsoft.archie.gametest.AGameTestSide /** - * Archie's central, mod-scoped event registry, built on top of Architectury's event system. + * `archie-gametest`'s central, mod-scoped event registry, built on top of Architectury's event + * system - the gametest half of what used to be `archie-core`'s combined `AEvents`. * - * A downstream mod opts in with `AEvents += MOD` (its own [Mod] descriptor), then listens for - * [GATHER_DATA] and/or [REGISTER_GAME_TEST] via the corresponding handler's - * [HandlerConstructor.create]. Handlers are mod-scoped: [GatherDataHandler] and - * [RegisterGameTestHandler] both check the invoking [Mod] and no-op (via - * [EventResult.pass]) for any mod other than the one they were created for. + * A downstream mod opts in with `AGametestEvents += MOD` (its own [Mod] descriptor), then listens + * for [REGISTER_GAME_TEST] via [RegisterGameTestHandler.Companion.create]. Handlers are + * mod-scoped: they check the invoking [Mod] and no-op (via [EventResult.pass]) for any mod other + * than the one they were created for. */ -object AEvents +object AGametestEvents { - /** Fired during datagen runs; handlers should gate by owning [Mod]. */ - val GATHER_DATA: Event = EventFactory.createEventResult() - /** Fired during gametest registration runs; handlers should register test classes per [Mod]. */ val REGISTER_GAME_TEST: Event = EventFactory.createEventResult() private val mods: MutableList = mutableListOf() - /** Mods that opted into Archie event plumbing via `AEvents += MOD`. */ + /** Mods that opted into `archie-gametest`'s event plumbing via `AGametestEvents += MOD`. */ val MODS: List get() = mods @@ -40,49 +34,6 @@ object AEvents operator fun plusAssign(mod: Mod) = register(mod) - /** Marker for a mod-scoped Architectury event listener created by a [HandlerConstructor]. */ - interface Handler - - /** Builds a mod-scoped [H] whose body invokes `block` on the event's [T] payload. */ - fun interface HandlerConstructor> - { - /** Creates an [H] for [mod] that runs [block] against the [T] payload when invoked. */ - fun create(mod: Mod, block: T.() -> Unit): H - } - - /** - * Handler for [GATHER_DATA]. Implementations are produced via [HandlerConstructor.create] - * and forward to the registered `block` only when the firing [ADataGenerator.mod] matches - * the [Mod] the handler was created for. - */ - interface GatherDataHandler : Handler - { - operator fun invoke(dataGenerator: ADataGenerator): EventResult - - companion object : HandlerConstructor - { - override fun create(mod: Mod, block: ADataGenerator.() -> Unit): GatherDataHandler - { - return GatherDataHandlerImpl(mod, block) - } - - class GatherDataHandlerImpl internal constructor( - private val mod: Mod, - private val gatherData: ADataGenerator.() -> Unit - ) : - GatherDataHandler - { - override operator fun invoke(dataGenerator: ADataGenerator): EventResult - { - if (this.mod != dataGenerator.mod) - return EventResult.pass() - dataGenerator.gatherData() - return EventResult.interruptDefault() - } - } - } - } - /** * DSL receiver passed to [REGISTER_GAME_TEST] listeners for declaring gametest classes. * @@ -175,11 +126,7 @@ object AEvents } return EventResult.interruptDefault() } - - } } } - - } diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt new file mode 100644 index 000000000..d3f868c88 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt @@ -0,0 +1,1653 @@ +package net.kernelpanicsoft.archie.gametest + +import com.mojang.realmsclient.RealmsMainScreen +import dev.architectury.platform.Mod +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestCoroutineScheduler +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.gui.ComposeIdleAware +import net.kernelpanicsoft.archie.gui.ComposeTestClockOverride +import net.kernelpanicsoft.archie.gui.LayerManagerProvider +import net.kernelpanicsoft.archie.util.setReflection +import net.minecraft.SharedConstants +import net.minecraft.client.Minecraft +import net.minecraft.client.Screenshot +import net.minecraft.client.gui.components.AbstractWidget +import net.minecraft.client.gui.screens.GenericMessageScreen +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.gui.screens.TitleScreen +import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen +import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen +import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState +import net.minecraft.core.BlockPos +import net.minecraft.core.Holder +import net.minecraft.core.registries.Registries +import net.minecraft.network.chat.Component +import net.minecraft.network.chat.contents.TranslatableContents +import net.minecraft.server.MinecraftServer +import net.minecraft.world.level.block.entity.BlockEntity +import net.minecraft.world.level.levelgen.presets.WorldPreset +import net.minecraft.world.level.levelgen.presets.WorldPresets +import org.apache.commons.lang3.function.FailableConsumer +import org.apache.commons.lang3.function.FailableFunction +import org.joml.Vector2i +import org.lwjgl.glfw.GLFW +import java.nio.file.Path +import java.util.* +import java.util.concurrent.CountDownLatch +import java.util.concurrent.TimeUnit +import java.util.concurrent.ConcurrentHashMap +import java.util.function.Consumer +import java.util.function.Predicate +import java.util.function.Supplier +import java.lang.reflect.Modifier +import java.nio.file.Files +import kotlin.reflect.full.primaryConstructor + +private const val DEFAULT_TICK_MILLIS = 50L +private const val CLIENT_EXEC_TIMEOUT_SECONDS = 10L +private const val WORLD_BUILDER_EXEC_TIMEOUT_SECONDS = 300L +private const val SCREEN_SET_TIMEOUT_TICKS = 40 +private const val COMPOSE_IDLE_TIMEOUT_TICKS = 20 +private const val COMPOSE_IDLE_CONSECUTIVE_CHECKS = 4 + +private object DedicatedServerLifecycleTracker { + private val activeServers = ConcurrentHashMap.newKeySet() + + fun register(server: Any) { + activeServers += server + } + + fun unregister(server: Any) { + activeServers -= server + } + + fun stopAllLeakedServers() { + activeServers.toList().forEach { server -> + runCatching { + ADedicatedServerPlatform.stop(server) + }.onFailure { error -> + Archie.LOGGER.warn("Failed stopping leaked dedicated server: ${error.message}") + } + } + + val deadlineNanos = System.nanoTime() + TimeUnit.SECONDS.toNanos(10) + while (System.nanoTime() < deadlineNanos) { + val stillAlive = activeServers.filter { server -> + runCatching { ADedicatedServerPlatform.isAlive(server) }.getOrDefault(false) + } + if (stillAlive.isEmpty()) break + Thread.sleep(50L) + } + + activeServers.removeIf { server -> + runCatching { !ADedicatedServerPlatform.isAlive(server) }.getOrDefault(true) + } + } +} + +/** + * Vanilla's dedicated-server bootstrap (`Main.main`) always reads `server.properties`/`eula.txt` + * from the process's current working directory, regardless of the `--universe` argument - so + * per-test isolation there isn't possible without spawning a separate process. This captures + * whatever was there before the first override (once) and restores it once the harness run + * finishes, so the repo's own working directory isn't left permanently polluted with test + * artifacts. Deliberately not a blocking lock: a leaked dedicated server (one whose context is + * never explicitly closed) is already recovered via [DedicatedServerLifecycleTracker], and a + * blocking acquire here that's never released on that path would deadlock every later + * dedicated-server test instead of just risking a rare overlapping-write race. + */ +private object CwdBootstrapFileGuard { + private var captured = false + private var originalServerProperties: ByteArray? = null + private var originalEula: ByteArray? = null + + @Synchronized + fun writeOverride(properties: Properties) { + val cwd = Path.of(".") + val propertiesPath = cwd.resolve("server.properties") + val eulaPath = cwd.resolve("eula.txt") + + if (!captured) { + originalServerProperties = if (Files.exists(propertiesPath)) Files.readAllBytes(propertiesPath) else null + originalEula = if (Files.exists(eulaPath)) Files.readAllBytes(eulaPath) else null + captured = true + } + + Files.newBufferedWriter(propertiesPath).use { writer -> + properties.store(writer, "Archie GameTest dedicated server properties") + } + Files.newBufferedWriter(eulaPath).use { writer -> + writer.write("eula=true") + writer.newLine() + } + } + + @Synchronized + fun restoreIfCaptured() { + if (!captured) return + runCatching { + val cwd = Path.of(".") + val propertiesPath = cwd.resolve("server.properties") + val eulaPath = cwd.resolve("eula.txt") + originalServerProperties?.let { Files.write(propertiesPath, it) } ?: Files.deleteIfExists(propertiesPath) + originalEula?.let { Files.write(eulaPath, it) } ?: Files.deleteIfExists(eulaPath) + }.onFailure { error -> + Archie.LOGGER.warn("Failed to restore original server.properties/eula.txt in working directory: ${error.message}") + } + captured = false + originalServerProperties = null + originalEula = null + } +} + +/** Marks a test method to be executed by the Archie client GameTest backport harness. */ +@Target(AnnotationTarget.FUNCTION) +@Retention(AnnotationRetention.RUNTIME) +annotation class ClientGameTest(val name: String = "") + +data class TestScreenshotOptions( + val name: String, +) + +data class TestScreenshotComparisonOptions( + val templateImage: String, + val screenshot: TestScreenshotOptions = TestScreenshotOptions(name = "client-gametest"), +) + +data class AClientGameTestFailure( + val testId: String, + val rootCause: String, +) + +/** + * Synthetic input dispatch for a client GameTest's active [Screen], bypassing GLFW entirely + * (each call feeds the corresponding `mouseClicked`/`keyPressed`/... callback directly). Reached + * via [ClientGameTestContext.getInput] - most tests should prefer the higher-level, node-scoped + * [TestNodeScope] helpers (`click()`, `hover()`, `type()`, `scroll()`) instead of calling this + * directly, since those also resolve the target node's on-screen position first. + */ +interface TestInput { + fun click(x: Double, y: Double, button: Int = 0) + fun keyPress(keyCode: Int, scanCode: Int = 0, modifiers: Int = 0) + fun holdKey(keyCode: Int, scanCode: Int = 0, modifiers: Int = 0) + fun releaseKey(keyCode: Int, scanCode: Int = 0, modifiers: Int = 0) + fun pressKey(keyCode: Int, scanCode: Int = 0, modifiers: Int = 0) + fun holdMouse(button: Int = 0) + fun releaseMouse(button: Int = 0) + fun pressMouse(button: Int = 0) + fun holdControl() + fun releaseControl() + fun holdShift() + fun releaseShift() + fun holdAlt() + fun releaseAlt() + fun charTyped(char: Char, modifiers: Int = 0) + fun typeChars(value: String) + fun scroll(x: Double = 0.0, y: Double = 1.0) + fun setCursor(x: Double, y: Double) + fun moveCursor(deltaX: Double, deltaY: Double) + fun clearInputs() +} + +/** + * Builds a test world from [ClientGameTestContext.worldBuilder]: either a singleplayer world via + * [create]/[withSingleplayer], or a same-JVM dedicated server via [createServer]/[withServer] for + * tests that need a real client↔server boundary (e.g. exercising [ConfigSpec.Server] sync or + * other multiplayer-only codepaths) rather than the integrated server a singleplayer world uses. + */ +@Suppress("unused") +interface TestWorldBuilder { + fun setUseConsistentSettings(useConsistentSettings: Boolean): TestWorldBuilder + + fun adjustSettings(settingsAdjuster: Consumer): TestWorldBuilder + + fun create(): TestSingleplayerContext + + fun withSingleplayer(callback: TestSingleplayerContext.() -> Unit) { + val context = create() + try { + context.callback() + } finally { + context.close() + } + } + + fun createServer(serverProperties: Properties): TestDedicatedServerContext + + fun withServer(serverProperties: Properties, callback: TestDedicatedServerContext.() -> Unit) { + val context = createServer(serverProperties) + try { + context.callback() + } finally { + context.close() + } + } +} + +/** A running singleplayer test world created by [TestWorldBuilder.create], with server-side access via [server]. Closing (see [TestWorldBuilder.withSingleplayer]) disconnects and waits for the world to unload. */ +@Suppress("unused") +interface TestSingleplayerContext { + val clientContext: ClientGameTestContext + val saveDirectory: Path + val clientWorld: TestClientWorldContext + val server: TestServerContext + + fun close() +} + +/** A running same-JVM dedicated server created by [TestWorldBuilder.createServer]. [connect] joins the client to it; closing (see [TestWorldBuilder.withServer]) stops the server process. */ +@Suppress("unused") +interface TestDedicatedServerContext { + val clientContext: ClientGameTestContext + + val serverDirectory: Path + + fun connect(): TestServerConnection + + fun withConnection(callback: TestServerConnection.() -> Unit) { + val connection = connect() + try { + connection.callback() + } finally { + runCatching { connection.disconnect() } + .onFailure { error -> + Archie.LOGGER.warn("Failed to disconnect server connection cleanly: ${error.message}") + } + } + } + + fun close() +} + +/** The client's connection to a [TestDedicatedServerContext], returned by [TestDedicatedServerContext.connect]. */ +@Suppress("unused") +interface TestServerConnection { + val clientContext: ClientGameTestContext + val clientWorld: TestClientWorldContext + + fun disconnect() +} + +/** Client-side world-loading waits (chunk download/render), independent of world type - available on both [TestSingleplayerContext.clientWorld] and [TestServerConnection.clientWorld]. */ +@Suppress("unused") +interface TestClientWorldContext { + fun waitForChunksDownload(timeout: Int = ClientGameTestContext.DEFAULT_TIMEOUT): Int + + fun waitForChunksRender(waitForDownload: Boolean = true, timeout: Int = ClientGameTestContext.DEFAULT_TIMEOUT): Int +} + +/** Server-side access for a [TestSingleplayerContext]'s integrated server: run commands or arbitrary code on the server thread. */ +@Suppress("unused") +interface TestServerContext { + fun runCommand(command: String) + + fun runOnServer(action: FailableConsumer) + + fun computeOnServer(function: FailableFunction): T + + fun runOnServer(action: (MinecraftServer) -> Unit) + + fun computeOnServer(function: (MinecraftServer) -> T): T +} + +/** Minimal assertion/report API passed to client harness test methods. */ +@Suppress("unused") +interface ClientGameTestContext { + val testId: String + + companion object { + const val NO_TIMEOUT: Int = -1 + const val DEFAULT_TIMEOUT: Int = 10 * SharedConstants.TICKS_PER_SECOND + } + + fun assertTrue(condition: Boolean, message: () -> String) + + fun assertEquals(expected: Any?, actual: Any?, message: () -> String = { "Expected <$expected>, got <$actual>" }) + + fun fail(message: String): Nothing + + fun assertScreenshotContains(templateImage: String): Vector2i = + assertScreenshotContains(TestScreenshotComparisonOptions(templateImage = templateImage)) + + fun assertScreenshotContains(options: TestScreenshotComparisonOptions): Vector2i + + fun assertScreenshotEquals(templateImage: String) = + assertScreenshotEquals(TestScreenshotComparisonOptions(templateImage = templateImage)) + + fun assertScreenshotEquals(options: TestScreenshotComparisonOptions) + + fun clickScreenButton(translationKey: String) + + fun computeOnClient(function: (Minecraft) -> T): T + + fun computeOnClient(function: FailableFunction): T + + fun getInput(): TestInput + + fun restoreDefaultGameOptions() + + fun runOnClient(action: (Minecraft) -> Unit) + + fun runOnClient(action: FailableConsumer) + + fun setScreen(screen: Supplier) + + fun takeScreenshot(name: String): Path = takeScreenshot(TestScreenshotOptions(name)) + + fun takeScreenshot(options: TestScreenshotOptions): Path + + fun tryClickScreenButton(translationKey: String): Boolean + + fun waitFor(predicate: Predicate): Int = + waitFor(predicate, DEFAULT_TIMEOUT) + + fun waitFor(predicate: Predicate, timeout: Int): Int + + fun waitForScreen(screenClass: Class?): Int + fun waitForScreen(screenClass: Class, block: ComposeScreenTestContext.() -> Unit): Int + + fun waitTick() + + fun waitTicks(ticks: Int) + + /** + * Waits for asynchronous Compose recomposition triggered by a prior state mutation (e.g. a + * click, hover, keypress, or typed character) to settle, so a following assertion doesn't + * race the still-in-flight visual/layout update. No-ops for screens that aren't + * Compose-driven. Already used internally by [takeScreenshot]; call this explicitly after + * [TestInput]/[TestNodeScope] actions that aren't immediately followed by a screenshot. + */ + fun waitForComposeIdle() + + fun worldBuilder(): TestWorldBuilder + + fun withWorld(callback: TestWorldBuilder.() -> Unit) { + worldBuilder().apply(callback) + } +} + +internal class DefaultClientGameTestContext( + override val testId: String, +) : ClientGameTestContext { + companion object { + private val optionSnapshotLock = Any() + + @Volatile + private var defaultOptionValues: Map? = null + + @Volatile + private var deterministicOptionsInitialized: Boolean = false + } + + internal fun describeScreen(screen: Screen?): String = + screen?.let { "${it::class.java.name}@${System.identityHashCode(it)}" } ?: "null" + + private fun timeoutMessage(action: String, timeoutSeconds: Long, client: Minecraft): String { + val caller = Thread.currentThread() + return "Timed out waiting for client thread execution " + + "(testId=$testId, action=$action, timeoutSeconds=$timeoutSeconds, " + + "callerThread=${caller.name}, callerState=${caller.state}, " + + "clientScreen=${describeScreen(client.screen)}, levelLoaded=${client.level != null})" + } + + private val input: TestInput = object : TestInput { + private val keysDown = mutableSetOf>() + private val mouseButtonsDown = mutableSetOf() + private var cursorX: Double? = null + private var cursorY: Double? = null + + private fun currentCursorX(screen: Screen): Double = cursorX ?: (screen.width / 2.0) + + private fun currentCursorY(screen: Screen): Double = cursorY ?: (screen.height / 2.0) + + override fun click(x: Double, y: Double, button: Int) { + runOnClient { client -> + val screen = client.screen ?: return@runOnClient + cursorX = x + cursorY = y + warpRealCursor(client, x, y) + screen.mouseClicked(x, y, button) + screen.mouseReleased(x, y, button) + } + } + + override fun keyPress(keyCode: Int, scanCode: Int, modifiers: Int) { + pressKey(keyCode, scanCode, modifiers) + } + + override fun holdKey(keyCode: Int, scanCode: Int, modifiers: Int) { + val key = keyCode to scanCode + if (!keysDown.add(key)) return + runOnClient { client -> + client.screen?.keyPressed(keyCode, scanCode, modifiers) + } + } + + override fun releaseKey(keyCode: Int, scanCode: Int, modifiers: Int) { + val key = keyCode to scanCode + if (!keysDown.remove(key)) return + runOnClient { client -> + client.screen?.keyReleased(keyCode, scanCode, modifiers) + } + } + + override fun pressKey(keyCode: Int, scanCode: Int, modifiers: Int) { + holdKey(keyCode, scanCode, modifiers) + waitTick() + releaseKey(keyCode, scanCode, modifiers) + } + + override fun holdMouse(button: Int) { + if (!mouseButtonsDown.add(button)) return + runOnClient { client -> + val screen = client.screen ?: return@runOnClient + val x = currentCursorX(screen) + val y = currentCursorY(screen) + screen.mouseClicked(x, y, button) + } + } + + override fun releaseMouse(button: Int) { + if (!mouseButtonsDown.remove(button)) return + runOnClient { client -> + val screen = client.screen ?: return@runOnClient + val x = currentCursorX(screen) + val y = currentCursorY(screen) + screen.mouseReleased(x, y, button) + } + } + + override fun pressMouse(button: Int) { + holdMouse(button) + waitTick() + releaseMouse(button) + } + + override fun holdControl() { + holdKey(GLFW.GLFW_KEY_LEFT_CONTROL) + } + + override fun releaseControl() { + releaseKey(GLFW.GLFW_KEY_LEFT_CONTROL) + } + + override fun holdShift() { + holdKey(GLFW.GLFW_KEY_LEFT_SHIFT) + } + + override fun releaseShift() { + releaseKey(GLFW.GLFW_KEY_LEFT_SHIFT) + } + + override fun holdAlt() { + holdKey(GLFW.GLFW_KEY_LEFT_ALT) + } + + override fun releaseAlt() { + releaseKey(GLFW.GLFW_KEY_LEFT_ALT) + } + + override fun charTyped(char: Char, modifiers: Int) { + runOnClient { client -> + client.screen?.charTyped(char, modifiers) + } + } + + override fun typeChars(value: String) { + value.forEach { + charTyped(it) + waitForComposeIdle() + } + } + + override fun scroll(x: Double, y: Double) { + runOnClient { client -> + val screen = client.screen ?: return@runOnClient + val sx = currentCursorX(screen) + val sy = currentCursorY(screen) + screen.mouseScrolled(sx, sy, x, y) + } + } + + override fun setCursor(x: Double, y: Double) { + cursorX = x + cursorY = y + runOnClient { client -> + warpRealCursor(client, x, y) + client.screen?.mouseMoved(x, y) + } + } + + override fun moveCursor(deltaX: Double, deltaY: Double) { + runOnClient { client -> + val screen = client.screen ?: return@runOnClient + val nextX = currentCursorX(screen) + deltaX + val nextY = currentCursorY(screen) + deltaY + cursorX = nextX + cursorY = nextY + warpRealCursor(client, nextX, nextY) + screen.mouseMoved(nextX, nextY) + } + } + + override fun clearInputs() { + keysDown.toList().forEach { (keyCode, scanCode) -> + releaseKey(keyCode, scanCode) + } + mouseButtonsDown.toList().forEach { button -> + releaseMouse(button) + } + cursorX = null + cursorY = null + } + } + + override fun assertTrue(condition: Boolean, message: () -> String) { + if (!condition) fail(message()) + } + + override fun assertEquals(expected: Any?, actual: Any?, message: () -> String) { + if (expected != actual) fail(message()) + } + + override fun fail(message: String): Nothing = throw IllegalStateException(message) + + override fun assertScreenshotContains(options: TestScreenshotComparisonOptions): Vector2i { + val templatePath = ScreenshotManager.resolveTemplate(options.templateImage) + if (!templatePath.toFile().exists()) { + fail("Screenshot template not found: ${options.templateImage} (resolved to: $templatePath)") + } + + val capturePath = takeScreenshot(options.screenshot) + val matchPos = ScreenshotComparer.findInImage(templatePath.toFile(), capturePath.toFile()) + return matchPos ?: fail("Template image '${options.templateImage}' not found in screenshot") + } + + override fun assertScreenshotEquals(options: TestScreenshotComparisonOptions) { + val templatePath = ScreenshotManager.resolveTemplate(options.templateImage) + if (!templatePath.toFile().exists()) { + fail("Screenshot template not found: ${options.templateImage} (resolved to: $templatePath)") + } + + val capturePath = takeScreenshot(options.screenshot) + val equal = ScreenshotComparer.imagesEqual(templatePath.toFile(), capturePath.toFile()) + if (!equal) { + fail("Screenshot does not match template '${options.templateImage}' (expected: $templatePath, actual: $capturePath)") + } + } + + override fun clickScreenButton(translationKey: String) { + if (!tryClickScreenButton(translationKey)) { + fail("No screen button found for translation key '$translationKey'") + } + } + + override fun computeOnClient(function: (Minecraft) -> T): T { + return computeOnClient("anonymous-client-action", CLIENT_EXEC_TIMEOUT_SECONDS, function) + } + + internal fun computeOnClient(action: String, timeoutSeconds: Long, function: (Minecraft) -> T): T { + val client = Minecraft.getInstance() + return if (client.isSameThread) { + ensureDeterministicGameOptionsInitialized(client) + function(client) + } else { + var value: T? = null + var throwable: Throwable? = null + val latch = CountDownLatch(1) + client.execute { + runCatching { + ensureDeterministicGameOptionsInitialized(client) + function(client) + } + .onSuccess { value = it } + .onFailure { throwable = it } + latch.countDown() + } + + if (!latch.await(timeoutSeconds, TimeUnit.SECONDS)) { + fail(timeoutMessage(action, timeoutSeconds, client)) + } + + throwable?.let { throw it } + @Suppress("UNCHECKED_CAST") + value as T + } + } + + override fun computeOnClient(function: FailableFunction): T { + return computeOnClient("failable-client-function", CLIENT_EXEC_TIMEOUT_SECONDS) { client -> function.apply(client) } + } + + override fun getInput(): TestInput = input + + override fun restoreDefaultGameOptions() { + runOnClient { client -> + restoreCapturedGameOptions(client) + client.options.save() + } + } + + private fun initializeDeterministicGameOptions(client: Minecraft) { + val options = client.options + if (defaultOptionValues == null) { + synchronized(optionSnapshotLock) { + if (defaultOptionValues == null) { + defaultOptionValues = captureOptionValues(options) + } + } + } + + applyDeterministicTweaks(options) + disablePauseOnLostFocus(options) + options.save() + } + + /** + * `pauseOnLostFocus` is a raw boolean field on [net.minecraft.client.Options], not an + * `OptionInstance`/`SimpleOption` wrapper, so it's invisible to [applyDeterministicTweaks]'s + * [isOptionLike]-filtered reflection loop. Client GameTest windows routinely run without OS + * focus (headless CI, parallel loader:side invocations, a terminal/IDE stealing focus), and + * vanilla pauses world ticking whenever the window isn't focused - silently stalling every + * waitTick()/waitTicks() call - so it needs disabling separately. + */ + private fun disablePauseOnLostFocus(options: Any) { + runCatching { + options.setReflection("pauseOnLostFocus", false) + } + } + + private fun ensureDeterministicGameOptionsInitialized(client: Minecraft) { + if (deterministicOptionsInitialized) return + + synchronized(optionSnapshotLock) { + if (deterministicOptionsInitialized) return + initializeDeterministicGameOptions(client) + deterministicOptionsInitialized = true + } + } + + private fun restoreCapturedGameOptions(client: Minecraft) { + val options = client.options + val snapshot = defaultOptionValues ?: synchronized(optionSnapshotLock) { + defaultOptionValues ?: captureOptionValues(options).also { defaultOptionValues = it } + } + restoreOptionValues(options, snapshot) + } + + private fun captureOptionValues(options: Any): Map { + val captured = mutableMapOf() + for (field in options.javaClass.declaredFields) { + if (Modifier.isStatic(field.modifiers)) continue + field.isAccessible = true + val option = runCatching { field.get(options) }.getOrNull() ?: continue + if (!isOptionLike(option)) continue + captured[field.name] = readOptionValue(option) + } + return captured + } + + private fun restoreOptionValues(options: Any, snapshot: Map) { + for ((fieldName, value) in snapshot) { + val field = runCatching { options.javaClass.getDeclaredField(fieldName) }.getOrNull() ?: continue + field.isAccessible = true + val option = runCatching { field.get(options) }.getOrNull() ?: continue + if (!isOptionLike(option)) continue + writeOptionValue(option, value) + } + } + + private fun applyDeterministicTweaks(options: Any) { + for (field in options.javaClass.declaredFields) { + if (Modifier.isStatic(field.modifiers)) continue + field.isAccessible = true + val option = runCatching { field.get(options) }.getOrNull() ?: continue + if (!isOptionLike(option)) continue + + when { + field.name.contains("tutorial", ignoreCase = true) -> { + writeOptionEnumByName(option, "NONE") + } + field.name.contains("cloud", ignoreCase = true) -> { + writeOptionEnumByName(option, "OFF") + } + field.name.contains("renderDistance", ignoreCase = true) || field.name.contains("viewDistance", ignoreCase = true) -> { + writeOptionValue(option, 5) + } + field.name.contains("music", ignoreCase = true) -> { + writeOptionValue(option, 0.0) + } + } + } + } + + private fun isOptionLike(option: Any): Boolean { + val n = option.javaClass.simpleName + return n == "OptionInstance" || n == "SimpleOption" + } + + private fun readOptionValue(option: Any): Any? { + val getter = option.javaClass.methods.firstOrNull { + (it.name == "get" || it.name == "getValue") && it.parameterCount == 0 + } ?: return null + return runCatching { getter.invoke(option) }.getOrNull() + } + + private fun writeOptionValue(option: Any, value: Any?) { + val setter = option.javaClass.methods.firstOrNull { method -> + (method.name == "set" || method.name == "setValue") && method.parameterCount == 1 + } ?: return + + runCatching { + setter.invoke(option, value) + } + } + + private fun writeOptionEnumByName(option: Any, enumName: String) { + val current = readOptionValue(option) ?: return + if (!current.javaClass.isEnum) return + + val constant = current.javaClass.enumConstants + ?.firstOrNull { (it as? Enum<*>)?.name == enumName } ?: return + writeOptionValue(option, constant) + } + + override fun runOnClient(action: (Minecraft) -> Unit) { + computeOnClient("run-on-client", CLIENT_EXEC_TIMEOUT_SECONDS) { client -> + action(client) + } + } + + internal fun postToClient(action: (Minecraft) -> Unit) { + Minecraft.getInstance().execute { action(Minecraft.getInstance()) } + } + + override fun runOnClient(action: FailableConsumer) { + runOnClient { client -> action.accept(client) } + } + + override fun setScreen(screen: Supplier) { + var expected: Screen? = null + runOnClient { client -> + expected = screen.get() + client.setScreen(expected) + } + + runCatching { + waitFor( + { client -> + val current = client.screen + val target = expected + when { + target == null -> current == null + current === target -> true + current != null && current::class.java == target::class.java -> true + else -> false + } + }, + SCREEN_SET_TIMEOUT_TICKS, + ) + }.getOrElse { + fail( + "Screen transition did not complete in time " + + "(testId=$testId, expected=${describeScreen(expected)}, " + + "actual=${computeOnClient { describeScreen(it.screen) }}, cause=${it.message})" + ) + } + } + + /** + * Waits for asynchronous Compose recomposition (state write -> apply notification -> + * frame request -> recompose job -> next-frame join, see [ComposeIdleAware]) to settle + * before capturing a screenshot, so a `click()` (or similar) immediately followed by a + * screenshot assertion doesn't race the still-in-flight visual update. + * + * Requires two consecutive idle reads, since a single idle read can still land in the + * narrow window between a state mutation and the snapshot write observer's callback firing. + * Not a hard guarantee under extreme scheduler starvation, but turns an always-racy check + * into one that's reliable in practice. No-ops for screens that aren't Compose-driven. + */ + override fun waitForComposeIdle() { + var consecutiveIdle = 0 + repeat(COMPOSE_IDLE_TIMEOUT_TICKS) { + val idle = computeOnClient("compose-idle-check", CLIENT_EXEC_TIMEOUT_SECONDS) { client -> + (client.screen as? ComposeIdleAware)?.isComposeIdle() ?: true + } + if (idle) { + consecutiveIdle++ + if (consecutiveIdle >= COMPOSE_IDLE_CONSECUTIVE_CHECKS) return + } else { + consecutiveIdle = 0 + } + waitTick() + } + } + + override fun takeScreenshot(options: TestScreenshotOptions): Path { + waitForComposeIdle() + val capturePath = ScreenshotManager.generateCapturePath(testId, options.name) + computeOnClient("take-screenshot", CLIENT_EXEC_TIMEOUT_SECONDS) { client -> + try { + capturePath.parent?.let { Files.createDirectories(it) } + + // 1.21.1 API: capture from the main render target and write with NativeImage. + Screenshot.takeScreenshot(client.mainRenderTarget).use { screenshot -> + screenshot.writeToFile(capturePath) + } + } catch (e: Exception) { + fail("Failed to take screenshot: ${e.message}") + } + } + return capturePath + } + + override fun tryClickScreenButton(translationKey: String): Boolean { + return tryClickScreenButton(translationKey, CLIENT_EXEC_TIMEOUT_SECONDS) + } + + internal fun tryClickScreenButton(translationKey: String, timeoutSeconds: Long): Boolean { + return computeOnClient("try-click-screen-button", timeoutSeconds) { client: Minecraft -> + val screen = client.screen ?: return@computeOnClient false + val widget = screen.children() + .filterIsInstance() + .firstOrNull { + val contents = it.message.contents + contents is TranslatableContents && contents.key == translationKey + } ?: return@computeOnClient false + + val cx = widget.x + (widget.width / 2.0) + val cy = widget.y + (widget.height / 2.0) + screen.mouseClicked(cx, cy, 0) + screen.mouseReleased(cx, cy, 0) + true + } + } + + override fun waitFor(predicate: Predicate, timeout: Int): Int { + if (timeout == ClientGameTestContext.NO_TIMEOUT) { + var ticksWaited = 0 + while (!computeOnClient("wait-for", CLIENT_EXEC_TIMEOUT_SECONDS) { client: Minecraft -> predicate.test(client) }) { + ticksWaited++ + waitTick() + } + return ticksWaited + } + + require(timeout > 0) { "timeout must be positive or NO_TIMEOUT" } + for (tick in 0 until timeout) { + val ready = computeOnClient("wait-for", CLIENT_EXEC_TIMEOUT_SECONDS) { client: Minecraft -> predicate.test(client) } + if (ready) return tick + waitTick() + } + + if (!computeOnClient("wait-for-final-check", CLIENT_EXEC_TIMEOUT_SECONDS) { client: Minecraft -> predicate.test(client) }) { + fail("Predicate did not become true within $timeout ticks") + } + + return timeout + } + + override fun waitForScreen(screenClass: Class?): Int + { + return waitFor { client -> + val current = client.screen + if (screenClass == null) current == null else current != null && screenClass.isInstance(current) + } + } + + override fun waitForScreen( + screenClass: Class, + block: ComposeScreenTestContext.() -> Unit + ): Int + { + return waitForScreen(screenClass).also { + computeOnClient { screenClass.cast(it.screen) }.also { + ComposeScreenTestContext(this, it).block() + } + } + } + + override fun waitTick() { + waitTicks(1) + } + + override fun waitTicks(ticks: Int) { + val remainingTicks = ticks.coerceAtLeast(0) + if (remainingTicks == 0) return + + val timeoutMillis = (remainingTicks.toLong() * DEFAULT_TICK_MILLIS * 20L).coerceAtLeast(DEFAULT_TICK_MILLIS) + if (!ThreadingImpl.awaitTicks(remainingTicks, timeoutMillis)) { + fail("Timed out waiting for $remainingTicks client tick(s)") + } + } + + override fun worldBuilder(): TestWorldBuilder = DefaultTestWorldBuilder(this) +} + +private class DefaultTestWorldBuilder( + private val context: DefaultClientGameTestContext, +) : TestWorldBuilder { + private var useConsistentSettings = true + private var settingsAdjustor: Consumer = Consumer { } + + override fun setUseConsistentSettings(useConsistentSettings: Boolean): TestWorldBuilder = apply { + this.useConsistentSettings = useConsistentSettings + } + + override fun adjustSettings(settingsAdjuster: Consumer): TestWorldBuilder = apply { + this.settingsAdjustor = settingsAdjuster + } + + override fun create(): TestSingleplayerContext { + val saveDirectory = context.computeOnClient("world-builder-open-create-screen", WORLD_BUILDER_EXEC_TIMEOUT_SECONDS) { client -> + val oldScreen = client.screen + CreateWorldScreen.openFresh(client, oldScreen) + + val createWorldScreen = client.screen as? CreateWorldScreen + ?: context.fail("CreateWorldScreen.openFresh did not open a world-creation screen") + + val creator = createWorldScreen.uiState + + if (useConsistentSettings) { + setConsistentSettings(creator) + } + + settingsAdjustor.accept(creator) + + client.levelSource.baseDir.resolve(creator.targetFolder) + } + + context.postToClient { client -> + val screen = client.screen ?: return@postToClient + val widget = screen.children() + .filterIsInstance() + .firstOrNull { + val contents = it.message.contents + contents is TranslatableContents && contents.key == "selectWorld.create" + } ?: return@postToClient + + val cx = widget.x + (widget.width / 2.0) + val cy = widget.y + (widget.height / 2.0) + screen.mouseClicked(cx, cy, 0) + screen.mouseReleased(cx, cy, 0) + } + + // World creation transitions can momentarily starve the harness tick gate. + // Treat this first post-click tick as best-effort and rely on world-load checks below. + runCatching { context.waitTick() } + waitForWorldLoad() + + return DefaultTestSingleplayerContext( + clientContext = context, + saveDirectory = saveDirectory, + ) + } + + override fun createServer(serverProperties: Properties): TestDedicatedServerContext { + return DefaultServerWorldBuilder(context).create(serverProperties) + } + + private fun waitForWorldLoad() { + val worldLoadTimeoutTicks = (WORLD_BUILDER_EXEC_TIMEOUT_SECONDS * 1000 / DEFAULT_TICK_MILLIS).toInt() + runCatching { + context.waitTick() + context.waitFor( + { client -> client.level != null || client.singleplayerServer != null }, + worldLoadTimeoutTicks, + ) + }.getOrElse { + val client = Minecraft.getInstance() + Archie.LOGGER.warn( + "Timed out waiting for world load start (testId=${context.testId}, timeoutSeconds=$WORLD_BUILDER_EXEC_TIMEOUT_SECONDS, screen=${context.describeScreen(client.screen)}, levelLoaded=${client.level != null}, serverStarted=${client.singleplayerServer != null})" + ) + } + } + + private fun setConsistentSettings(creator: WorldCreationUiState) { + val flatPreset: Holder = creator.settings + .worldgenLoadContext() + .lookupOrThrow(Registries.WORLD_PRESET) + .getOrThrow(WorldPresets.FLAT) + + creator.worldType = WorldCreationUiState.WorldTypeEntry(flatPreset) + creator.seed = "1" + } +} + +private class DefaultTestSingleplayerContext( + override val clientContext: ClientGameTestContext, + override val saveDirectory: Path, +) : TestSingleplayerContext { + override val clientWorld: TestClientWorldContext = DefaultTestClientWorldContext(clientContext) + override val server: TestServerContext = DefaultTestServerContext(clientContext) + + override fun close() { + ThreadingImpl.checkOnGametestThread("close") + + clientContext.runOnClient { client -> + val hasLevel = client.level != null + val hasLocalServer = client.singleplayerServer != null || client.isLocalServer + if (!hasLevel && !hasLocalServer) return@runOnClient + + client.level?.disconnect() + if (hasLocalServer) { + client.disconnect(GenericMessageScreen(Component.translatable("menu.savingLevel"))) + } else { + client.disconnect() + } + } + + runCatching { + clientContext.waitFor( + { client -> client.level == null && client.singleplayerServer == null }, + SharedConstants.TICKS_PER_MINUTE, + ) + }.getOrElse { error -> + Archie.LOGGER.warn( + "Singleplayer world did not close cleanly (testId=${clientContext.testId}, saveDir=$saveDirectory, cause=${error.message})" + ) + } + + // Final recovery pass: always try to land on title and clear any lingering local session state. + clientContext.runOnClient { client -> + if (client.level != null || client.singleplayerServer != null || client.isLocalServer) { + client.level?.disconnect() + if (client.singleplayerServer != null || client.isLocalServer) { + client.disconnect(GenericMessageScreen(Component.translatable("menu.savingLevel"))) + } else { + client.disconnect() + } + } + if (client.screen !is TitleScreen) { + client.setScreen(TitleScreen()) + } + } + + runCatching { + clientContext.waitFor( + { client -> client.level == null && client.singleplayerServer == null }, + SharedConstants.TICKS_PER_SECOND * 10, + ) + }.getOrElse { error -> + Archie.LOGGER.warn( + "Singleplayer session still present after forced close (testId=${clientContext.testId}, saveDir=$saveDirectory, cause=${error.message})" + ) + } + } +} + +private class DefaultTestServerContext( + private val clientContext: ClientGameTestContext, +) : TestServerContext { + override fun runCommand(command: String) { + ThreadingImpl.checkOnGametestThread("runCommand") + require(command.isNotBlank()) { "command cannot be blank" } + + runOnServer(FailableConsumer { server -> + runCommandReflective(server, command) + }) + } + + override fun runOnServer(action: FailableConsumer) { + ThreadingImpl.checkOnGametestThread("runOnServer") + val server = requireSingleplayerServer() + ThreadingImpl.runOnServer { + action.accept(server) + } + } + + override fun computeOnServer(function: FailableFunction): T { + ThreadingImpl.checkOnGametestThread("computeOnServer") + val server = requireSingleplayerServer() + var result: T? = null + ThreadingImpl.runOnServer { + result = function.apply(server) + } + + @Suppress("UNCHECKED_CAST") + return result as T + } + + override fun runOnServer(action: (MinecraftServer) -> Unit) = runOnServer(FailableConsumer(action)) + + override fun computeOnServer(function: (MinecraftServer) -> T): T = computeOnServer(FailableFunction(function)) + + private fun requireSingleplayerServer(): MinecraftServer { + return clientContext.computeOnClient { client -> + client.singleplayerServer ?: throw IllegalStateException("No integrated server is running") + } + } + + private fun runCommandReflective(server: MinecraftServer, command: String) { + val source = runCatching { + server.javaClass.methods.firstOrNull { it.name == "createCommandSourceStack" && it.parameterCount == 0 } + ?.invoke(server) + }.getOrNull() + + val commandsObj = runCatching { + server.javaClass.methods.firstOrNull { it.name == "getCommands" && it.parameterCount == 0 }?.invoke(server) + ?: server.javaClass.methods.firstOrNull { it.name == "getCommandManager" && it.parameterCount == 0 }?.invoke(server) + }.getOrNull() ?: throw IllegalStateException("Could not resolve command manager from server") + + val executeMethod = commandsObj.javaClass.methods.firstOrNull { method -> + method.parameterCount == 2 && + (method.name == "performPrefixedCommand" || method.name == "executeWithPrefix" || method.name == "performCommand") + } ?: throw IllegalStateException("Could not find command execution method on ${commandsObj.javaClass.name}") + + executeMethod.invoke(commandsObj, source, command) + } +} + +private class DefaultServerWorldBuilder( + private val context: DefaultClientGameTestContext, +) { + fun create(serverProperties: Properties): TestDedicatedServerContext { + val serverStartTimeout = WORLD_BUILDER_EXEC_TIMEOUT_SECONDS + + lateinit var serverInstance: Any + lateinit var serverDirectory: Path + + try { + // Prepare server directory on client thread + context.computeOnClient("setup-dedicated-server-dir", serverStartTimeout) { mc -> + try { + val gameDir = mc.gameDirectory.toPath() + val dir = gameDir.resolve("test_server_${System.nanoTime()}") + Files.createDirectories(dir) + serverDirectory = dir + + // Apply server properties + writeServerBootstrapFiles(dir, serverProperties) + } catch (e: Exception) { + context.fail("Failed to set up server directory: ${e.message}") + } + } + + // Start server asynchronously (blocks gametest thread, not client thread) + try { + serverInstance = ADedicatedServerPlatform.start( + serverDirectory, + serverProperties, + serverStartTimeout + ) + DedicatedServerLifecycleTracker.register(serverInstance) + } catch (e: Exception) { + context.fail("Failed to start dedicated server: ${e.message}") + } + + return DefaultTestDedicatedServerContext( + clientContext = context, + serverInstance = serverInstance, + serverDirectory = serverDirectory, + ) + } catch (e: Exception) { + context.fail("Error creating dedicated server: ${e.message}") + } + } + + private fun writeServerBootstrapFiles(serverDirectory: Path, serverProperties: Properties) { + val merged = Properties() + merged.putAll(serverProperties) + merged.putIfAbsent("online-mode", "false") + merged.putIfAbsent("spawn-protection", "0") + merged.putIfAbsent("max-players", "1") + // This dedicated server shares the JVM with the client under test (no subprocess + // isolation). ServerWatchdog calls System.exit(1) if a single tick exceeds + // max-tick-time, which would kill the whole test JVM - disable it by default. + merged.putIfAbsent("max-tick-time", "0") + + // The dedicated-server launcher reads these files from the process working directory, + // while the harness also keeps an isolated copy under the generated per-test server dir. + writeBootstrapFiles(serverDirectory, merged) + CwdBootstrapFileGuard.writeOverride(merged) + } + + private fun writeBootstrapFiles(targetDirectory: Path, properties: Properties) { + writeTextFile(targetDirectory, "server.properties") { writer -> + properties.store(writer, "Archie GameTest dedicated server properties") + } + writeTextFile(targetDirectory, "eula.txt") { writer -> + writer.write("eula=true") + writer.newLine() + } + } + + private fun writeTextFile(targetDirectory: Path, fileName: String, writerAction: (java.io.BufferedWriter) -> Unit) { + Files.newBufferedWriter(targetDirectory.resolve(fileName)).use(writerAction) + } +} + +private data class DefaultTestDedicatedServerContext( + override val clientContext: ClientGameTestContext, + val serverInstance: Any, + override val serverDirectory: Path, +) : TestDedicatedServerContext { + override fun connect(): TestServerConnection { + ThreadingImpl.checkOnGametestThread("connect") + + val port = ADedicatedServerPlatform.port(serverInstance) + clientContext.runOnClient { client -> + connectToLocalhost(client, port) + } + + clientContext.waitFor( + { client -> client.level != null }, + ClientGameTestContext.DEFAULT_TIMEOUT, + ) + + return DefaultTestServerConnection( + clientContext = clientContext, + clientWorld = DefaultTestClientWorldContext(clientContext), + ) + } + + override fun close() { + ThreadingImpl.checkOnGametestThread("close") + + try { + val stopRequested = requestStopWithTimeout(timeoutMillis = TimeUnit.SECONDS.toMillis(10)) + if (!stopRequested) { + Archie.LOGGER.warn( + "Timed out requesting dedicated server stop; forcing halt (testId=${clientContext.testId}, serverDir=$serverDirectory)" + ) + forceHaltServer() + } + + runCatching { + clientContext.waitFor( + { _ -> !ADedicatedServerPlatform.isAlive(serverInstance) }, + SharedConstants.TICKS_PER_MINUTE, + ) + }.getOrElse { error -> + Archie.LOGGER.warn( + "Dedicated server did not stop cleanly (testId=${clientContext.testId}, serverDir=$serverDirectory, alive=${ADedicatedServerPlatform.isAlive(serverInstance)}, cause=${error.message})" + ) + } + } finally { + DedicatedServerLifecycleTracker.unregister(serverInstance) + } + } + + private fun requestStopWithTimeout(timeoutMillis: Long): Boolean { + var stopFailure: Throwable? = null + val stopThread = Thread({ + runCatching { + // Stopping from outside the server tick thread avoids self-stop deadlocks. + ADedicatedServerPlatform.stop(serverInstance) + }.recoverCatching { + ThreadingImpl.runOnServer { + ADedicatedServerPlatform.stop(serverInstance) + } + }.onFailure { + stopFailure = it + } + }, "Archie Dedicated GameTest Server Stop") + + stopThread.isDaemon = true + stopThread.start() + stopThread.join(timeoutMillis) + + stopFailure?.let { + throw IllegalStateException("Failed to stop dedicated server context", it) + } + + return !stopThread.isAlive + } + + private fun forceHaltServer() { + runCatching { + val haltMethod = serverInstance.javaClass.methods.firstOrNull { method -> + (method.name == "stopServer") && method.parameterCount <= 1 + } ?: return + + when (haltMethod.parameterCount) { + 0 -> haltMethod.invoke(serverInstance) + 1 -> { + val paramType = haltMethod.parameterTypes[0] + when (paramType) { + Boolean::class.javaPrimitiveType, Boolean::class.java -> haltMethod.invoke(serverInstance, true) + else -> haltMethod.invoke(serverInstance, null) + } + } + } + } + } + + private fun connectToLocalhost(client: Minecraft, port: Int) { + val connectScreenClass = runCatching { + Class.forName("net.minecraft.client.gui.screens.ConnectScreen") + }.getOrElse { + throw IllegalStateException("ConnectScreen class not found") + } + + val addressClass = runCatching { + Class.forName("net.minecraft.client.multiplayer.resolver.ServerAddress") + }.getOrNull() ?: runCatching { + Class.forName("net.minecraft.client.multiplayer.ServerAddress") + }.getOrElse { + throw IllegalStateException("ServerAddress class not found") + } + + val serverDataClass = runCatching { + Class.forName("net.minecraft.client.multiplayer.ServerData") + }.getOrElse { + throw IllegalStateException("ServerData class not found") + } + + val parseMethod = addressClass.methods.firstOrNull { + it.name == "parseString" && it.parameterCount == 1 + } ?: addressClass.methods.firstOrNull { + it.name == "parse" && it.parameterCount == 1 + } ?: throw IllegalStateException("Could not find ServerAddress parse method") + + val address = parseMethod.invoke(null, "localhost:$port") + + val serverTypeClass = serverDataClass.declaredClasses.firstOrNull { + it.simpleName == "Type" && it.isEnum + } + val serverTypeValue = serverTypeClass?.enumConstants?.firstOrNull() + val serverData = serverDataClass.constructors.firstOrNull { it.parameterCount >= 3 } + ?.newInstance("localhost", "localhost:$port", serverTypeValue) + + val connectMethod = connectScreenClass.methods.firstOrNull { + it.name == "startConnecting" || it.name == "connect" + } ?: throw IllegalStateException("Could not find ConnectScreen connect method") + + val args = connectMethod.parameterTypes.map { param -> + when { + Screen::class.java.isAssignableFrom(param) -> client.screen + Minecraft::class.java.isAssignableFrom(param) -> client + param.isAssignableFrom(addressClass) -> address + serverData != null && param.isAssignableFrom(serverDataClass) -> serverData + param == Boolean::class.javaPrimitiveType || param == Boolean::class.java -> false + else -> null + } + }.toTypedArray() + + connectMethod.invoke(null, *args) + } + +} + +private data class DefaultTestServerConnection( + override val clientContext: ClientGameTestContext, + override val clientWorld: TestClientWorldContext, +) : TestServerConnection { + override fun disconnect() { + ThreadingImpl.checkOnGametestThread("close") + + clientContext.runOnClient { client -> + if (client.level == null) { + if (client.screen !is TitleScreen) { + client.setScreen(TitleScreen()) + } + return@runOnClient + } + + client.level?.disconnect() + client.disconnect() + } + + runCatching { + clientContext.waitFor({ client -> client.level == null }, ClientGameTestContext.DEFAULT_TIMEOUT) + }.getOrElse { error -> + Archie.LOGGER.warn( + "Timed out waiting for dedicated-server client disconnect (testId=${clientContext.testId}, cause=${error.message})" + ) + } + clientContext.setScreen(Supplier { TitleScreen() }) + } +} + +private class DefaultTestClientWorldContext( + private val clientContext: ClientGameTestContext, +) : TestClientWorldContext { + override fun waitForChunksDownload(timeout: Int): Int { + ThreadingImpl.checkOnGametestThread("waitForChunksDownload") + return clientContext.waitFor({ client -> areChunksLoaded(client) }, timeout) + } + + override fun waitForChunksRender(waitForDownload: Boolean, timeout: Int): Int { + ThreadingImpl.checkOnGametestThread("waitForChunksRender") + return clientContext.waitFor( + { client -> + (!waitForDownload || areChunksLoaded(client)) && areChunksRendered(client) + }, + timeout, + ) + } + + private fun areChunksLoaded(client: Minecraft): Boolean { + val level = client.level ?: return false + val player = client.player ?: return false + + val viewDistance = resolveClientViewDistance(client).coerceAtLeast(2) + val centerChunkX = player.blockX shr 4 + val centerChunkZ = player.blockZ shr 4 + val chunkSource = level.chunkSource + + val hasChunkMethod = chunkSource.javaClass.methods.firstOrNull { + (it.name == "hasChunk" || it.name == "isChunkLoaded") && + it.parameterCount == 2 && + it.parameterTypes[0] == Int::class.javaPrimitiveType && + it.parameterTypes[1] == Int::class.javaPrimitiveType + } + + if (hasChunkMethod != null) { + for (dz in -viewDistance..viewDistance) { + for (dx in -viewDistance..viewDistance) { + val loaded = runCatching { + hasChunkMethod.invoke(chunkSource, centerChunkX + dx, centerChunkZ + dz) as? Boolean + }.getOrNull() ?: false + if (!loaded) return false + } + } + return true + } + + // Fallback when chunk-source internals differ across mappings. + return true + } + + private fun areChunksRendered(client: Minecraft): Boolean { + val levelRenderer = client.levelRenderer ?: return false + val renderCompleteMethod = levelRenderer.javaClass.methods.firstOrNull { + (it.name == "isTerrainRenderComplete" || it.name == "isRenderComplete") && + it.parameterCount == 0 + } + + if (renderCompleteMethod != null) { + return runCatching { + renderCompleteMethod.invoke(levelRenderer) as? Boolean + }.getOrNull() ?: false + } + + return true + } + + private fun resolveClientViewDistance(client: Minecraft): Int { + val options = client.options + val method = options.javaClass.methods.firstOrNull { + (it.name == "getEffectiveRenderDistance" || it.name == "getClampedViewDistance") && it.parameterCount == 0 + } + + return runCatching { + (method?.invoke(options) as? Int) ?: 5 + }.getOrDefault(5) + } +} + +/** Aggregate result of an [AClientGameTestHarness.run] invocation. */ +data class AClientGameTestSummary( + val passed: Int, + val failed: Int, + val skipped: Int, + val failedTests: List = emptyList(), + val failedDetails: List = emptyList(), +) + +/** + * Runs every [ClientGameTest]-annotated method across [modToClasses] (as collected by + * [AGameTestPlatform.register] via [AGameTestEventObject]/`AGametestEvents.ArchieGameTestBuilder`'s + * `client { }` block) sequentially on the client thread, then returns to the title screen. + * No-ops (returning an all-zero summary) unless [side] is [AGameTestSide.CLIENT]. + */ +object AClientGameTestHarness { + @OptIn(ExperimentalCoroutinesApi::class) + fun run(modToClasses: Map>>, side: AGameTestSide?): AClientGameTestSummary { + if (side != AGameTestSide.CLIENT) return AClientGameTestSummary(passed = 0, failed = 0, skipped = 0) + + val selectedMods = selectModsToRun(modToClasses) + + var passed = 0 + var failed = 0 + var skipped = 0 + val failedTests = mutableListOf() + val failedDetails = mutableListOf() + + selectedMods.forEach { (mod, classes) -> + classes.forEach { clazz -> + val methods = clazz.declaredMethods.filter { + it.isAnnotationPresent(ClientGameTest::class.java) + } + + if (methods.isEmpty()) return@forEach + + val instance = clazz.kotlin.objectInstance ?: clazz.kotlin.primaryConstructor?.call() + + methods.forEach { method -> + val clientTest = method.getAnnotation(ClientGameTest::class.java) + val explicitName = clientTest?.name?.takeIf { it.isNotBlank() } + val testId = explicitName ?: "${mod.modId}:${clazz.simpleName.lowercase()}.${method.name.lowercase()}" + val context = DefaultClientGameTestContext(testId) + + val params = method.parameterTypes + val supported = when { + params.isEmpty() -> true + params.size == 1 && ClientGameTestContext::class.java.isAssignableFrom(params[0]) -> true + else -> false + } + + if (!supported) { + skipped++ + Archie.LOGGER.warn("[ClientGameTest] Skipping {} (unsupported signature: {} params)", testId, params.size) + return@forEach + } + + // Give every ComposeScreen this test opens a virtual clock/dispatcher (see + // ComposeTestClockOverride's KDoc) instead of real threads/wall-clock time - + // installed here (not the render thread) since setScreen{} dispatches actual + // screen construction there, but reads the override via a plain shared + // static, not a ThreadLocal. Always cleared, even on failure, so it can never + // leak into the next test or (in principle) the running game. + // + // advanceTimeBy(bounded), NOT advanceUntilIdle() - composables can run + // legitimately infinite delay() loops (e.g. TextFieldCore's blinking-cursor + // LaunchedEffect), and advanceUntilIdle() only returns once truly nothing is + // scheduled, which for an unboundedly-recurring loop is never: it hangs the + // render thread forever the first time such a composable is on screen. A fixed + // per-pump increment (~1 tick) makes every delay() progress a little on every + // real frame instead, bounded and hang-proof either way. + val scheduler = TestCoroutineScheduler() + ComposeTestClockOverride.dispatcher = StandardTestDispatcher(scheduler) + ComposeTestClockOverride.pump = { scheduler.advanceTimeBy(50) } + try { + runCatching { + context.getInput().clearInputs() + method.isAccessible = true + if (params.isEmpty()) method.invoke(instance) + else method.invoke(instance, context) + }.onSuccess { + context.getInput().clearInputs() + passed++ + Archie.LOGGER.info("[ClientGameTest] PASS {}", testId) + }.onFailure { error -> + runCatching { context.getInput().clearInputs() } + failed++ + failedTests += testId + failedDetails += AClientGameTestFailure(testId, rootCauseSummary(error)) + Archie.LOGGER.error("[ClientGameTest] FAIL {}", testId, error) + } + } finally { + ComposeTestClockOverride.dispatcher = null + ComposeTestClockOverride.pump = null + } + } + } + } + + Archie.LOGGER.info("[ClientGameTest] Completed: passed={}, failed={}, skipped={}", passed, failed, skipped) + // Safety net: if a test aborted before context.close(), force-stop leaked dedicated servers + // before the client begins shutdown to avoid dedicated tick crashes against torn-down GLFW. + DedicatedServerLifecycleTracker.stopAllLeakedServers() + // Always runs, even if some test leaked its server context - see CwdBootstrapFileGuard. + CwdBootstrapFileGuard.restoreIfCaptured() + + val minecraft = Minecraft.getInstance() + minecraft.execute { + val flag = minecraft.isLocalServer + val serverdata = minecraft.currentServer + minecraft.level?.disconnect() + if (flag) { + minecraft.disconnect(GenericMessageScreen(Component.translatable("menu.savingLevel"))) + } else { + minecraft.disconnect() + } + + val titlescreen = TitleScreen() + if (flag) { + minecraft.setScreen(titlescreen) + } else if (serverdata != null && serverdata.isRealm) { + minecraft.setScreen(RealmsMainScreen(titlescreen)) + } else { + minecraft.setScreen(JoinMultiplayerScreen(titlescreen)) + } + } + return AClientGameTestSummary( + passed = passed, + failed = failed, + skipped = skipped, + failedTests = failedTests, + failedDetails = failedDetails, + ) + } +} + +/** + * Warps the real GLFW cursor to the same position a synthetic [TestInput] call just fed to + * [net.minecraft.client.gui.screens.Screen.mouseMoved]/`mouseClicked` directly. + * + * That synthetic dispatch bypasses GLFW entirely, so vanilla's own `MouseHandler` never learns + * about it - its own cursor-position callback still fires from whatever the OS/window's real + * cursor is doing, completely independent of the test's intended position. If that callback + * later reports a position outside the node the test just hovered/clicked, it fires its own + * `mouseMoved` with the stale real coordinates, silently overwriting `hovered` state right back + * to false - a real cursor twitch (or, under Xvfb, a window-manager cursor warp on focus) racing + * a test assertion and failing it. Keeping the real cursor in sync removes that race outright: + * any later real callback reports the same position the test already set, so no spurious + * enter/exit transition can happen. + */ +private fun warpRealCursor(client: Minecraft, guiX: Double, guiY: Double) { + val window = client.window + val realX = guiX * window.screenWidth / window.guiScaledWidth + val realY = guiY * window.screenHeight / window.guiScaledHeight + GLFW.glfwSetCursorPos(window.window, realX, realY) +} + +private fun selectModsToRun(modToClasses: Map>>): Map>> { + val selected = AGameTestModFilter.selectMods(modToClasses.keys).toSet() + return modToClasses.filterKeys { it in selected } +} + +private fun rootCauseSummary(error: Throwable): String { + val root = generateSequence(error) { it.cause }.last() + val message = root.message?.takeIf { it.isNotBlank() } + return if (message != null) { + "${root::class.java.name}: $message" + } else { + root::class.java.name + } +} + diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt new file mode 100644 index 000000000..e53281870 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt @@ -0,0 +1,20 @@ +package net.kernelpanicsoft.archie.gametest + +import net.kernelpanicsoft.archie.events.AGametestEvents +import net.kernelpanicsoft.archie.events.AEventObject +import dev.architectury.event.Event +import dev.architectury.platform.Mod + +/** + * Convenience [AEventObject] base for listening to [AGametestEvents.REGISTER_GAME_TEST] for + * [mod]. Subclass and override [handler] (an [AGametestEvents.ArchieGameTestBuilder] receiver) + * to declare gametest classes via `server { register<...>() }` / `client { ... }` / `common { ... }`. + */ +abstract class AGameTestEventObject(mod: Mod) : + AEventObject( + mod + ) +{ + override val event: Event = AGametestEvents.REGISTER_GAME_TEST + override val handlerConstructor: AGametestEvents.RegisterGameTestHandler.Companion = AGametestEvents.RegisterGameTestHandler.Companion +} diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ComposeScreenTestContext.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ComposeScreenTestContext.kt new file mode 100644 index 000000000..55312b708 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ComposeScreenTestContext.kt @@ -0,0 +1,286 @@ +package net.kernelpanicsoft.archie.gametest + +import dev.architectury.registry.menu.ExtendedMenuProvider +import dev.architectury.registry.menu.MenuRegistry +import net.kernelpanicsoft.archie.gui.ComposeContainerScreen +import net.kernelpanicsoft.archie.gui.LayerManagerProvider +import net.kernelpanicsoft.archie.gui.layer.Layer +import net.kernelpanicsoft.archie.gui.layout.LayoutNode +import net.minecraft.client.gui.screens.Screen +import net.minecraft.client.player.LocalPlayer +import net.minecraft.core.BlockPos +import net.minecraft.world.level.block.entity.BlockEntity +import net.minecraft.world.level.block.state.BlockState +import org.apache.commons.lang3.function.FailableConsumer +import org.apache.commons.lang3.function.FailableFunction + +/** Selects which [Layer] a [ComposeScreenTestContext] node lookup searches. */ +enum class LayerSelector { + /** The frontmost layer (a modal/dialog if one is open, otherwise the base screen). */ + Top, + + /** The screen's original base layer, regardless of any modals stacked on top of it. */ + Base, +} + +/** A resolved [LayoutNode] handle, scoped to a single [ComposeScreenTestContext.node] block. */ +@Suppress("unused") +class TestNodeScope( + val context: ClientGameTestContext, + val node: LayoutNode, +) { + /** + * Waits for compose to settle before reading [node]'s on-screen bounds - without this, a + * node's very first interaction (right after [ComposeScreenTestContext.waitForScreen]/ + * [ComposeScreenTestContext.node] finds it) can read a transient pre-layout-settle position + * (e.g. before a wrapping Scrollable's initial measure has stabilized), computing a click/ + * hover target that no longer matches the node's real bounds one frame later - silently + * missing the node (no ENTER/PRESS ever dispatches) rather than failing loudly. + */ + private fun centerCoords(): Pair { + context.waitForComposeIdle() + return context.computeOnClient { + val (nx, ny) = node.absoluteCoords + (nx + node.width / 2.0) to (ny + node.height / 2.0) + } + } + + /** Clicks the center of this node's on-screen bounds. */ + fun click(button: Int = 0) { + val (x, y) = centerCoords() + context.getInput().click(x, y, button) + } + + /** Moves the cursor to the center of this node's on-screen bounds, without clicking - e.g. to assert a [TextureStates.HOVERED] visual state. */ + fun hover() { + val (x, y) = centerCoords() + context.getInput().setCursor(x, y) + } + + /** + * Presses and releases [keyCode]. Key input in this framework targets the active screen as + * a whole, not a specific node - [click] (or [hover], for a text field that focuses on + * hover) the target first if it needs focus. + */ + fun pressKey(keyCode: Int, scanCode: Int = 0, modifiers: Int = 0) { + context.getInput().pressKey(keyCode, scanCode, modifiers) + } + + /** Types each character of [value] as if typed at the keyboard. See [pressKey] re: focus. */ + fun type(value: String) { + context.getInput().typeChars(value) + } + + /** Moves the cursor to this node's center, then scrolls there. See [TestInput.scroll]. */ + fun scroll(x: Double = 0.0, y: Double = 1.0) { + val (cx, cy) = centerCoords() + context.getInput().setCursor(cx, cy) + context.getInput().scroll(x, y) + } + + /** + * The [TextureStates] key this node's [net.kernelpanicsoft.archie.gui.layout.Renderer] most + * recently selected to draw (e.g. `"hovered"`), or `null` if this node doesn't render a + * theme-state-driven visual. See [net.kernelpanicsoft.archie.gui.nodes.UINode.renderState]. + */ + val renderState: String? get() = context.computeOnClient { node.renderState } + + /** + * Fails unless this node's [renderState] equals [expected]. + * + * Reads [renderState] exactly once - the same value is used both to decide pass/fail and + * (on failure) in the default message. Re-reading it live inside the message lambda instead + * would race further recomposition between the comparison and the (lazily-evaluated, only + * on failure) message being built, showing a misleading "got " that no longer + * matches whatever value the comparison actually failed on. + */ + fun assertRenderState( + expected: String, + message: (() -> String)? = null, + ) { + val actual = renderState + context.assertEquals(expected, actual, message ?: { + "Expected node '${node.name}' render state <$expected>, got <$actual> (testId=${context.testId})" + }) + } + + /** This node's direct children's names, in composition order. */ + fun childNames(): List = context.computeOnClient { node.children.map { it.name } } + + /** Fails unless this node's direct children's names, in order, equal [expected]. */ + fun assertChildNames(vararg expected: String) { + val actual = childNames() + context.assertEquals(expected.toList(), actual) { + "Expected node '${node.name}' children <${expected.toList()}>, got <$actual> (testId=${context.testId})\n${describeTree()}" + } + } + + /** Whether a descendant named [name] exists anywhere in this node's subtree, without failing. */ + fun hasDescendant(name: String): Boolean = context.computeOnClient { node.findNode(name) != null } + + /** Fails unless a descendant named [name] exists anywhere in this node's subtree. */ + fun assertHasDescendant(name: String) { + context.assertTrue(hasDescendant(name)) { + "Expected node '${node.name}' to have a descendant named '$name' (testId=${context.testId})\n${describeTree()}" + } + } + + /** + * Fails if this node or any descendant has a non-positive width or height - the "zero-size + * widget" class of layout bug, catchable without any pixel comparison. + */ + fun assertAllDescendantsSized() { + val unsized = context.computeOnClient { node.flatten().filter { it.width <= 0 || it.height <= 0 } } + context.assertTrue(unsized.isEmpty()) { + "Expected every node under '${node.name}' to have a positive size, but found zero-sized: " + + unsized.joinToString { "${it.name}(${it.width}x${it.height})" } + + " (testId=${context.testId})\n${describeTree()}" + } + } + + /** A recursive dump of this node's subtree (name, nested per child), for failure messages. */ + fun describeTree(): String = context.computeOnClient { node.toString() } + + /** + * All descendants of this node named [name], in depth-first order - the escape hatch for + * [node] (which requires exactly one match) when a subtree legitimately has several, e.g. + * every "Button" in a dialog's action row. + */ + fun nodes(name: String): List = context.computeOnClient { node.findAllNodes(name) } + + /** + * Waits for a descendant named [name] within this node's subtree (not the whole layer) to + * appear, then runs [block] against it. Fails if [name] doesn't appear within [timeout] ticks. + */ + fun node( + name: String, + timeout: Int = ClientGameTestContext.DEFAULT_TIMEOUT, + block: TestNodeScope.() -> R, + ): R { + context.waitFor({ _ -> node.findNode(name) != null }, timeout) + val resolved = context.computeOnClient { node.findNode(name) } + ?: context.fail("Node '$name' not found under '${node.name}' (testId=${context.testId})") + return TestNodeScope(context, resolved).block() + } + + operator fun LayoutNode.invoke(block: TestNodeScope.() -> R): R + { + return TestNodeScope(context, this).block() + } +} + +/** + * Kotlin-idiomatic access to a [ComposeContainerScreen]'s layer/node tree from a client game + * test, replacing manual `layerManager.layers...findNode(...)` bookkeeping with a small + * receiver-block DSL. + */ +@Suppress("unused") +class ComposeScreenTestContext internal constructor( + val context: ClientGameTestContext, + val screen: S, +) { + /** The number of layers currently on the stack (1 = no modal open). */ + val layerCount: Int get() = context.computeOnClient { screen.layerManager.layers.size } + + /** The frontmost [Layer] (a modal/dialog if one is open, otherwise the base screen). */ + val topLayer: Layer get() = layer(layer = LayerSelector.Top) + /** The screen's original base [Layer], regardless of any modals stacked on top of it. */ + val baseLayer: Layer get() = layer(layer = LayerSelector.Base) + + private fun resolveLayer(selector: LayerSelector): Layer? = when (selector) { + LayerSelector.Top -> screen.layerManager.top + LayerSelector.Base -> screen.layerManager.layers.firstOrNull() + } + + private fun resolveNode(name: String, layer: LayerSelector): LayoutNode? = + resolveLayer(layer)?.findNode(name) + + /** Checks whether a node named [name] currently exists, without waiting for it. */ + fun hasNode(name: String, layer: LayerSelector = LayerSelector.Top): Boolean = + context.computeOnClient { resolveNode(name, layer) != null } + + /** Resolves [layer] to a [Layer] immediately, without waiting. Fails if it doesn't currently exist. */ + fun layer(layer: LayerSelector = LayerSelector.Top): Layer = context.computeOnClient { resolveLayer(layer) ?: error("Layer not found") } + + /** Resolves the layer at stack position [index] immediately, without waiting. Fails if it doesn't currently exist. */ + fun layer(index: Int): Layer = context.computeOnClient { screen.layerManager.layers.getOrNull(index) ?: error("Layer not found") } + + /** Waits for a layer to appear at stack position [index], then runs [block] against it. */ + fun waitForLayer(index: Int, block: Layer.() -> Unit = {}) { + context.waitFor { screen.layerManager.layers.getOrNull(index) != null } + val resolved = context.computeOnClient { screen.layerManager.layers.getOrNull(index) } + ?: context.fail("Layer '$index' not found (testId=${context.testId})") + return resolved.block() + } + + /** Waits for a node named [name] to appear on this specific [Layer], then runs [block] against it. */ + fun Layer.node( + name: String, + timeout: Int = ClientGameTestContext.DEFAULT_TIMEOUT, + block: TestNodeScope.() -> R, + ): R { + context.waitFor { findNode(name) != null } + val resolved = context.computeOnClient { findNode(name) } + ?: context.fail("Node '$name' not found (testId=${context.testId})") + return TestNodeScope(context, resolved).block() + } + + /** + * Waits for a node named [name] to appear on [layer] (default: the topmost layer), then + * runs [block] against it. Fails the test if the node doesn't appear within [timeout] ticks. + */ + fun node( + name: String, + layer: LayerSelector = LayerSelector.Top, + timeout: Int = ClientGameTestContext.DEFAULT_TIMEOUT, + block: TestNodeScope.() -> R, + ): R { + context.waitFor({ _ -> resolveNode(name, layer) != null }, timeout) + val resolved = context.computeOnClient { resolveNode(name, layer) } + ?: context.fail("Node '$name' not found on ${layer.name.lowercase()} layer (testId=${context.testId})") + return TestNodeScope(context, resolved).block() + } + + /** Wraps an already-resolved [LayoutNode] (e.g. one indexed out of [TestNodeScope.nodes]) for interaction, without constructing a [TestNodeScope] by hand. */ + operator fun LayoutNode.invoke(block: TestNodeScope.() -> R): R = TestNodeScope(context, this).block() +} + +/** Reified convenience for [ClientGameTestContext.waitForScreen] that resolves [S]'s [Class] automatically. */ +inline fun ClientGameTestContext.waitForScreen(noinline block: ComposeScreenTestContext.() -> Unit = {}) where S : Screen, S : LayerManagerProvider = waitForScreen(S::class.java, block) + +/** Waits until the client-side player entity exists, then returns it. */ +fun ClientGameTestContext.waitForPlayer(): LocalPlayer = waitFor { client -> client.player != null }.let { computeOnClient { client -> client.player!! } } + +/** Waits until a block entity of type [T] exists at [pos] on the client, then returns the server-side instance. */ +inline fun TestSingleplayerContext.waitForTile(pos: BlockPos): T +{ + clientContext.waitFor { client -> client.level?.getBlockEntity(pos) is T } + return server.computeOnServer { minecraftServer -> + val player = minecraftServer.playerList.players.first() + val level = player.level() + val tile = level.getBlockEntity(pos) as? T + tile ?: error("Tile not found at $pos") + } +} + +/** Places [state] at [pos], waits for its [BlockEntity] of type [T] to exist, then opens its menu for the test's player. */ +inline fun TestSingleplayerContext.placeTileAndOpenMenu(pos: BlockPos, state: BlockState) where T : BlockEntity, T : ExtendedMenuProvider +{ + server.runOnServer { minecraftServer -> + val player = minecraftServer.playerList.players.first() + val level = player.level() + level.setBlockAndUpdate(pos, state) + } + val tile = waitForTile(pos) + server.runOnServer { minecraftServer -> + val player = minecraftServer.playerList.players.first() + MenuRegistry.openExtendedMenu(player, tile) + } +} + +/** Combines [placeTileAndOpenMenu] and [waitForScreen]: places [state], opens its menu, then waits for [S] and runs [block]. */ +inline fun TestSingleplayerContext.placeTileAndWaitForScreen(pos: BlockPos, state: BlockState, noinline block: ComposeScreenTestContext.() -> Unit = {}) where T : BlockEntity, T : ExtendedMenuProvider, S : Screen, S : LayerManagerProvider +{ + placeTileAndOpenMenu(pos, state) + clientContext.waitForScreen(block) +} diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt new file mode 100644 index 000000000..be5a0238a --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt @@ -0,0 +1,50 @@ +package net.kernelpanicsoft.archie.gametest + +import net.minecraft.gametest.framework.GameTestHelper + +/** + * Fails this GameTest via [GameTestHelper.fail] with [message] if [condition] is `false`. + */ +fun GameTestHelper.assertTrue(condition: Boolean, message: () -> String) +{ + if (!condition) { + fail(message()) + } +} + +/** + * Fails this GameTest via [GameTestHelper.fail] if [expected] and [actual] are not equal. + * + * @param message Failure message builder; defaults to reporting both values. + */ +fun GameTestHelper.assertEquals( + expected: T, + actual: T, + message: () -> String = { "Expected <$expected>, got <$actual>" }, +) +{ + if (expected != actual) { + fail(message()) + } +} + +/** + * Runs [block] and asserts it throws a [T], failing this GameTest via [GameTestHelper.fail] + * if [block] completes without throwing or throws a different exception type. + * + * @return The caught exception of type [T]. + */ +inline fun GameTestHelper.expectThrows(noinline block: () -> Unit): T +{ + return try { + block() + fail("Expected exception ${T::class.simpleName} to be thrown") + throw IllegalStateException("Unreachable") + } catch (t: Throwable) { + if (t is T) t + else { + fail("Expected ${T::class.simpleName}, got ${t::class.simpleName}") + throw IllegalStateException("Unreachable", t) + } + } +} diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt new file mode 100644 index 000000000..6745acc6a --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt @@ -0,0 +1,21 @@ +package net.kernelpanicsoft.archie.gametest + +import net.minecraft.gametest.framework.GameTest +import net.minecraft.gametest.framework.GameTestHelper + +/** + * A trivially-succeeding placeholder, registered by [AGameTestPlatformInternal] on each loader + * when a mod's [AGameTestModFilter]-selected suite has no real test functions for the current + * [AGameTestSide] - e.g. a mod with only client-side coverage (like Archie-Test, whose own suite + * registers just [net.kernelpanicsoft.archie.test.gametest.TestScreenGameTest]) running its + * server invocation. Vanilla's `GameTestServer` refuses to boot with zero registered test + * functions at all (`IllegalArgumentException: No test functions were given!`); this keeps that + * boot trivially satisfied instead of crashing the whole invocation. + */ +@Suppress("unused") +class NoOpGameTest { + @GameTest(template = "archie:gametest/empty") + fun GameTestHelper.testNoOpPlaceholder() { + succeed() + } +} diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparer.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparer.kt new file mode 100644 index 000000000..8bb02e113 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparer.kt @@ -0,0 +1,101 @@ +package net.kernelpanicsoft.archie.gametest + +import org.joml.Vector2i +import java.awt.Color +import java.awt.image.BufferedImage +import java.io.File +import javax.imageio.ImageIO +import kotlin.math.abs + +/** + * Screenshot comparison utility with support for both exact and fuzzy matching. + * Based on Fabric's TestScreenshotComparisonAlgorithms implementation. + */ +object ScreenshotComparer { + /** + * Compare two images for exact equality (all pixels must match). + * @return true if images are identical, false otherwise + */ + fun imagesEqual(templateFile: File, captureFile: File): Boolean { + val template = readImage(templateFile) ?: return false + val capture = readImage(captureFile) ?: return false + + if (template.width != capture.width || template.height != capture.height) { + return false + } + + val templateRgb = template.getRGB(0, 0, template.width, template.height, null, 0, template.width) + val captureRgb = capture.getRGB(0, 0, capture.width, capture.height, null, 0, capture.width) + + return templateRgb.contentEquals(captureRgb) + } + + /** + * Find a template image within a larger capture image (allowing sub-image matching). + * Uses exact pixel-by-pixel matching by default. + * @return top-left corner of the matched region, or null if not found + */ + fun findInImage(templateFile: File, captureFile: File, tolerance: Int = 0): Vector2i? { + val template = readImage(templateFile) ?: return null + val capture = readImage(captureFile) ?: return null + + if (template.width > capture.width || template.height > capture.height) { + return null + } + + val algorithm = if (tolerance > 0) { + MeanSquaredDifferenceAlgorithm(tolerance / 255.0f) + } else { + ExactScreenshotComparisonAlgorithm + } + + val templateRgb = template.getRGB(0, 0, template.width, template.height, null, 0, template.width) + val captureRgb = capture.getRGB(0, 0, capture.width, capture.height, null, 0, capture.width) + + val templateRawImage = RawImageImpl(template.width, template.height, templateRgb) + val captureRawImage = RawImageImpl(capture.width, capture.height, captureRgb) + + return algorithm.findColor(captureRawImage, templateRawImage) + } + + /** + * Find a template image using exact pixel matching. + */ + fun findInImageExact(templateFile: File, captureFile: File): Vector2i? { + return findInImage(templateFile, captureFile, tolerance = 0) + } + + /** + * Find a template image using fuzzy matching with configurable threshold. + * @param maxMeanSquaredDifference tolerance threshold (0.0-1.0) + */ + fun findInImageFuzzy(templateFile: File, captureFile: File, maxMeanSquaredDifference: Float = 0.005f): Vector2i? { + val template = readImage(templateFile) ?: return null + val capture = readImage(captureFile) ?: return null + + if (template.width > capture.width || template.height > capture.height) { + return null + } + + val algorithm = MeanSquaredDifferenceAlgorithm(maxMeanSquaredDifference) + + val templateRgb = template.getRGB(0, 0, template.width, template.height, null, 0, template.width) + val captureRgb = capture.getRGB(0, 0, capture.width, capture.height, null, 0, capture.width) + + val templateRawImage = RawImageImpl(template.width, template.height, templateRgb) + val captureRawImage = RawImageImpl(capture.width, capture.height, captureRgb) + + return algorithm.findColor(captureRawImage, templateRawImage) + } + + private fun readImage(file: File): BufferedImage? { + return try { + if (file.exists()) ImageIO.read(file) else null + } catch (e: Exception) { + null + } + } +} + + + diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparisonAlgorithm.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparisonAlgorithm.kt new file mode 100644 index 000000000..a3d53e291 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparisonAlgorithm.kt @@ -0,0 +1,211 @@ +package net.kernelpanicsoft.archie.gametest + +/** + * Comparison algorithm interface for screenshot matching. + * Supports both exact and fuzzy matching with configurable thresholds. + */ +interface ScreenshotComparisonAlgorithm { + /** + * Find a template pattern in a larger capture image using color data. + * @return top-left corner of matched region, or null if not found + */ + fun findColor(haystack: RawImage, needle: RawImage): org.joml.Vector2i? + + /** + * Find a template pattern in a larger capture image using grayscale data. + * @return top-left corner of matched region, or null if not found + */ + fun findGrayscale(haystack: RawImage, needle: RawImage): org.joml.Vector2i? + + /** + * Raw image data holder for comparison operations. + */ + interface RawImage { + fun width(): Int + fun height(): Int + fun data(): DATA + } +} + +/** + * Exact pixel matching algorithm - all pixels must match exactly. + */ +object ExactScreenshotComparisonAlgorithm : ScreenshotComparisonAlgorithm { + override fun findColor(haystack: ScreenshotComparisonAlgorithm.RawImage, needle: ScreenshotComparisonAlgorithm.RawImage): org.joml.Vector2i? { + val haystackData = haystack.data() + val needleData = needle.data() + val haystackWidth = haystack.width() + val needleWidth = needle.width() + val needleHeight = needle.height() + + if (needleWidth > haystackWidth || needleHeight > haystack.height()) { + return null + } + + for (needleY in 0..(haystack.height() - needleHeight)) { + for (needleX in 0..(haystackWidth - needleWidth)) { + var match = true + for (y in 0 until needleHeight) { + for (x in 0 until needleWidth) { + val haystackColor = haystackData[(needleY + y) * haystackWidth + needleX + x] + val needleColor = needleData[y * needleWidth + x] + if (haystackColor != needleColor) { + match = false + break + } + } + if (!match) break + } + if (match) { + return org.joml.Vector2i(needleX, needleY) + } + } + } + return null + } + + override fun findGrayscale(haystack: ScreenshotComparisonAlgorithm.RawImage, needle: ScreenshotComparisonAlgorithm.RawImage): org.joml.Vector2i? { + val haystackData = haystack.data() + val needleData = needle.data() + val haystackWidth = haystack.width() + val needleWidth = needle.width() + val needleHeight = needle.height() + + if (needleWidth > haystackWidth || needleHeight > haystack.height()) { + return null + } + + for (needleY in 0..(haystack.height() - needleHeight)) { + for (needleX in 0..(haystackWidth - needleWidth)) { + var match = true + for (y in 0 until needleHeight) { + for (x in 0 until needleWidth) { + val haystackLuminance = haystackData[(needleY + y) * haystackWidth + needleX + x] + val needleLuminance = needleData[y * needleWidth + x] + if (haystackLuminance != needleLuminance) { + match = false + break + } + } + if (!match) break + } + if (match) { + return org.joml.Vector2i(needleX, needleY) + } + } + } + return null + } +} + +/** + * Mean squared difference algorithm - allows fuzzy matching within a tolerance threshold. + * Based on Fabric's TestScreenshotComparisonAlgorithms implementation. + */ +data class MeanSquaredDifferenceAlgorithm(val maxMeanSquaredDifference: Float = 0.005f) : ScreenshotComparisonAlgorithm { + override fun findColor(haystack: ScreenshotComparisonAlgorithm.RawImage, needle: ScreenshotComparisonAlgorithm.RawImage): org.joml.Vector2i? { + val haystackData = haystack.data() + val needleData = needle.data() + val haystackWidth = haystack.width() + val needleWidth = needle.width() + val needleHeight = needle.height() + + if (needleWidth > haystackWidth || needleHeight > haystack.height()) { + return null + } + + // Threshold calculation to avoid floating point in inner loop + val threshold = (maxMeanSquaredDifference * needleWidth * needleHeight * 3 * 255 * 255).toLong() + + for (needleY in 0..(haystack.height() - needleHeight)) { + for (needleX in 0..(haystackWidth - needleWidth)) { + var sumSquaredDifference = 0L + var match = true + + for (y in 0 until needleHeight) { + for (x in 0 until needleWidth) { + val haystackColor = haystackData[(needleY + y) * haystackWidth + needleX + x] + val haystackRed = (haystackColor shr 16) and 0xFF + val haystackGreen = (haystackColor shr 8) and 0xFF + val haystackBlue = haystackColor and 0xFF + + val needleColor = needleData[y * needleWidth + x] + val needleRed = (needleColor shr 16) and 0xFF + val needleGreen = (needleColor shr 8) and 0xFF + val needleBlue = needleColor and 0xFF + + val diffRed = haystackRed - needleRed + val diffGreen = haystackGreen - needleGreen + val diffBlue = haystackBlue - needleBlue + + sumSquaredDifference += (diffRed * diffRed + diffGreen * diffGreen + diffBlue * diffBlue).toLong() + + if (sumSquaredDifference >= threshold) { + match = false + break + } + } + if (!match) break + } + + if (match) { + return org.joml.Vector2i(needleX, needleY) + } + } + } + return null + } + + override fun findGrayscale(haystack: ScreenshotComparisonAlgorithm.RawImage, needle: ScreenshotComparisonAlgorithm.RawImage): org.joml.Vector2i? { + val haystackData = haystack.data() + val needleData = needle.data() + val haystackWidth = haystack.width() + val needleWidth = needle.width() + val needleHeight = needle.height() + + if (needleWidth > haystackWidth || needleHeight > haystack.height()) { + return null + } + + val threshold = (maxMeanSquaredDifference * needleWidth * needleHeight * 255 * 255).toLong() + + for (needleY in 0..(haystack.height() - needleHeight)) { + for (needleX in 0..(haystackWidth - needleWidth)) { + var sumSquaredDifference = 0L + var match = true + + for (y in 0 until needleHeight) { + for (x in 0 until needleWidth) { + val haystackLuminance = haystackData[(needleY + y) * haystackWidth + needleX + x].toInt() and 0xFF + val needleLuminance = needleData[y * needleWidth + x].toInt() and 0xFF + val diff = haystackLuminance - needleLuminance + + sumSquaredDifference += (diff * diff).toLong() + + if (sumSquaredDifference >= threshold) { + match = false + break + } + } + if (!match) break + } + + if (match) { + return org.joml.Vector2i(needleX, needleY) + } + } + } + return null + } +} + +/** + * Raw image data implementation for comparison operations. + */ +data class RawImageImpl(val width: Int, val height: Int, val data: DATA) : ScreenshotComparisonAlgorithm.RawImage { + override fun width() = width + override fun height() = height + override fun data() = data +} + + diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotManager.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotManager.kt new file mode 100644 index 000000000..24c92301a --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotManager.kt @@ -0,0 +1,54 @@ +package net.kernelpanicsoft.archie.gametest + +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths + +/** + * Manages screenshot file I/O, template storage, and directory organization. + */ +object ScreenshotManager { + private fun getScreenshotBaseDir(): Path { + val baseDir = Paths.get("build", "gametests", "screenshots") + Files.createDirectories(baseDir) + return baseDir + } + + /** + * Get directory for captured test screenshots. + */ + fun getCaptureDir(): Path { + val dir = getScreenshotBaseDir().resolve("captures") + Files.createDirectories(dir) + return dir + } + + /** + * Get directory for screenshot templates (baselines). + */ + fun getTemplateDir(): Path { + val dir = getScreenshotBaseDir().resolve("templates") + Files.createDirectories(dir) + return dir + } + + /** + * Resolve a template image file by name. + * Searches in template directory with .png extension. + */ + fun resolveTemplate(templateName: String): Path { + val filename = if (templateName.endsWith(".png")) templateName else "$templateName.png" + return getTemplateDir().resolve(filename) + } + + /** + * Generate a unique capture filename for a test screenshot. + */ + fun generateCapturePath(testId: String, screenshotName: String): Path { + val sanitized = (testId + "_" + screenshotName) + .replace(Regex("[^a-zA-Z0-9_\\-.]"), "_") + val filename = if (sanitized.endsWith(".png")) sanitized else "$sanitized.png" + return getCaptureDir().resolve(filename) + } +} + diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/VerboseTestReporter.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/VerboseTestReporter.kt new file mode 100644 index 000000000..bb06bfd63 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/VerboseTestReporter.kt @@ -0,0 +1,30 @@ +package net.kernelpanicsoft.archie.gametest + +import dev.architectury.platform.Platform +import net.kernelpanicsoft.archie.Archie +import net.minecraft.gametest.framework.GameTestInfo +import net.minecraft.gametest.framework.TestReporter +import net.minecraft.resources.ResourceLocation + +/** Logs each GameTest's pass/fail result through [Archie.LOGGER] as it completes. */ +object VerboseTestReporter : TestReporter +{ + override fun onTestFailed(testInfo: GameTestInfo) + { + Archie.LOGGER.error("[GameTest] FAIL {}", testId(testInfo), testInfo.error) + } + + override fun onTestSuccess(testInfo: GameTestInfo) + { + + Archie.LOGGER.info("[GameTest] PASS {}", testId(testInfo)) + } + + /** A human-readable id for [testInfo]: `":"`, or just the test name if the owning mod isn't loaded. */ + fun testId(testInfo: GameTestInfo): String + { + val testModId = ResourceLocation.parse(testInfo.structureName).namespace + if (!Platform.isModLoaded(testModId)) return testInfo.testName + return "${testModId}:${testInfo.testName}" + } +} \ No newline at end of file diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt new file mode 100644 index 000000000..9fd978f81 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt @@ -0,0 +1,49 @@ +package net.kernelpanicsoft.archie.gametest.internal + +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.events.AGametestEvents +import net.kernelpanicsoft.archie.gametest.AGameTestEventObject +import net.kernelpanicsoft.archie.gametest.internal.tests.ArchieItemHandlerTests +import net.kernelpanicsoft.archie.gametest.internal.tests.BlockEntityNBTHolderTests +import net.kernelpanicsoft.archie.gametest.internal.tests.BlockEntityStateManagerTests +import net.kernelpanicsoft.archie.gametest.internal.tests.ComposeRenderingTests +import net.kernelpanicsoft.archie.gametest.internal.tests.InputComponentsGameTest +import net.kernelpanicsoft.archie.gametest.internal.tests.LayoutComponentsGameTest +import net.kernelpanicsoft.archie.gametest.internal.tests.ModalComponentsGameTest + +/** + * ID of the empty structure template used by every GameTest in this suite; GameTests that don't + * need a specific structure should reference this via `@GameTest(template = EMPTY)`. + */ +const val EMPTY = "archie:gametest/empty" + +/** + * Registers Archie's own internal GameTest suite (the tests under [net.kernelpanicsoft.archie.gametest.internal.tests]) + * against the given builder, scoped by the environment each suite needs to run in. + */ +internal fun AGametestEvents.ArchieGameTestBuilder.archieGameTests() +{ + common { + + } + client { + register() + register() + register() + register() + } + server { + register() + register() + register() + } +} + +/** + * Registration entry point for Archie's internal GameTest suite, hooked into [AGametestEvents]'s + * GameTest registration handler for [Archie.MOD]. + */ +internal object ArchieGameTest : AGameTestEventObject(Archie.MOD) +{ + override fun AGametestEvents.ArchieGameTestBuilder.handler() = archieGameTests() +} \ No newline at end of file diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/GametestArchieExtension.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/GametestArchieExtension.kt new file mode 100644 index 000000000..522a4c158 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/GametestArchieExtension.kt @@ -0,0 +1,12 @@ +package net.kernelpanicsoft.archie.gametest.internal + +import net.kernelpanicsoft.archie.ArchieExtension + +/** [ArchieExtension] hook that activates [ArchieGameTest] when `archie-gametest` is on the classpath. */ +internal class GametestArchieExtension : ArchieExtension +{ + override fun onGameTest() + { + ArchieGameTest.init() + } +} diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt new file mode 100644 index 000000000..8bf0ca92a --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt @@ -0,0 +1,77 @@ +package net.kernelpanicsoft.archie.gametest.internal.tests + +import earth.terrarium.common_storage_lib.resources.item.ItemResource +import net.kernelpanicsoft.archie.gametest.assertEquals +import net.kernelpanicsoft.archie.gametest.assertTrue +import net.kernelpanicsoft.archie.gametest.internal.EMPTY +import net.kernelpanicsoft.archie.transfer.ArchieItemStorage +import net.minecraft.gametest.framework.GameTest +import net.minecraft.gametest.framework.GameTestHelper +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items + +/** + * GameTest coverage for [ArchieItemStorage]: max-stack-size clamping on insert, resource + * clearing on full extraction, and that simulated insert/extract calls never mutate storage. + */ +@Suppress("unused") +class ArchieItemHandlerTests +{ + @GameTest(template = EMPTY) + fun GameTestHelper.testInsertRespectsMaxStackSize() + { + val storage = ArchieItemStorage(1) + val stone = ItemResource.of(ItemStack(Items.STONE, 1)) + + val inserted = storage.insert(stone, 80, false) + + assertEquals(64L, inserted) + assertEquals(64, storage.get(0).getItem().count) + succeed() + } + + @GameTest(template = EMPTY) + fun GameTestHelper.testExtractToZeroClearsResource() + { + val storage = ArchieItemStorage(1) + val stone = ItemResource.of(ItemStack(Items.STONE, 1)) + + storage.insert(stone, 10, false) + val extracted = storage.extract(stone, 10, false) + + assertEquals(10L, extracted) + assertTrue(storage.get(0).getItem().isEmpty) { + "Expected slot to be empty after full extraction" + } + succeed() + } + + @GameTest(template = EMPTY) + fun GameTestHelper.testSimulatedInsertDoesNotMutateStorage() + { + val storage = ArchieItemStorage(1) + val diamond = ItemResource.of(ItemStack(Items.DIAMOND, 1)) + + val inserted = storage.insert(diamond, 16, true) + + assertEquals(16L, inserted) + assertTrue(storage.get(0).getItem().isEmpty) { + "Simulated insert should not mutate slot contents" + } + succeed() + } + + @GameTest(template = EMPTY) + fun GameTestHelper.testSimulatedExtractDoesNotMutateStorage() + { + val storage = ArchieItemStorage(1) + val iron = ItemResource.of(ItemStack(Items.IRON_INGOT, 1)) + storage.insert(iron, 7, false) + + val extracted = storage.extract(iron, 4, true) + + assertEquals(4L, extracted) + assertEquals(7, storage.get(0).getItem().count) + succeed() + } +} diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt new file mode 100644 index 000000000..20252e262 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt @@ -0,0 +1,127 @@ +package net.kernelpanicsoft.archie.gametest.internal.tests + +import dev.architectury.fluid.FluidStack +import net.kernelpanicsoft.archie.gametest.assertEquals +import net.kernelpanicsoft.archie.gametest.assertTrue +import net.kernelpanicsoft.archie.gametest.internal.EMPTY +import net.kernelpanicsoft.archie.serialization.NBTHolder +import net.kernelpanicsoft.archie.serialization.Sync +import net.kernelpanicsoft.archie.serialization.listField +import net.kernelpanicsoft.archie.serialization.mapField +import net.minecraft.gametest.framework.GameTest +import net.minecraft.gametest.framework.GameTestHelper +import net.minecraft.nbt.CompoundTag +import net.minecraft.world.item.ItemStack +import net.minecraft.world.item.Items +import net.minecraft.world.level.material.Fluids + +/** + * GameTest coverage for [NBTHolder]: default values, save/load round-tripping for scalar, + * list, map, item, fluid, and energy delegated fields, and that only [Sync]-annotated fields + * appear in the sync tag. + */ +@Suppress("unused") +class BlockEntityNBTHolderTests +{ + /** Minimal [NBTHolder] with one of each supported field kind, used as a fixture across tests. */ + private class HolderFixture : NBTHolder by NBTHolder.create() + { + var counter by intField { 1 } + var label by stringField { "default" } + val values by listField { listOf(1, 2) } + val weights by mapField { mapOf("a" to 1) } + + @Sync + var syncedCounter by intField { 7 } + } + + /** [NBTHolder] with one of each resource-storage field kind, used only by the test below. */ + private class ResourceFixture : NBTHolder by NBTHolder.create() + { + val items by itemField(1) + val tank by fluidField(FluidStack.bucketAmount() * 2) + val energy by energyField(1_000) + } + + @GameTest(template = EMPTY) + fun GameTestHelper.testFieldDefaultsAndPersistenceRoundTrip() + { + val holder = HolderFixture() + assertEquals(1, holder.counter) + assertEquals("default", holder.label) + + holder.counter = 12 + holder.label = "changed" + + val tag = CompoundTag() + holder.saveToTag(tag) + + val loaded = HolderFixture() + loaded.loadFromTag(tag) + + assertEquals(12, loaded.counter) + assertEquals("changed", loaded.label) + succeed() + } + + @GameTest(template = EMPTY) + fun GameTestHelper.testListAndMapDelegatesPersistMutations() + { + val holder = HolderFixture() + holder.values.add(3) + holder.weights["b"] = 2 + + val tag = CompoundTag() + holder.saveToTag(tag) + + val loaded = HolderFixture() + loaded.loadFromTag(tag) + + assertEquals(listOf(1, 2, 3), loaded.values.toList()) + assertEquals(2, loaded.weights["b"]) + succeed() + } + + @GameTest(template = EMPTY) + fun GameTestHelper.testSyncTagContainsOnlySyncAnnotatedFields() + { + val holder = HolderFixture() + holder.counter = 42 + holder.syncedCounter = 9 + + val syncTag = holder.getSyncTag() + assertTrue("synced_counter" in syncTag) { + "Expected sync tag to include synced field" + } + assertTrue("counter" !in syncTag) { + "Expected sync tag to exclude non-synced field" + } + succeed() + } + + @GameTest(template = EMPTY) + fun GameTestHelper.testItemFluidAndEnergyFieldsPersistMutations() + { + val holder = ResourceFixture() + holder.items[0].set(ItemStack(Items.DIAMOND, 5)) + holder.tank[0].set(FluidStack.create(Fluids.WATER, FluidStack.bucketAmount())) + holder.energy.insert(400, false) + + val tag = CompoundTag() + holder.saveToTag(tag) + + val loaded = ResourceFixture() + loaded.loadFromTag(tag) + + assertEquals(ItemStack(Items.DIAMOND, 5).item, loaded.items[0].getItem().item) + assertEquals(5, loaded.items[0].getItem().count) + assertEquals(FluidStack.bucketAmount(), loaded.tank[0].getFluid().amount) + assertTrue(loaded.tank[0].getFluid().fluid == Fluids.WATER) { + "Expected loaded tank to still hold water" + } + assertEquals(400L, loaded.energy.getStoredAmount()) + assertEquals(1_000L, loaded.energy.getCapacity()) + succeed() + } + +} diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt new file mode 100644 index 000000000..42ede1ede --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt @@ -0,0 +1,86 @@ +package net.kernelpanicsoft.archie.gametest.internal.tests + +import kotlinx.serialization.builtins.serializer +import net.kernelpanicsoft.archie.gametest.assertEquals +import net.kernelpanicsoft.archie.gametest.assertTrue +import net.kernelpanicsoft.archie.gametest.internal.EMPTY +import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager +import net.minecraft.core.BlockPos +import net.minecraft.gametest.framework.GameTest +import net.minecraft.gametest.framework.GameTestHelper +import net.minecraft.world.level.block.Blocks +import net.minecraft.world.level.block.entity.ChestBlockEntity + +/** + * GameTest coverage for [BlockEntityStateManager]: container identity on repeated registration, + * that dirty containers stay dirty when there are no tracked players to sync to, and that + * unregistering/clearing correctly drops tracked containers. + */ +@Suppress("unused") +class BlockEntityStateManagerTests +{ + @GameTest(template = EMPTY) + fun GameTestHelper.testRegisterReturnsStableContainer() + { + BlockEntityStateManager.clear() + val blockEntity = ChestBlockEntity(BlockPos(1, 2, 3), Blocks.CHEST.defaultBlockState()) + + val first = BlockEntityStateManager.registerBlockEntity(blockEntity) + val second = BlockEntityStateManager.registerBlockEntity(blockEntity) + + assertTrue(first === second) { "Expected the same container instance for repeated registration" } + assertTrue(BlockEntityStateManager.getContainer(blockEntity) != null) { "Expected container to be retrievable" } + BlockEntityStateManager.clear() + succeed() + } + + @GameTest(template = EMPTY) + fun GameTestHelper.testSyncSkipsWithoutTrackedPlayers() + { + BlockEntityStateManager.clear() + val blockEntity = ChestBlockEntity(BlockPos(2, 2, 3), Blocks.CHEST.defaultBlockState()) + val container = BlockEntityStateManager.registerBlockEntity(blockEntity) + container.setPropertySerializer("energy", Int.serializer()) + container.updateProperty("energy", 99) + + var packetsSent = 0 + BlockEntityStateManager.syncDirtyEntities(40L) { _, _ -> packetsSent++ } + + assertEquals(0, packetsSent) + assertTrue(container.isDirty) { "Container should remain dirty until a packet is sent" } + BlockEntityStateManager.clear() + succeed() + } + + @GameTest(template = EMPTY) + fun GameTestHelper.testUnregisterRemovesContainer() + { + BlockEntityStateManager.clear() + val blockEntity = ChestBlockEntity(BlockPos(3, 2, 3), Blocks.CHEST.defaultBlockState()) + + val container = BlockEntityStateManager.registerBlockEntity(blockEntity) + container.setPropertySerializer("progress", Int.serializer()) + container.updateProperty("progress", 3) + + BlockEntityStateManager.unregisterBlockEntity(blockEntity) + assertEquals(null, BlockEntityStateManager.getContainer(blockEntity)) + BlockEntityStateManager.clear() + succeed() + } + + @GameTest(template = EMPTY) + fun GameTestHelper.testClearRemovesAllTrackedEntities() + { + BlockEntityStateManager.clear() + val first = ChestBlockEntity(BlockPos(4, 2, 3), Blocks.CHEST.defaultBlockState()) + val second = ChestBlockEntity(BlockPos(5, 2, 3), Blocks.CHEST.defaultBlockState()) + + BlockEntityStateManager.registerBlockEntity(first) + BlockEntityStateManager.registerBlockEntity(second) + BlockEntityStateManager.clear() + + assertEquals(null, BlockEntityStateManager.getContainer(first)) + assertEquals(null, BlockEntityStateManager.getContainer(second)) + succeed() + } +} diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ComposeRenderingTests.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ComposeRenderingTests.kt new file mode 100644 index 000000000..760248a79 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ComposeRenderingTests.kt @@ -0,0 +1,102 @@ +package net.kernelpanicsoft.archie.gametest.internal.tests + +import androidx.compose.runtime.LaunchedEffect +import net.kernelpanicsoft.archie.gametest.ClientGameTest +import net.kernelpanicsoft.archie.gametest.ClientGameTestContext +import net.kernelpanicsoft.archie.gametest.LayerSelector +import net.kernelpanicsoft.archie.gametest.waitForScreen +import net.kernelpanicsoft.archie.gui.ComposeScreen +import net.kernelpanicsoft.archie.gui.layer.LocalLayerManager +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.MeasurePolicy +import net.kernelpanicsoft.archie.gui.layout.MeasureResult +import net.kernelpanicsoft.archie.gui.modifiers.Constraints +import net.minecraft.network.chat.Component + +private fun fixedSizeMeasurePolicy(width: Int, height: Int): MeasurePolicy = MeasurePolicy { _, _, _ -> + MeasureResult(width, height) { } +} + +/** + * Client GameTest coverage for [ComposeScreen] rendering: that a [Layout] node is measured + * with its policy's reported size, and that a layer pushed via `LocalLayerManager.modal` is + * measured independently on top of the base layer. + */ +@Suppress("unused") +class ComposeRenderingTests { + @ClientGameTest + fun ClientGameTestContext.testComposeScreenMeasuresRenderableNode() { + setScreen { RenderProbeScreen() } + waitForScreen { + waitForLayer(0) { + assertTrue(hasNode(RENDER_PROBE_NAME)) { "Expected render probe node to exist" } + computeOnClient { + rootNode.measure(Constraints(maxWidth = 320, maxHeight = 240)) + } + node(RENDER_PROBE_NAME) { + assertEquals(120, computeOnClient { node.width }) + assertEquals(64, computeOnClient { node.height }) + } + } + } + } + + @ClientGameTest + fun ClientGameTestContext.testComposeScreenPushesModalLayer() { + setScreen { ModalProbeScreen() } + waitForScreen { + waitForLayer(1) { + assertTrue(hasNode(MODAL_PROBE_NAME, LayerSelector.Top)) { "Expected modal probe node to exist" } + computeOnClient { + rootNode.measure(Constraints(maxWidth = 320, maxHeight = 240)) + } + node(MODAL_PROBE_NAME) { + assertEquals(96, computeOnClient { node.width }) + assertEquals(48, computeOnClient { node.height }) + } + } + } + } + + private class RenderProbeScreen : ComposeScreen(Component.literal("Compose Render Probe")) { + override fun init() { + super.init() + start { + Layout( + name = RENDER_PROBE_NAME, + measurePolicy = fixedSizeMeasurePolicy(120, 64), + ) + } + } + } + + private class ModalProbeScreen : ComposeScreen(Component.literal("Compose Modal Probe")) { + override fun init() { + super.init() + start { + Layout( + name = BASE_PROBE_NAME, + measurePolicy = fixedSizeMeasurePolicy(160, 80), + ) + val layerManager = LocalLayerManager.current + LaunchedEffect(Unit) { + layerManager.modal(dismissOnClickOutside = false) { + Layout( + name = MODAL_PROBE_NAME, + measurePolicy = fixedSizeMeasurePolicy(96, 48), + ) + } + } + } + } + } + + companion object { + private const val BASE_PROBE_NAME = "ComposeBaseProbe" + private const val MODAL_PROBE_NAME = "ComposeModalProbe" + private const val RENDER_PROBE_NAME = "ComposeRenderProbe" + } +} + + + diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/InputComponentsGameTest.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/InputComponentsGameTest.kt new file mode 100644 index 000000000..1cdad6e25 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/InputComponentsGameTest.kt @@ -0,0 +1,303 @@ +package net.kernelpanicsoft.archie.gametest.internal.tests + +import androidx.compose.runtime.getValue +import androidx.compose.runtime.mutableStateOf +import androidx.compose.runtime.remember +import androidx.compose.runtime.setValue +import net.kernelpanicsoft.archie.gametest.ClientGameTest +import net.kernelpanicsoft.archie.gametest.ClientGameTestContext +import net.kernelpanicsoft.archie.gametest.waitForScreen +import net.kernelpanicsoft.archie.gui.ComposeScreen +import net.kernelpanicsoft.archie.gui.composables.basic.Text +import net.kernelpanicsoft.archie.gui.composables.input.Button +import net.kernelpanicsoft.archie.gui.composables.input.Checkbox +import net.kernelpanicsoft.archie.gui.composables.input.ColorPicker +import net.kernelpanicsoft.archie.gui.composables.input.RadioGroup +import net.kernelpanicsoft.archie.gui.composables.input.RadioOption +import net.kernelpanicsoft.archie.gui.composables.input.Slider +import net.kernelpanicsoft.archie.gui.composables.input.Switch +import net.kernelpanicsoft.archie.gui.composables.input.textfield.BasicTextField +import net.kernelpanicsoft.archie.gui.composables.containers.Scrollable +import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates +import net.kernelpanicsoft.archie.gui.layout.Arrangement +import net.kernelpanicsoft.archie.gui.layout.Column +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.fillMaxSize +import net.kernelpanicsoft.archie.gui.modifiers.sizeIn +import net.kernelpanicsoft.archie.gui.theme.Theme +import net.kernelpanicsoft.archie.gui.util.HsvColor +import net.kernelpanicsoft.archie.gui.util.KColor +import net.minecraft.network.chat.Component +import org.lwjgl.glfw.GLFW +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +/** + * Client GameTest coverage for every standalone input composable in + * `net.kernelpanicsoft.archie.gui.composables.input` - Checkbox, Switch, RadioGroup, Slider, + * BasicTextField, ColorPicker, Button: hierarchy shape, click/hover/keypress/type input + * handling, and (where the component is texture-state driven) which [TextureStates] key it + * resolves for a given interaction - a texture-correctness check with no pixel comparison. + * + * Runs against [InputComponentsProbeScreen], a plain [ComposeScreen] with no world/menu/player, + * since none of these composables need one - unlike slot/menu rendering, which does (see + * `net.kernelpanicsoft.archie.test.gametest.TestScreenGameTest` in Archie-Test for that case). + */ +@Suppress("unused") +class InputComponentsGameTest { + @ClientGameTest + fun ClientGameTestContext.testHierarchyAndSizing() { + setScreen { InputComponentsProbeScreen() } + waitForScreen { + waitForLayer(0) { + node("Column") { + assertHasDescendant("Checkbox") + assertHasDescendant("Switch") + assertHasDescendant("RadioButton") + assertHasDescendant("Slider") + assertHasDescendant("TextFieldCore") + assertHasDescendant("ColorPicker") + assertHasDescendant("Button") + + // Every RadioGroup option's Row wraps exactly one RadioButton (itself inside + // the Box every Clickable-based composable renders its content in) plus its + // label, in order. + node("Row") { + assertChildNames("Box", "Text") + node("Box") { assertHasDescendant("RadioButton") } + } + + assertAllDescendantsSized() + } + } + } + } + + @ClientGameTest + fun ClientGameTestContext.testCheckboxHoverAndClickRenderState() { + setScreen { InputComponentsProbeScreen() } + waitForScreen { + waitForLayer(0) { + node("Checkbox") { + assertRenderState(TextureStates.DEFAULT) + + hover() + waitForComposeIdle() + assertRenderState(TextureStates.HOVERED) + + click() + waitForComposeIdle() + assertRenderState(TextureStates.CLICKED_AND_HOVERED) { "Expected checkbox to be both checked and hovered right after a click at its own center" } + + click() + waitForComposeIdle() + assertRenderState(TextureStates.HOVERED) { "Expected checkbox to be unchecked again after a second click" } + } + } + } + } + + @ClientGameTest + fun ClientGameTestContext.testSwitchHoverAndClickRenderState() { + setScreen { InputComponentsProbeScreen() } + waitForScreen { + waitForLayer(0) { + node("Switch") { + // Probe's initial `switched = true`. + assertRenderState(TextureStates.CLICKED) + + click() + waitForComposeIdle() + assertRenderState(TextureStates.DEFAULT) { "Expected switch to be off after toggling its initial on state" } + + hover() + waitForComposeIdle() + assertRenderState(TextureStates.HOVERED) + } + } + } + } + + @ClientGameTest + fun ClientGameTestContext.testRadioGroupSelectsOptionExclusively() { + val selected = AtomicReference("alpha") + setScreen { InputComponentsProbeScreen(onRadioSelected = { selected.set(it) }) } + waitForScreen { + waitForLayer(0) { + node("Column") { + val options = nodes("RadioButton") + assertTrue(options.size == 3) { "Expected 3 RadioButton options, found ${options.size}" } + + // Probe's initial `radio = "alpha"` (declaration order: alpha, beta, gamma). + options[0] { + assertRenderState(TextureStates.CLICKED) { "Expected the first (initially selected) radio option to render CLICKED" } + } + options[1] { assertRenderState(TextureStates.DEFAULT) } + options[2] { assertRenderState(TextureStates.DEFAULT) } + + // Select "beta" (options[1]) and verify selection moved there exclusively. + options[1] { click() } + waitForComposeIdle() + + assertEquals("beta", selected.get()) { "Expected clicking the second radio option to select 'beta'" } + options[0] { + assertRenderState(TextureStates.DEFAULT) { "Expected the first option to deselect once a different option is chosen" } + } + options[1] { assertRenderState(TextureStates.CLICKED) } + options[2] { assertRenderState(TextureStates.DEFAULT) } + + // Clicking an already-selected option is a documented no-op (see RadioButtonCore). + options[1] { click() } + waitForComposeIdle() + assertEquals("beta", selected.get()) { "Expected re-clicking the selected option to stay a no-op" } + } + } + } + } + + @ClientGameTest + fun ClientGameTestContext.testSliderHoverAndDragRenderState() { + setScreen { InputComponentsProbeScreen() } + waitForScreen { + waitForLayer(0) { + node("Slider") { + assertRenderState(TextureStates.DEFAULT) + + hover() + waitForComposeIdle() + assertRenderState(TextureStates.HOVERED) + + context.getInput().holdMouse(0) + waitForComposeIdle() + assertRenderState(TextureStates.CLICKED) { "Expected slider to report the dragging (CLICKED) state while the mouse button is held" } + + context.getInput().releaseMouse(0) + waitForComposeIdle() + assertRenderState(TextureStates.HOVERED) { "Expected slider to return to hovered after releasing the drag" } + } + } + } + } + + @ClientGameTest + fun ClientGameTestContext.testButtonClickFiresCallbackAndRenderState() { + val clicked = AtomicBoolean(false) + setScreen { InputComponentsProbeScreen(onButtonClick = { clicked.set(true) }) } + waitForScreen { + waitForLayer(0) { + node("Button") { + assertRenderState(TextureStates.DEFAULT) + + hover() + waitForComposeIdle() + assertRenderState(TextureStates.HOVERED) + + click() + waitForComposeIdle() + + assertTrue(clicked.get()) { "Expected Button's onClick callback to have fired" } + } + } + } + } + + @ClientGameTest + fun ClientGameTestContext.testTextFieldTypeAndBackspace() { + val typed = AtomicReference("") + setScreen { InputComponentsProbeScreen(onTextChanged = { typed.set(it) }) } + waitForScreen { + waitForLayer(0) { + node("TextFieldCore") { + click() + waitForComposeIdle() + + type("hello") + waitForComposeIdle() + assertEquals("hello", typed.get()) { "Expected typed characters to reach onValueChange" } + + pressKey(GLFW.GLFW_KEY_BACKSPACE) + waitForComposeIdle() + assertEquals("hell", typed.get()) { "Expected backspace to remove the last typed character" } + + assertAllDescendantsSized() + } + } + } + } + + @ClientGameTest + fun ClientGameTestContext.testColorPickerInteractionUpdatesColor() { + val initial = HsvColor.from(KColor.CYAN) + val changed = AtomicReference(null) + setScreen { InputComponentsProbeScreen(initialColor = initial, onColorChanged = { changed.set(it) }) } + waitForScreen { + waitForLayer(0) { + node("ColorPicker") { + assertChildNames("SaturationValueArea", "AlphaBar", "HueBar") + assertAllDescendantsSized() + + node("HueBar") { click() } + waitForComposeIdle() + + assertTrue(changed.get() != null) { "Expected clicking the hue bar to report a color change" } + } + } + } + } +} + +private class InputComponentsProbeScreen( + private val initialColor: HsvColor = HsvColor.from(KColor.CYAN), + private val onButtonClick: () -> Unit = {}, + private val onTextChanged: (String) -> Unit = {}, + private val onColorChanged: (HsvColor) -> Unit = {}, + private val onRadioSelected: (String) -> Unit = {}, +) : ComposeScreen(Component.literal("Input Components Probe")) { + override fun init() { + super.init() + start { + Theme { + var checked by remember { mutableStateOf(false) } + var switched by remember { mutableStateOf(true) } + var radio by remember { mutableStateOf("alpha") } + var slider by remember { mutableStateOf(0.35f) } + var text by remember { mutableStateOf("") } + var color by remember { mutableStateOf(initialColor) } + + // Scrollable, since this probe's fixed-height components (ColorPicker etc.) can + // together exceed the actual game window's height depending on display/GUI + // scale - without it, a plain Column silently starves later children of their + // Column-inherited remaining-height budget instead of scrolling. + Scrollable(modifier = Modifier.fillMaxSize()) { + Column(verticalArrangement = Arrangement.spacedBy(4)) { + Checkbox(checked = checked, onCheckedChange = { checked = it }) + Switch(checked = switched, onCheckedChange = { switched = it }) + RadioGroup( + options = listOf( + RadioOption("alpha", Component.literal("Alpha")), + RadioOption("beta", Component.literal("Beta")), + RadioOption("gamma", Component.literal("Gamma")), + ), + selected = radio, + onSelected = { radio = it; onRadioSelected(it) }, + ) + Slider(value = slider, onValueChange = { slider = it }, steps = 10) + BasicTextField( + value = text, + onValueChange = { text = it; onTextChanged(it) }, + modifier = Modifier.sizeIn(minWidth = 120, maxWidth = 180), + ) + ColorPicker( + color = color, + onColorChanged = { color = it; onColorChanged(it) }, + modifier = Modifier.sizeIn(minWidth = 150, minHeight = 90, maxWidth = 170, maxHeight = 100), + ) + Button(onClick = { onButtonClick() }) { + Text(Component.literal("Click me"), dropShadow = false) + } + } + } + } + } + } +} diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/LayoutComponentsGameTest.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/LayoutComponentsGameTest.kt new file mode 100644 index 000000000..49677cc27 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/LayoutComponentsGameTest.kt @@ -0,0 +1,211 @@ +package net.kernelpanicsoft.archie.gametest.internal.tests + +import androidx.compose.runtime.Composable +import net.kernelpanicsoft.archie.gametest.ClientGameTest +import net.kernelpanicsoft.archie.gametest.ClientGameTestContext +import net.kernelpanicsoft.archie.gametest.TestNodeScope +import net.kernelpanicsoft.archie.gametest.waitForScreen +import net.kernelpanicsoft.archie.gui.ComposeScreen +import net.kernelpanicsoft.archie.gui.composables.basic.HorizontalDivider +import net.kernelpanicsoft.archie.gui.composables.basic.Icon +import net.kernelpanicsoft.archie.gui.composables.basic.Text +import net.kernelpanicsoft.archie.gui.composables.containers.Collapsible +import net.kernelpanicsoft.archie.gui.composables.containers.Panel +import net.kernelpanicsoft.archie.gui.composables.containers.Scrollable +import net.kernelpanicsoft.archie.gui.composables.containers.ScrollableState +import net.kernelpanicsoft.archie.gui.composables.containers.TabPanel +import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates +import net.kernelpanicsoft.archie.gui.layout.Arrangement +import net.kernelpanicsoft.archie.gui.layout.Column +import net.kernelpanicsoft.archie.gui.layout.Layout +import net.kernelpanicsoft.archie.gui.layout.MeasureResult +import net.kernelpanicsoft.archie.gui.modifiers.Modifier +import net.kernelpanicsoft.archie.gui.modifiers.height +import net.kernelpanicsoft.archie.gui.modifiers.width +import net.kernelpanicsoft.archie.gui.theme.Theme +import net.minecraft.network.chat.Component +import net.minecraft.resources.ResourceLocation +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Client GameTest coverage for the remaining (non-input) composables: [Panel]/[net.kernelpanicsoft.archie.gui.composables.containers.Surface], + * [HorizontalDivider], [Icon], [Text], [Collapsible], [Scrollable], and [TabPanel]. + * + * Each gets its own small, single-purpose probe screen rather than one shared screen, since + * several of these composables reuse generic container node names ("Row", "Column") - isolating + * them keeps [TestNodeScope.node] lookups unambiguous without needing a unique-name redesign. + */ +@Suppress("unused") +class LayoutComponentsGameTest { + @ClientGameTest + fun ClientGameTestContext.testPanelDividerIconTextHierarchyAndSizing() { + setScreen { PanelDisplayProbeScreen() } + waitForScreen { + waitForLayer(0) { + node("Surface") { + // Panel wraps its content in a padded Box inside the themed Surface. + node("Box") { + assertChildNames("Text", "Spacer", "Texture") + } + assertAllDescendantsSized() + } + } + } + } + + @ClientGameTest + fun ClientGameTestContext.testCollapsibleTogglesContentOnHeaderClick() { + val toggled = AtomicBoolean(false) + setScreen { CollapsibleProbeScreen(initiallyExpanded = false, onToggled = { toggled.set(true) }) } + waitForScreen { + waitForLayer(0) { + node("Column") { + assertTrue(!hasDescendant("CollapsibleContent")) { "Expected content hidden while collapsed" } + + node("Row") { click() } + waitForComposeIdle() + + assertTrue(toggled.get()) { "Expected onToggled to fire on header click" } + assertHasDescendant("CollapsibleContent") + assertAllDescendantsSized() + + node("Row") { click() } + waitForComposeIdle() + assertTrue(!hasDescendant("CollapsibleContent")) { "Expected content hidden again after collapsing" } + } + } + } + } + + @ClientGameTest + fun ClientGameTestContext.testScrollableScrollsContent() { + val scrollState = ScrollableState() + setScreen { ScrollableProbeScreen(scrollState) } + waitForScreen { + waitForLayer(0) { + node("Scrollable") { + assertAllDescendantsSized() + assertEquals(0.0, computeOnClient { scrollState.scrollOffset }) + + scroll(y = -10.0) + waitForComposeIdle() + + val offsetAfterScroll = computeOnClient { scrollState.scrollOffset } + assertTrue(offsetAfterScroll > 0.0) { "Expected scrolling over the Scrollable node to move scrollOffset, got $offsetAfterScroll" } + } + } + } + } + + @ClientGameTest + fun ClientGameTestContext.testTabPanelSwitchesActiveTabOnClick() { + setScreen { TabPanelProbeScreen() } + waitForScreen { + waitForLayer(0) { + node("Column") { + val tabs = nodes("Tab") + assertTrue(tabs.size == 2) { "Expected 2 Tab headers, found ${tabs.size}" } + + assertHasDescendant("FirstTabMarker") + assertTrue(!hasDescendant("SecondTabMarker")) { "Expected only the first tab's content to be composed initially" } + tabs[0] { + assertRenderState(TextureStates.CLICKED) { + "Expected the first (initially active) tab to render CLICKED" + } + } + tabs[1] { click() } + waitForComposeIdle() + + assertHasDescendant("SecondTabMarker") + assertTrue(!hasDescendant("FirstTabMarker")) { "Expected only the second tab's content to be composed after switching" } + tabs[1] { assertRenderState(TextureStates.CLICKED) } + tabs[0] { assertRenderState(TextureStates.DEFAULT) } + } + } + } + } +} + +private class PanelDisplayProbeScreen : ComposeScreen(Component.literal("Panel Display Probe")) { + override fun init() { + super.init() + start { + Theme { + Panel { + Text(Component.literal("Panel content"), dropShadow = false) + HorizontalDivider() + Icon(texture = ResourceLocation.withDefaultNamespace("textures/item/porkchop.png"), size = 16) + } + } + } + } +} + +private class CollapsibleProbeScreen( + private val initiallyExpanded: Boolean, + private val onToggled: (Boolean) -> Unit, +) : ComposeScreen(Component.literal("Collapsible Probe")) { + override fun init() { + super.init() + start { + Theme { + Column { + Collapsible( + title = Component.literal("Section"), + initiallyExpanded = initiallyExpanded, + onToggled = onToggled, + ) { + Text(Component.literal("Collapsible body"), dropShadow = false) + } + } + } + } + } +} + +private class ScrollableProbeScreen( + private val scrollState: ScrollableState, +) : ComposeScreen(Component.literal("Scrollable Probe")) { + override fun init() { + super.init() + start { + Theme { + Scrollable(state = scrollState, modifier = Modifier.height(80).width(120)) { + Column(verticalArrangement = Arrangement.spacedBy(2)) { + repeat(60) { index -> + Text(Component.literal("Row ${index + 1}"), dropShadow = false) + } + } + } + } + } + } +} + +/** A zero-size, invisible node whose mere presence in the tree marks which branch was composed. */ +@Composable +private fun Marker(name: String) { + Layout(name = name, measurePolicy = { _, _, _ -> MeasureResult(0, 0) {} }) +} + +private class TabPanelProbeScreen : ComposeScreen(Component.literal("Tab Panel Probe")) { + override fun init() { + super.init() + start { + Theme { + Column { + TabPanel { + tab("first", Component.literal("First")) { + Marker("FirstTabMarker") + Text(Component.literal("First tab content"), dropShadow = false) + } + tab("second", Component.literal("Second")) { + Marker("SecondTabMarker") + Text(Component.literal("Second tab content"), dropShadow = false) + } + } + } + } + } + } +} diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ModalComponentsGameTest.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ModalComponentsGameTest.kt new file mode 100644 index 000000000..b78e4b820 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ModalComponentsGameTest.kt @@ -0,0 +1,210 @@ +package net.kernelpanicsoft.archie.gametest.internal.tests + +import net.kernelpanicsoft.archie.gametest.ClientGameTest +import net.kernelpanicsoft.archie.gametest.ClientGameTestContext +import net.kernelpanicsoft.archie.gametest.LayerSelector +import net.kernelpanicsoft.archie.gametest.waitForScreen +import net.kernelpanicsoft.archie.gui.ComposeScreen +import net.kernelpanicsoft.archie.gui.composables.basic.Text +import net.kernelpanicsoft.archie.gui.composables.input.Button +import net.kernelpanicsoft.archie.gui.composables.modal.ModalChoice +import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates +import net.kernelpanicsoft.archie.gui.layer.LocalLayerManager +import net.kernelpanicsoft.archie.gui.layout.Column +import net.minecraft.network.chat.Component +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.atomic.AtomicReference + +/** + * Client GameTest coverage for the built-in modal dialogs (`net.kernelpanicsoft.archie.gui.composables.modal`): + * [net.kernelpanicsoft.archie.gui.composables.modal.AlertDialog], + * [net.kernelpanicsoft.archie.gui.composables.modal.PromptDialog], + * [net.kernelpanicsoft.archie.gui.composables.modal.ChoiceDialog], and + * [net.kernelpanicsoft.archie.gui.composables.modal.ConfirmDialog] - modal-layer hierarchy, + * typing into a prompt, disabled-button texture state, and confirm/cancel/dismiss flows. + * + * Runs against [ModalComponentsProbeScreen], a plain [ComposeScreen] (a [net.kernelpanicsoft.archie.gui.LayerManagerProvider] + * on its own, same as [net.kernelpanicsoft.archie.gui.ComposeContainerScreen]), so no world, + * menu, or player is needed. `net.kernelpanicsoft.archie.test.gametest.TestScreenGameTest` in + * Archie-Test already covers a [net.kernelpanicsoft.archie.gui.composables.modal.ConfirmDialog] + * end to end against the real block-entity-backed screen; this file exercises it (plus the other + * three dialog kinds) at the lightweight-probe level instead of duplicating that coverage. + */ +@Suppress("unused") +class ModalComponentsGameTest { + @ClientGameTest + fun ClientGameTestContext.testAlertDialogConfirmDismisses() { + val confirmed = AtomicBoolean(false) + setScreen { ModalComponentsProbeScreen(onAlertConfirm = { confirmed.set(true) }) } + waitForScreen { + assertEquals(1, layerCount) + val triggers = baseLayer.rootNode { nodes("Button") } + triggers[0] { click() } // "Open Alert" + waitFor { _ -> layerCount == 2 } + + node("Surface", layer = LayerSelector.Top) { + val buttons = nodes("Button") + assertTrue(buttons.size == 1) { "Expected AlertDialog to have exactly 1 action button, found ${buttons.size}" } + buttons[0] { click() } + } + + waitFor { _ -> layerCount == 1 } + assertTrue(confirmed.get()) { "Expected AlertDialog's onConfirm to fire" } + } + } + + @ClientGameTest + fun ClientGameTestContext.testPromptDialogValidatorDisablesConfirmUntilTyped() { + val confirmedValue = AtomicReference(null) + setScreen { ModalComponentsProbeScreen(onPromptConfirm = { confirmedValue.set(it) }) } + waitForScreen { + val triggers = baseLayer.rootNode { nodes("Button") } + triggers[1] { click() } // "Open Prompt" + waitFor { _ -> layerCount == 2 } + + node("Surface", layer = LayerSelector.Top) { + assertHasDescendant("TextFieldCore") + val buttons = nodes("Button") + assertTrue(buttons.size == 2) { "Expected PromptDialog to have 2 action buttons (cancel, confirm), found ${buttons.size}" } + val confirmButton = buttons[1] + + // Empty initial value fails the `it.isNotBlank()` validator - confirm starts disabled. + confirmButton { + assertRenderState(TextureStates.DISABLED) { "Expected the confirm button to render DISABLED while the field is blank" } + } + + node("TextFieldCore") { click(); type("hello") } + waitForComposeIdle() + + confirmButton { + assertTrue(renderState != TextureStates.DISABLED) { "Expected the confirm button to no longer be disabled once the field has text" } + } + + confirmButton { click() } + } + + waitFor { _ -> layerCount == 1 } + assertEquals("hello", confirmedValue.get()) { "Expected PromptDialog's onConfirm to report the typed value" } + } + } + + @ClientGameTest + fun ClientGameTestContext.testChoiceDialogSelectsEnabledOptionAndSkipsDisabled() { + val selected = AtomicReference(null) + setScreen { ModalComponentsProbeScreen(onChoiceSelected = { selected.set(it) }) } + waitForScreen { + val triggers = baseLayer.rootNode { nodes("Button") } + triggers[2] { click() } // "Open Choice" + waitFor { _ -> layerCount == 2 } + waitForComposeIdle() + + node("Surface", layer = LayerSelector.Top) { + val buttons = nodes("Button") + // 3 choices ("alpha", "beta", disabled "locked") + 1 cancel button, in that order. + assertTrue(buttons.size == 4) { "Expected ChoiceDialog to have 4 buttons (3 choices + cancel), found ${buttons.size}" } + + buttons[2] { + assertRenderState(TextureStates.DISABLED) { "Expected the disabled 'locked' choice to render DISABLED" } + } + + buttons[1] { click() } // "beta" + } + + waitFor { _ -> layerCount == 1 } + assertEquals("beta", selected.get()) { "Expected ChoiceDialog's onSelected to report the clicked choice" } + } + } + + @ClientGameTest + fun ClientGameTestContext.testConfirmDialogConfirmAndCancelFlows() { + val confirmed = AtomicBoolean(false) + val cancelled = AtomicBoolean(false) + setScreen { + ModalComponentsProbeScreen( + onConfirmDialogConfirm = { confirmed.set(true) }, + onConfirmDialogCancel = { cancelled.set(true) }, + ) + } + waitForScreen { + val triggers = baseLayer.rootNode { nodes("Button") } + + // Confirm path. + triggers[3] { click() } // "Open Confirm" + waitFor { _ -> layerCount == 2 } + node("Surface", layer = LayerSelector.Top) { + val buttons = nodes("Button") + assertTrue(buttons.size == 2) { "Expected ConfirmDialog to have 2 action buttons (confirm, cancel), found ${buttons.size}" } + buttons[0] { click() } // confirm is first, see ConfirmDialog.kt + } + // ConfirmDialog's dismiss is deferred behind a close animation (see DIALOG_ANIMATION_MS), + // so unlike the other three dialogs this can't rely on waitForComposeIdle alone. + waitFor { _ -> layerCount == 1 } + assertTrue(confirmed.get()) { "Expected ConfirmDialog's onConfirm to fire" } + + // Cancel path. + triggers[3] { click() } // "Open Confirm" again + waitFor { _ -> layerCount == 2 } + node("Surface", layer = LayerSelector.Top) { + val buttons = nodes("Button") + buttons[1] { click() } // cancel is second + } + waitFor { _ -> layerCount == 1 } + assertTrue(cancelled.get()) { "Expected ConfirmDialog's onCancel to fire" } + } + } +} + +private class ModalComponentsProbeScreen( + private val onAlertConfirm: () -> Unit = {}, + private val onPromptConfirm: (String) -> Unit = {}, + private val onChoiceSelected: (String) -> Unit = {}, + private val onConfirmDialogConfirm: () -> Unit = {}, + private val onConfirmDialogCancel: () -> Unit = {}, +) : ComposeScreen(Component.literal("Modal Components Probe")) { + override fun init() { + super.init() + start { + val layers = LocalLayerManager.current + Column { + Button(onClick = { + layers.alertDialog( + title = Component.literal("Alert"), + message = Component.literal("Something happened."), + onConfirm = onAlertConfirm, + ) + }) { Text(Component.literal("Open Alert"), dropShadow = false) } + + Button(onClick = { + layers.promptDialog( + title = Component.literal("Prompt"), + initialValue = "", + validator = { it.isNotBlank() }, + onConfirm = onPromptConfirm, + ) + }) { Text(Component.literal("Open Prompt"), dropShadow = false) } + + Button(onClick = { + layers.choiceDialog( + title = Component.literal("Choice"), + choices = listOf( + ModalChoice("alpha", Component.literal("Alpha")), + ModalChoice("beta", Component.literal("Beta")), + ModalChoice("locked", Component.literal("Locked"), enabled = false), + ), + onSelected = onChoiceSelected, + ) + }) { Text(Component.literal("Open Choice"), dropShadow = false) } + + Button(onClick = { + layers.confirmDialog( + title = Component.literal("Confirm"), + onConfirm = onConfirmDialogConfirm, + onCancel = onConfirmDialogCancel, + ) { + Text(Component.literal("Are you sure?"), dropShadow = false) + } + }) { Text(Component.literal("Open Confirm"), dropShadow = false) } + } + } + } +} diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt new file mode 100644 index 000000000..63dc0d8d8 --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt @@ -0,0 +1,327 @@ +package net.kernelpanicsoft.archie.gametest.junit + +import java.nio.channels.FileChannel +import java.nio.file.Path +import java.nio.file.StandardOpenOption +import java.time.Duration +import java.util.concurrent.CompletableFuture +import java.util.concurrent.CompletionException +import java.util.concurrent.ConcurrentHashMap +import java.util.concurrent.Executors +import java.util.concurrent.TimeUnit +import java.util.concurrent.atomic.AtomicBoolean +import java.util.concurrent.locks.ReentrantLock +import kotlin.io.path.appendText +import kotlin.io.path.createDirectories +import kotlin.io.path.exists +import kotlin.io.path.outputStream +import kotlin.io.path.readLines + +/** One test's pass/fail outcome, parsed from a GameTest invocation's log output. */ +internal data class TestResult( + val testId: String, + val passed: Boolean, +) + +/** The outcome of one [GameTestGradleExecutor.start] invocation, including per-test results parsed from its log. */ +internal data class GameTestGradleResult( + val success: Boolean, + val command: List, + val exitCode: Int, + val logFile: Path, + val logTail: String, + val testResults: Map = emptyMap(), // testId -> TestResult +) + +/** + * A [GameTestGradleInvocation]'s Gradle process, already running by the time this is returned. + * `liveTestResults` is populated live as PASS/FAIL lines are parsed from the process's output, so + * a caller can report an individual test as soon as it's known instead of waiting for [result] + * (the invocation's overall outcome) to complete. + */ +internal class GameTestGradleHandle( + val invocation: GameTestGradleInvocation, + private val liveTestResults: ConcurrentHashMap, + val result: CompletableFuture, +) { + /** + * Blocks until [testId] has been observed in the live log output, or [result] completes - + * whichever happens first. Returns `null` if the invocation finished without ever reporting + * a result for [testId] (e.g. it wasn't reached before a crash/timeout). + */ + fun awaitTestResult(testId: String, pollInterval: Duration = Duration.ofMillis(200)): TestResult? { + while (!result.isDone) { + liveTestResults[testId]?.let { return it } + Thread.sleep(pollInterval.toMillis()) + } + return liveTestResults[testId] + } +} + +/** Shells out to the Gradle wrapper to run one [GameTestGradleInvocation], capturing and parsing its log output. */ +internal object GameTestGradleExecutor { + /** Backs every in-flight invocation's blocking wait-for-exit; sized generously since these are I/O-bound, not CPU-bound. */ + private val ioExecutor = Executors.newCachedThreadPool { runnable -> + Thread(runnable, "archie-gametest-runner").apply { isDaemon = true } + } + + /** + * Every invocation is a separate --no-daemon Gradle process, and (at least for Archie-Test) + * they all depend on the same upstream composite-build artifact (e.g. Archie:common's + * remapped jar) - starting all of them at once races multiple processes rebuilding/rewriting + * that shared output concurrently, corrupting it (observed: `:Archie:common:remapJar FAILED + * ... ZipException: invalid stored block lengths`). Whichever invocation calls [start] first + * claims [primingClaimed] and runs a single, fast `assemble` build to force those shared + * outputs to exist once; everyone else blocks on [primingComplete] until that finishes. Once + * it's done, Gradle's up-to-date checks mean every invocation's own process only reads the + * already-built artifact, so all of them - including the priming one - start their real + * (long-running) invocation concurrently right after, instead of one invocation blocking + * every other one on its entire run. + * + * This alone only serializes invocations launched from the same JVM. `Archie`'s and + * `Archie-Test`'s own `:*:test` tasks each run in a *separate* Gradle test JVM, but + * Archie-Test composite-includes `../Archie` (see its settings.gradle.kts), so both JVMs' + * priming runs `assemble` against the very same `Archie:common` build output concurrently - + * this in-process guard does nothing across that boundary (observed: + * `:common:remapJar FAILED ... NoSuchFileException: archie-common-1.0.0.jar.tmp`, one + * process's remap temp file vanishing out from under the other). [withCrossProcessPrimingLock] + * closes that gap with an OS-level file lock shared by both JVMs. + */ + private val primingClaimed = AtomicBoolean(false) + private val primingComplete = CompletableFuture() + + /** + * Starts [invocation]'s Gradle task via `ProcessBuilder` and returns immediately with a + * [GameTestGradleHandle] tracking it - the process itself, and the background threads + * streaming its output, are already running. This lets a caller [start] every matrix entry + * up front so independent invocations run concurrently, instead of only starting the next + * one once a prior invocation's JUnit node happens to execute. + */ + fun start( + invocation: GameTestGradleInvocation, + timeout: Duration, + workspaceRoot: Path, + ): GameTestGradleHandle { + if (primingClaimed.compareAndSet(false, true)) { + runCatching { primeSharedBuildOutputs(workspaceRoot) } + .onSuccess { primingComplete.complete(null) } + .onFailure { primingComplete.completeExceptionally(it) } + .getOrThrow() + } else { + try { + primingComplete.join() + } catch (e: CompletionException) { + throw IllegalStateException("Shared build output priming failed; see cause", e.cause ?: e) + } + } + + val liveTestResults = ConcurrentHashMap() + val result = CompletableFuture.supplyAsync( + { withLoaderRunLock(workspaceRoot, invocation.loader) { runProcess(invocation, timeout, workspaceRoot, liveTestResults) } }, + ioExecutor, + ) + + return GameTestGradleHandle(invocation, liveTestResults, result) + } + + /** + * Runs a single, fast `assemble` (compiles and packages every subproject, including + * composite-included ones, without launching anything) so the shared upstream artifacts every + * matrix invocation depends on exist before any of them starts its own (much longer) process. + * Deliberately generic (not a hardcoded task path) so it works the same for both Archie's and + * Archie-Test's workspace roots. + * + * Wrapped in [withCrossProcessPrimingLock] since [primingClaimed] only guards against races + * within this JVM - see its doc comment. + */ + private fun primeSharedBuildOutputs(workspaceRoot: Path) { + withCrossProcessPrimingLock { + val logsDir = workspaceRoot.resolve("build/tmp/junit-gametest-runner").createDirectories() + val logFile = logsDir.resolve("priming.log") + + val wrapper = resolveGradleWrapper(workspaceRoot) + val command = listOf(wrapper.toString(), "assemble", "--console=plain", "--no-daemon") + + val process = ProcessBuilder(command) + .directory(workspaceRoot.toFile()) + .redirectErrorStream(true) + .start() + + logFile.outputStream().bufferedWriter().use { writer -> + process.inputStream.bufferedReader().useLines { lines -> + lines.forEach { line -> + println(line) + writer.appendLine(line) + } + } + } + + val exitCode = process.waitFor() + check(exitCode == 0) { + "Priming build ('${command.joinToString(" ")}') failed with exit code $exitCode. Log file: $logFile\n--- Log tail ---\n${tail(logFile)}" + } + } + } + + /** + * Runs [action] while holding an OS-level advisory lock on a fixed file under the system + * temp directory, blocking until it's acquired. `Archie`'s and `Archie-Test`'s `:*:test` + * tasks each spawn their own JVM (this object's in-process guards don't share state between + * them), but both machines' priming runs ultimately `assemble` the same physical + * `Archie:common` build output when run on the same machine (Archie-Test composite-includes + * `../Archie`) - this lock is what actually serializes them. Scoped to the whole machine + * rather than a specific workspace path since that's simpler and there's only ever one such + * priming race to guard against per machine (CI runner or dev box). + */ + private fun withCrossProcessPrimingLock(action: () -> T): T { + val lockFile = Path.of(System.getProperty("java.io.tmpdir"), "archie-gametest-priming.lock") + FileChannel.open(lockFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE).use { channel -> + channel.lock().use { + return action() + } + } + } + + /** Per-(workspaceRoot, loader) in-JVM locks backing [withLoaderRunLock]. */ + private val loaderRunLocks = ConcurrentHashMap, ReentrantLock>() + + /** + * Runs [action] while holding an in-JVM lock scoped to [workspaceRoot] and [loader], blocking + * (whichever `ioExecutor` thread is running the invocation, never the caller of [start]) until + * it's acquired. + * + * Every invocation for one workspaceRoot is spawned as a child --no-daemon Gradle process from + * the same `:common:test` JVM, so a plain in-JVM lock is enough here - unlike + * [withCrossProcessPrimingLock], which guards a race between *separate* JVMs (Archie's and + * Archie-Test's own `:*:test` tasks) and needs an OS-level file lock. (A `FileChannel` lock + * would be wrong here for a different reason too: `java.nio.channels.FileLock` throws + * `OverlappingFileLockException` rather than blocking when a *second* lock on the same file + * is requested from within the same JVM - it's designed to guard against other processes, not + * queue other threads in this one.) + * + * Unlike [withCrossProcessPrimingLock]'s one-time shared-artifact priming, this serializes + * the *actual* invocation runs for the same loader (e.g. fabric:server and fabric:client), + * which both depend on and mutate that loader subproject's own build outputs + * (`:fabric:processResources` etc.) via their own separate, concurrently-launched + * --no-daemon Gradle processes. Without this, one invocation's spawned Minecraft process can + * read e.g. `fabric.mod.json` straight off disk at the exact moment the other invocation's + * own build is mid-rewrite of that same file - observed as a `ParseMetadataException: + * ... EOFException` from a momentarily-empty `fabric.mod.json`, cascading into every + * unrelated server test in that suite reporting a spurious failure. Different loaders (and + * different workspace roots) get different locks, so fabric and neoforge invocations - and + * Archie's vs Archie-Test's own invocations - still run fully in parallel. + */ + private fun withLoaderRunLock(workspaceRoot: Path, loader: Loader, action: () -> T): T { + val lock = loaderRunLocks.computeIfAbsent(workspaceRoot to loader) { ReentrantLock() } + lock.lock() + try { + return action() + } finally { + lock.unlock() + } + } + + private fun runProcess( + invocation: GameTestGradleInvocation, + timeout: Duration, + workspaceRoot: Path, + liveTestResults: ConcurrentHashMap, + ): GameTestGradleResult { + val logsDir = workspaceRoot.resolve("build/tmp/junit-gametest-runner").createDirectories() + val logFile = logsDir.resolve("${invocation.id.replace(':', '-')}.log") + + val wrapper = resolveGradleWrapper(workspaceRoot) + val command = mutableListOf(wrapper.toString()) + command += invocation.taskPath + command += "--console=plain" + command += "--no-daemon" + + val extraArgs = System.getProperty("archie.junit.gametest.extraArgs")?.trim().orEmpty() + if (extraArgs.isNotEmpty()) { + command.addAll(extraArgs.split(Regex("\\s+"))) + } + + val process = ProcessBuilder(command) + .directory(workspaceRoot.toFile()) + .redirectErrorStream(true) + .start() + + val testPattern = when (invocation.side) { + Side.SERVER -> Regex("""\[GameTest] (PASS|FAIL) (.+)""") + Side.CLIENT -> Regex("""\[ClientGameTest] (PASS|FAIL) (.+)""") + } + + val outputPump = Thread { + logFile.outputStream().bufferedWriter().use { writer -> + process.inputStream.bufferedReader().useLines { lines -> + lines.forEach { line -> + println(line) + writer.appendLine(line) + writer.flush() + + val match = testPattern.find(line) + if (match != null) { + val passed = match.groupValues[1] == "PASS" + val testId = match.groupValues[2].trim() + liveTestResults[testId] = TestResult(testId, passed) + } + } + } + } + }.apply { + name = "archie-gametest-output-${invocation.id}" + isDaemon = true + start() + } + + return awaitCompletion(process, outputPump, timeout, logFile, command, liveTestResults) + } + + private fun awaitCompletion( + process: Process, + outputPump: Thread, + timeout: Duration, + logFile: Path, + command: List, + liveTestResults: Map, + ): GameTestGradleResult { + val finished = process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS) + val exitCode = if (finished) process.exitValue() else { + process.destroyForcibly() + process.waitFor() + -1 + } + + outputPump.join(5_000) + + if (!finished) { + logFile.appendText("\n[runner] Timed out after ${timeout.toMinutes()} minute(s).\n") + } + + val tail = tail(logFile) + return GameTestGradleResult( + success = finished && exitCode == 0, + command = command, + exitCode = exitCode, + logFile = logFile, + logTail = tail, + testResults = liveTestResults.toMap(), + ) + } + + private fun resolveGradleWrapper(workspaceRoot: Path): Path { + val unix = workspaceRoot.resolve("gradlew") + if (unix.exists()) return unix + + val windows = workspaceRoot.resolve("gradlew.bat") + if (windows.exists()) return windows + + error("Could not locate Gradle wrapper in $workspaceRoot") + } + + private fun tail(file: Path): String { + if (!file.exists()) return "" + val lines = file.readLines() + return lines.takeLast(120).joinToString("\n") + } +} diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt new file mode 100644 index 000000000..aedb7de4f --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt @@ -0,0 +1,60 @@ +package net.kernelpanicsoft.archie.gametest.junit + +/** A mod loader [GameTestRunner] can launch a GameTest Gradle task for. */ +enum class Loader { + FABRIC, + NEOFORGE, +} + +/** The GameTest side (matches [net.kernelpanicsoft.archie.gametest.AGameTestSide]) to launch. */ +enum class Side { + SERVER, + CLIENT, +} + +/** One `loader:side` entry from [GameTestRunner]'s matrix, resolving to a single Gradle [taskPath] to run. */ +data class GameTestGradleInvocation( + val loader: Loader, + val side: Side, +) { + /** The fully-qualified Gradle task path that launches this invocation, e.g. `:fabric:runGametest`. */ + val taskPath: String + get() = when (loader) { + Loader.FABRIC -> when (side) { + Side.SERVER -> ":fabric:runGametest" + Side.CLIENT -> ":fabric:runGametestClient" + } + + Loader.NEOFORGE -> when (side) { + Side.SERVER -> ":neoforge:runGametest" + Side.CLIENT -> ":neoforge:runGametestClient" + } + } + + /** A short id for this invocation, e.g. `"fabric:server"`, used in test/container display names. */ + val id: String + get() = "${loader.name.lowercase()}:${side.name.lowercase()}" + + companion object { + /** + * Parses a comma-separated list of `loader:side` tokens (e.g. `"fabric:server,neoforge:client"`) + * into invocations, as used by [GameTestRunner.PROP_MATRIX]. + * + * @throws IllegalArgumentException if a token isn't in `loader:side` form or names an unknown [Loader]/[Side]. + */ + fun parseMatrix(value: String): List { + if (value.isBlank()) return emptyList() + return value.split(',').map { token -> + val parts = token.trim().split(':') + require(parts.size == 2) { + "Invalid matrix token '$token'. Expected format :, e.g. fabric:server" + } + GameTestGradleInvocation( + loader = Loader.valueOf(parts[0].trim().uppercase()), + side = Side.valueOf(parts[1].trim().uppercase()), + ) + } + } + } +} + diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt new file mode 100644 index 000000000..75781b05f --- /dev/null +++ b/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt @@ -0,0 +1,159 @@ +package net.kernelpanicsoft.archie.gametest.junit + +import net.kernelpanicsoft.archie.events.AGametestEvents +import net.kernelpanicsoft.archie.gametest.ClientGameTest +import net.minecraft.gametest.framework.GameTest +import org.junit.jupiter.api.Assumptions.assumeTrue +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.DynamicTest +import java.net.URI +import java.nio.file.Path +import java.time.Duration +import kotlin.collections.forEach + +/** + * Bridges Archie's Loom-driven GameTests into a regular JUnit 5 run, so `./gradlew test` (or an + * IDE test runner) can launch `runGametest`/`runGametestClient` for a `loader:side` matrix and + * report each declared test as its own JUnit [DynamicTest], parsed from the launched process's + * log output. + * + * Disabled by default (opt-in via [PROP_ENABLED]) since it shells out to Gradle and boots a full + * Minecraft process per matrix entry. + */ +object GameTestRunner +{ + /** System property (`-D...=true`) that must be set to enable [tests]; otherwise it reports a single skipped test. */ + const val PROP_ENABLED = "archie.junit.gametest" + /** System property overriding [DEFAULT_MATRIX], a comma-separated list of `loader:side` pairs to launch. */ + const val PROP_MATRIX = "archie.junit.gametest.matrix" + /** System property overriding the per-invocation timeout, in minutes (default 20, minimum 1). */ + const val PROP_TIMEOUT_MINUTES = "archie.junit.gametest.timeoutMinutes" + /** System property overriding the auto-detected workspace root (the directory containing `settings.gradle.kts`). */ + const val PROP_WORKSPACE_ROOT = "archie.junit.gametest.root" + + /** The default `loader:side` matrix launched by [tests] when [PROP_MATRIX] isn't set. */ + const val DEFAULT_MATRIX = "fabric:server,fabric:client,neoforge:server,neoforge:client" + + /** + * Builds the JUnit dynamic test tree for [modID]: one container per `loader:side` invocation + * in the configured matrix, each running the loader's GameTest Gradle task once and then + * reporting one [DynamicTest] per test method declared via [tests] (an + * [AGametestEvents.ArchieGameTestBuilder] receiver, same DSL as [AGametestEvents.REGISTER_GAME_TEST]) whose + * side matches that invocation. + */ + fun tests(modID: String, tests: AGametestEvents.ArchieGameTestBuilder.() -> Unit): Collection + { + val enabled = System.getProperty(PROP_ENABLED)?.toBooleanStrictOrNull() == true + if (!enabled) { + return listOf( + DynamicContainer.dynamicContainer("gametest:disabled", mutableListOf(DynamicTest.dynamicTest("GameTest runner is disabled") { + assumeTrue(false) { + "GameTest runner is disabled. Set -D$PROP_ENABLED=true to launch Loom gametest tasks from JUnit." + } + })) + ) + } + + val matrixValue = System.getProperty(PROP_MATRIX) ?: DEFAULT_MATRIX + val invocations = GameTestGradleInvocation.parseMatrix(matrixValue) + require(invocations.isNotEmpty()) { + "No GameTest invocations configured. Set -D$PROP_MATRIX with at least one loader:side pair." + } + + val timeout = Duration.ofMinutes((System.getProperty(PROP_TIMEOUT_MINUTES)?.toLongOrNull() ?: 20L).coerceAtLeast(1L)) + val root = resolveWorkspaceRoot() + + val handleLazies = invocations.associateWith { invocation -> + lazy(LazyThreadSafetyMode.SYNCHRONIZED) { GameTestGradleExecutor.start(invocation, timeout, root) } + } + + val containers = mutableListOf() + + invocations.forEach { invocation -> + val handleLazy = handleLazies.getValue(invocation) + containers.add(DynamicContainer.dynamicContainer(invocation.id, buildList { + val invocationTestName = "GameTest Invocation [${invocation.id}]" + val invocationTest = DynamicTest.dynamicTest(invocationTestName) { + val result = handleLazy.value.result.get() + + // Check if the invocation itself succeeded (exit code 0) + if (!result.success) { + val message = buildString { + append("GameTest invocation failed: $invocationTestName\n") + append("Exit code: ${result.exitCode}\n") + append("Command: ${result.command.joinToString(" ")}\n") + append("Log file: ${result.logFile}\n") + append("--- Log tail ---\n") + append(result.logTail) + } + throw AssertionError(message) + } + } + add(invocationTest) + AGametestEvents.ArchieGameTestBuilder(true).apply(tests).classes.forEach { clazz -> + val classUri = URI.create("class:${clazz.name}") + add(DynamicContainer.dynamicContainer(clazz.simpleName, classUri, clazz.declaredMethods.flatMap { method -> + val hasGameTest = method.getAnnotationsByType(GameTest::class.java).isNotEmpty() + val hasClientGameTest = method.getAnnotationsByType(ClientGameTest::class.java).isNotEmpty() + val tests = mutableListOf() + if ((hasGameTest && invocation.side == Side.SERVER) || (hasClientGameTest && invocation.side == Side.CLIENT)) { + val id = "$modID:${clazz.simpleName.lowercase()}.${method.name.lowercase()}" + val displayName = "${method.name}" + val testName = "$displayName [${invocation.id}]" + val methodUri = URI.create("method:${clazz.name}#${method.name}") + val test = DynamicTest.dynamicTest(testName, methodUri) { + val testResult = handleLazy.value.awaitTestResult(id) + if (testResult != null && !testResult.passed) { + val result = handleLazy.value.result.get() + val message = buildString { + append("GameTest failed: $testName\n") + append("Exit code: ${result.exitCode}\n") + append("Command: ${result.command.joinToString(" ")}\n") + append("Log file: ${result.logFile}\n") + append("--- Log tail ---\n") + append(result.logTail) + } + throw AssertionError(message) + } else if (testResult == null) { + // Test wasn't found in the log output + val result = handleLazy.value.result.get() + if (!result.success) { + // Invocation failed entirely, report that + val message = buildString { + append("GameTest invocation failed: $testName\n") + append("Exit code: ${result.exitCode}\n") + append("Command: ${result.command.joinToString(" ")}\n") + append("Log file: ${result.logFile}\n") + append("--- Log tail ---\n") + append(result.logTail) + } + throw AssertionError(message) + } + // Otherwise treat as passed if test ID wasn't found (test might not have run) + } + } + tests.add(test) + } + tests + }.stream())) + } + })) + } + + return containers + } + + private fun resolveWorkspaceRoot(): Path { + val explicit = System.getProperty(PROP_WORKSPACE_ROOT)?.trim().orEmpty() + if (explicit.isNotEmpty()) return Path.of(explicit) + + var cursor = Path.of("").toAbsolutePath() + repeat(8) { + if (cursor.resolve("settings.gradle.kts").toFile().exists()) return cursor + cursor = cursor.parent ?: return@repeat + } + error("Unable to locate workspace root from ${Path.of("").toAbsolutePath()}. Set -D$PROP_WORKSPACE_ROOT=/path/to/Archie") + } + + +} \ No newline at end of file diff --git a/Archie-Core/gametest/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension b/Archie-Core/gametest/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension new file mode 100644 index 000000000..d945124ce --- /dev/null +++ b/Archie-Core/gametest/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension @@ -0,0 +1 @@ +net.kernelpanicsoft.archie.gametest.internal.GametestArchieExtension diff --git a/Archie-Core/gametest/fabric/build.gradle.kts b/Archie-Core/gametest/fabric/build.gradle.kts new file mode 100644 index 000000000..bc6284cd4 --- /dev/null +++ b/Archie-Core/gametest/fabric/build.gradle.kts @@ -0,0 +1,103 @@ +plugins { + alias(libs.plugins.archie) +} + +architectury { + platformSetupLoomIde() + fabric() +} + +actualizer { + actualizes(project(":archie-gametest-common")) +} + +configurations { + create("common") + compileClasspath.get().extendsFrom(configurations["common"]) + runtimeClasspath.get().extendsFrom(configurations["common"]) + testCompileClasspath.get().extendsFrom(compileClasspath.get()) + testRuntimeClasspath.get().extendsFrom(runtimeClasspath.get()) +} + +loom { + accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) + + mods { + maybeCreate("main").apply { + sourceSet(sourceSets.main.get()) + } + } + + runs { + getByName("client") { + name = "Minecraft Client" + source(sourceSets.main.get()) + vmArg("-XX:+AllowEnhancedClassRedefinition") + } + getByName("server") { + name = "Minecraft Server" + source(sourceSets.main.get()) + vmArgs("-XX:+AllowEnhancedClassRedefinition") + } + create("gametest") { + server() + name = "Minecraft GameTest" + property("fabric-api.gametest") + property("archie.gametest", "true") + property("archie.gametest.side", "server") + property("archie.gametest.modid", "archie_gametest") + } + create("gametestClient") { + client() + name = "Minecraft GameTest Client" + property("fabric-api.gametest") + property("archie.gametest", "true") + property("archie.gametest.side", "client") + property("archie.gametest.modid", "archie_gametest") + } + } +} + +dependencies { + modImplementation(libs.fabric.loader) + modApi(libs.fabric.api) + modImplementation(libs.kotlin.fabric) + compileOnly(libs.kotlinx.serialization) + + implementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.api) + testRuntimeOnly(libs.junit.jupiter.engine) + + "common"(project(":archie-gametest-common", "namedElements")) { isTransitive = false } + modApi(project(":archie-core-fabric")) +} + +modResources { + filesMatching.add("fabric.mod.json") +} + +tasks { + base.archivesName.set(base.archivesName.get() + "-gametest-fabric") + + test { + useJUnitPlatform() + } + + processResources { + dependsOn(processTestResources) + } + + processTestResources { + } + + classes { + finalizedBy(testClasses) + } + + sourcesJar { + val commonSources = project(":archie-gametest-common").tasks.sourcesJar + dependsOn(commonSources) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(commonSources.get().archiveFile.map { zipTree(it) }) + } +} diff --git a/Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestHelperMixin.java b/Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestHelperMixin.java new file mode 100644 index 000000000..ab1474f24 --- /dev/null +++ b/Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestHelperMixin.java @@ -0,0 +1,16 @@ +package net.kernelpanicsoft.archie.mixin.fabric; + +import net.fabricmc.fabric.impl.gametest.FabricGameTestHelper; +import net.kernelpanicsoft.archie.gametest.AGameTestRegistrationBridge; +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(FabricGameTestHelper.class) +public class FabricGameTestHelperMixin { + @Inject(method = "runHeadlessServer(Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;Lnet/minecraft/server/packs/repository/PackRepository;)V", at = @At("HEAD")) + private static void runHeadlessServer(CallbackInfo ci) { + AGameTestRegistrationBridge.registerGameTests(); + } +} diff --git a/Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java b/Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java new file mode 100644 index 000000000..f2b750a74 --- /dev/null +++ b/Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java @@ -0,0 +1,27 @@ +package net.kernelpanicsoft.archie.mixin.fabric; + +import net.fabricmc.fabric.impl.gametest.FabricGameTestModInitializer; +import net.kernelpanicsoft.archie.Archie; +import net.kernelpanicsoft.archie.gametest.AGameTestPlatform; +import net.kernelpanicsoft.archie.gametest.VerboseTestReporter; +import net.minecraft.gametest.framework.GlobalTestReporter; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Accessor; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; + +import java.util.Map; + +@SuppressWarnings("UnstableApiUsage") +@Mixin(FabricGameTestModInitializer.class) +public interface FabricGameTestModInitializerMixin +{ + + @Accessor("GAME_TEST_IDS") + static Map, String> getGameTestIds() { return null; } + + + @Accessor("LOGGER") + static org.slf4j.Logger getLogger() { return null; } +} \ No newline at end of file diff --git a/Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/lifecycle/MinecraftClientMixin.java b/Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/lifecycle/MinecraftClientMixin.java new file mode 100644 index 000000000..7799dd663 --- /dev/null +++ b/Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/lifecycle/MinecraftClientMixin.java @@ -0,0 +1,32 @@ +package net.kernelpanicsoft.archie.mixin.fabric.lifecycle; + +import net.kernelpanicsoft.archie.gametest.AGameTestClientHarnessInternal; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Overlay; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.Unique; +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; + +import org.jetbrains.annotations.Nullable; + +@Mixin(Minecraft.class) +public class MinecraftClientMixin { + @Unique + private boolean archie$startedClientGametests = false; + + @Shadow + @Nullable + private Overlay overlay; + + @Inject(method = "tick", at = @At("HEAD")) + private void onTick(CallbackInfo ci) { + if (!archie$startedClientGametests && overlay == null) { + archie$startedClientGametests = true; + AGameTestClientHarnessInternal.runIfNeeded(); + } + } +} + diff --git a/Archie-Core/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt b/Archie-Core/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt new file mode 100644 index 000000000..4fbbb1950 --- /dev/null +++ b/Archie-Core/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt @@ -0,0 +1,50 @@ +package net.kernelpanicsoft.archie.gametest + +import dev.architectury.platform.Mod +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.events.AGametestEvents +import net.minecraft.client.Minecraft +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Kicks off the client GameTest run once the client has finished loading past its title-screen + * overlay, called from `MinecraftClientMixin.onTick` on every client tick. + */ +internal object AGameTestClientHarnessInternal { + private val hasRun = AtomicBoolean(false) + + /** + * No-ops unless this is a client-side GameTest run ([AGameTestPlatform.isGameTest] and + * [AGameTestPlatform.side] `== CLIENT`) that hasn't started yet. Otherwise registers each mod's + * test classes and runs them via [AClientGameTestHarness.run] on the dedicated test thread. + * + * The client process is always terminated afterward, on both success and failure - it must + * exit with a non-zero code on failure, since [ThreadingImpl.runTestThread] catches and + * stores any thrown exception rather than propagating it, so a plain `error(...)` throw here + * would leave the client sitting at the title screen forever instead of failing the run + * (which is what CI observed: the game never closed, so the Gradle task - and the whole + * CI job - just hung until the outer timeout killed it). + */ + @JvmStatic + fun runIfNeeded() + { + if (!AGameTestPlatform.isGameTest || AGameTestPlatform.side != AGameTestSide.CLIENT) return + if (!hasRun.compareAndSet(false, true)) return + + ThreadingImpl.runTestThread { + val mods = AGametestEvents.MODS.ifEmpty { listOf(Archie.MOD) } + mods.forEach { mod -> AGametestEvents.REGISTER_GAME_TEST.invoker()(mod) } + + val collected: Map>> = AGameTestPlatform.testClasses.mapValues { it.value.toList() } + val summary = AClientGameTestHarness.run(collected, AGameTestPlatform.side) + if (summary.failed > 0) { + val details = summary.failedDetails.joinToString("\n") { failure -> + " - ${failure.testId}: ${failure.rootCause}" + } + Archie.LOGGER.error("Client GameTests failed: {} failing test(s)\n{}", summary.failed, details) + kotlin.system.exitProcess(1) + } + Minecraft.getInstance().stop() + } + } +} diff --git a/Archie-Core/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt b/Archie-Core/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt new file mode 100644 index 000000000..2066faffb --- /dev/null +++ b/Archie-Core/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt @@ -0,0 +1,63 @@ +package net.kernelpanicsoft.archie.gametest + +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.events.AGametestEvents +import net.kernelpanicsoft.archie.gametest.AGameTestPlatform.isGameTest +import net.kernelpanicsoft.archie.mixin.fabric.FabricGameTestModInitializerMixin +import net.minecraft.gametest.framework.GameTestRegistry +import net.minecraft.gametest.framework.GlobalTestReporter + +/** + * Backs `FabricGameTestHelperMixin`, which calls [registerGameTests] at the head of Fabric's + * `FabricGameTestHelper.runHeadlessServer` - the flush step that drives Fabric's own GameTest + * registry. Named distinctly from `archie-core`'s own (internal, `testClasses`-only) + * `AGameTestPlatformInternal` to avoid a same-package class name collision on the runtime + * classpath - this reaches `archie-core`'s test class map via [AGameTestPlatform.testClasses] + * instead of touching that internal object directly. + */ +object AGameTestRegistrationBridge +{ + /** + * No-ops unless [isGameTest]. Fires [AGametestEvents.REGISTER_GAME_TEST] for every mod + * selected by [AGameTestModFilter] (or just [Archie.MOD] if [AGametestEvents.MODS] is empty), + * then registers each resulting test class with [GameTestRegistry] and + * [FabricGameTestModInitializerMixin]'s id/logger bookkeeping - throwing if the same class is + * registered under more than one mod. + * + * Falls back to [NoOpGameTest] for a mod whose registration turns up no classes at all for the + * current [AGameTestPlatform.side] (e.g. a client-only mod's server invocation) - vanilla's + * `GameTestServer` refuses to boot with zero test functions registered anywhere. + */ + @JvmStatic + fun registerGameTests() + { + if (!isGameTest) return + Archie.LOGGER.info("Registering GameTests") + GlobalTestReporter.replaceWith(VerboseTestReporter) + val mods = AGameTestModFilter.selectMods(AGametestEvents.MODS.ifEmpty { listOf(Archie.MOD) }) + for (mod in mods) + { + AGametestEvents.REGISTER_GAME_TEST.invoker()(mod) + val classes = AGameTestPlatform.testClasses.getOrPut(mod, ::mutableSetOf) + val toRegister = if (classes.isEmpty()) setOf(NoOpGameTest::class.java) else classes + for (clazz in toRegister) + { + if (FabricGameTestModInitializerMixin.getGameTestIds().containsKey(clazz)) + { + throw UnsupportedOperationException( + "Test class (${clazz.canonicalName}) has already been registered with mod (${mod.modId})" + ) + } + + FabricGameTestModInitializerMixin.getGameTestIds()[clazz] = mod.modId + GameTestRegistry.register(clazz) + + FabricGameTestModInitializerMixin.getLogger().debug( + "Registered test class {} for mod {}", + clazz.canonicalName, + mod.modId + ) + } + } + } +} diff --git a/Archie-Core/gametest/fabric/src/main/resources/archie_gametest.mixins.json b/Archie-Core/gametest/fabric/src/main/resources/archie_gametest.mixins.json new file mode 100644 index 000000000..be3a066da --- /dev/null +++ b/Archie-Core/gametest/fabric/src/main/resources/archie_gametest.mixins.json @@ -0,0 +1,16 @@ +{ + "required": true, + "package": "net.kernelpanicsoft.archie.mixin.fabric", + "compatibilityLevel": "JAVA_17", + "minVersion": "0.8", + "client": [ + "lifecycle.MinecraftClientMixin" + ], + "mixins": [ + "FabricGameTestHelperMixin", + "FabricGameTestModInitializerMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/Archie-Core/gametest/fabric/src/main/resources/fabric.mod.json b/Archie-Core/gametest/fabric/src/main/resources/fabric.mod.json new file mode 100644 index 000000000..9189dd54f --- /dev/null +++ b/Archie-Core/gametest/fabric/src/main/resources/fabric.mod.json @@ -0,0 +1,26 @@ +{ + "schemaVersion": 1, + "id": "${mod_id}_gametest", + "version": "${mod_version}", + "name": "${mod_display_name} GameTest", + "description": "${mod_display_name}'s GameTest framework/harness - dev/test-time only, never shipped in a production jar.", + "authors": [ + "${mod_authors}" + ], + "contact": { + "homepage": "${mod_url}", + "sources": "${mod_source}" + }, + "license": "${mod_license}", + "environment": "*", + "mixins": [ + "${mod_id}_gametest.mixins.json" + ], + "depends": { + "minecraft": "${versions.minecraft}", + "fabricloader": ">=${versions.fabric_loader}", + "fabric-api": ">=${versions.fabric_api}", + "fabric-language-kotlin": ">=${versions.kotlin_fabric}", + "archie": ">=${mod_version}" + } +} diff --git a/Archie-Core/gametest/neoforge/build.gradle.kts b/Archie-Core/gametest/neoforge/build.gradle.kts new file mode 100644 index 000000000..6f5d33904 --- /dev/null +++ b/Archie-Core/gametest/neoforge/build.gradle.kts @@ -0,0 +1,110 @@ +plugins { + alias(libs.plugins.archie) +} + +architectury { + platformSetupLoomIde() + neoForge() +} + +actualizer { + actualizes(project(":archie-gametest-common")) +} + +configurations { + create("common") + configureEach { + exclude(group = "thedarkcolour", module = "kotlinforforge-neoforge") + exclude(group = "remapped.thedarkcolour", module = "kotlinforforge-neoforge-1d1bcbf2") + } + compileClasspath.get().extendsFrom(configurations["common"]) + runtimeClasspath.get().extendsFrom(configurations["common"]) + testCompileClasspath.get().extendsFrom(compileClasspath.get()) + testRuntimeClasspath.get().extendsFrom(runtimeClasspath.get()) +} + +loom { + accessWidenerPath.set(project(":archie-core-common").loom.accessWidenerPath) + + mods { + maybeCreate("main").apply { + sourceSet(sourceSets.main.get()) + } + } + + runs { + getByName("client") { + name = "Minecraft Client" + source(sourceSets.main.get()) + vmArgs("-XX:+AllowEnhancedClassRedefinition") + property("kotlinx.coroutines.debug", "off") + } + getByName("server") { + name = "Minecraft Server" + source(sourceSets.main.get()) + vmArgs("-XX:+AllowEnhancedClassRedefinition") + property("kotlinx.coroutines.debug", "off") + } + create("gametest") { + server() + name = "Minecraft GameTest" + property("neoforge.enableGameTest", "true") + property("neoforge.gameTestServer", "true") + property("archie.gametest", "true") + property("archie.gametest.modid", "archie_gametest") + property("kotlinx.coroutines.debug", "off") + } + create("gametestClient") { + client() + name = "Minecraft GameTest Client" + property("neoforge.enableGameTest", "true") + property("archie.gametest.side", "client") + property("archie.gametest", "true") + property("archie.gametest.modid", "archie_gametest") + property("kotlinx.coroutines.debug", "off") + } + } +} + +dependencies { + "neoForge"(libs.neoforge) + implementation(libs.kotlin.neoforge) + compileOnly(libs.kotlinx.serialization) + + implementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.api) + testRuntimeOnly(libs.junit.jupiter.engine) + + "common"(project(":archie-gametest-common", "namedElements")) { isTransitive = false } + modApi(project(":archie-core-neoforge")) +} + +modResources { + filesMatching.add("META-INF/neoforge.mods.toml") +} + +tasks { + base.archivesName.set(base.archivesName.get() + "-gametest-neoforge") + + test { + useJUnitPlatform() + } + + processResources { + dependsOn(processTestResources) + } + + processTestResources { + } + + classes { + finalizedBy(testClasses) + } + + sourcesJar { + val commonSources = project(":archie-gametest-common").tasks.sourcesJar + dependsOn(commonSources) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(commonSources.get().archiveFile.map { zipTree(it) }) + } +} diff --git a/Archie-Core/gametest/neoforge/gradle.properties b/Archie-Core/gametest/neoforge/gradle.properties new file mode 100644 index 000000000..2914393db --- /dev/null +++ b/Archie-Core/gametest/neoforge/gradle.properties @@ -0,0 +1 @@ +loom.platform=neoforge \ No newline at end of file diff --git a/Archie-Core/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java b/Archie-Core/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java new file mode 100644 index 000000000..00023e14a --- /dev/null +++ b/Archie-Core/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java @@ -0,0 +1,64 @@ +package net.kernelpanicsoft.archie.mixin.neoforge; + +import net.kernelpanicsoft.archie.Archie; +import net.kernelpanicsoft.archie.gametest.AGameTestPlatform; +import net.kernelpanicsoft.archie.gametest.AGameTestRegistrationBridge; +import net.kernelpanicsoft.archie.gametest.VerboseTestReporter; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GlobalTestReporter; +import net.minecraft.resources.ResourceLocation; +import net.neoforged.neoforge.gametest.GameTestHooks; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Inject; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; +import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; +import dev.architectury.platform.Mod; + +import java.lang.reflect.Method; + +@Mixin(GameTestHooks.class) +public abstract class GameTestHooksMixin { + + @Inject(method = "getTemplateNamespace(Ljava/lang/reflect/Method;)Ljava/lang/String;", at = @At("HEAD"), cancellable = true) + private static void getTemplateNamespaceMixin(Method method, CallbackInfoReturnable cir) + { + GameTest gameTest = method.getAnnotation(GameTest.class); + Mod mod = AGameTestRegistrationBridge.getTestClassToMod().get(method.getDeclaringClass()); + + if (gameTest.template().contains(":")) + { + ResourceLocation template = ResourceLocation.parse(gameTest.template()); + cir.setReturnValue(template.getNamespace()); + return; + } + + if (mod != null) + { + cir.setReturnValue(mod.getModId()); + return; + } + + } + + @Inject(method = "prefixGameTestTemplate(Ljava/lang/reflect/Method;)Z", at = @At("HEAD"), cancellable = true) + private static void prefixGameTestTemplateMixin(Method method, CallbackInfoReturnable cir) + { + GameTest gameTest = method.getAnnotation(GameTest.class); + if (gameTest.template().contains(":")) + { + cir.setReturnValue(false); + } + } + + @Inject(method = "registerGametests()V", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/ModLoader;postEvent(Lnet/neoforged/bus/api/Event;)V")) + private static void registerGametests(CallbackInfo ci) + { + if (AGameTestPlatform.INSTANCE.isGameTest()) + { + Archie.LOGGER.info("Registering GameTests"); + GlobalTestReporter.replaceWith(VerboseTestReporter.INSTANCE); + AGameTestRegistrationBridge.addEventHandlers(); + } + } +} diff --git a/Archie-Core/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/lifecycle/MinecraftClientMixin.java b/Archie-Core/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/lifecycle/MinecraftClientMixin.java new file mode 100644 index 000000000..126fa4382 --- /dev/null +++ b/Archie-Core/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/lifecycle/MinecraftClientMixin.java @@ -0,0 +1,31 @@ +package net.kernelpanicsoft.archie.mixin.neoforge.lifecycle; + +import net.kernelpanicsoft.archie.gametest.AGameTestClientHarnessInternal; +import net.minecraft.client.Minecraft; +import net.minecraft.client.gui.screens.Overlay; +import org.jetbrains.annotations.Nullable; +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(Minecraft.class) +public class MinecraftClientMixin { + @Unique + private boolean archie$startedClientGametests = false; + + @Shadow + @Nullable + private Overlay overlay; + + @Inject(method = "tick", at = @At("HEAD")) + private void onTick(CallbackInfo ci) { + if (!archie$startedClientGametests && overlay == null) { + archie$startedClientGametests = true; + AGameTestClientHarnessInternal.runIfNeeded(); + } + } +} + diff --git a/Archie-Core/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt b/Archie-Core/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt new file mode 100644 index 000000000..4fbbb1950 --- /dev/null +++ b/Archie-Core/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt @@ -0,0 +1,50 @@ +package net.kernelpanicsoft.archie.gametest + +import dev.architectury.platform.Mod +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.events.AGametestEvents +import net.minecraft.client.Minecraft +import java.util.concurrent.atomic.AtomicBoolean + +/** + * Kicks off the client GameTest run once the client has finished loading past its title-screen + * overlay, called from `MinecraftClientMixin.onTick` on every client tick. + */ +internal object AGameTestClientHarnessInternal { + private val hasRun = AtomicBoolean(false) + + /** + * No-ops unless this is a client-side GameTest run ([AGameTestPlatform.isGameTest] and + * [AGameTestPlatform.side] `== CLIENT`) that hasn't started yet. Otherwise registers each mod's + * test classes and runs them via [AClientGameTestHarness.run] on the dedicated test thread. + * + * The client process is always terminated afterward, on both success and failure - it must + * exit with a non-zero code on failure, since [ThreadingImpl.runTestThread] catches and + * stores any thrown exception rather than propagating it, so a plain `error(...)` throw here + * would leave the client sitting at the title screen forever instead of failing the run + * (which is what CI observed: the game never closed, so the Gradle task - and the whole + * CI job - just hung until the outer timeout killed it). + */ + @JvmStatic + fun runIfNeeded() + { + if (!AGameTestPlatform.isGameTest || AGameTestPlatform.side != AGameTestSide.CLIENT) return + if (!hasRun.compareAndSet(false, true)) return + + ThreadingImpl.runTestThread { + val mods = AGametestEvents.MODS.ifEmpty { listOf(Archie.MOD) } + mods.forEach { mod -> AGametestEvents.REGISTER_GAME_TEST.invoker()(mod) } + + val collected: Map>> = AGameTestPlatform.testClasses.mapValues { it.value.toList() } + val summary = AClientGameTestHarness.run(collected, AGameTestPlatform.side) + if (summary.failed > 0) { + val details = summary.failedDetails.joinToString("\n") { failure -> + " - ${failure.testId}: ${failure.rootCause}" + } + Archie.LOGGER.error("Client GameTests failed: {} failing test(s)\n{}", summary.failed, details) + kotlin.system.exitProcess(1) + } + Minecraft.getInstance().stop() + } + } +} diff --git a/Archie-Core/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt b/Archie-Core/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt new file mode 100644 index 000000000..4bde72c69 --- /dev/null +++ b/Archie-Core/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt @@ -0,0 +1,61 @@ +package net.kernelpanicsoft.archie.gametest + +import dev.architectury.platform.Mod +import net.kernelpanicsoft.archie.events.AGametestEvents +import net.neoforged.fml.ModList +import net.neoforged.neoforge.event.RegisterGameTestsEvent + +/** + * Backs [AGameTestPlatform] on NeoForge: drives NeoForge's own `RegisterGameTestsEvent`. Named + * distinctly from `archie-core`'s own (internal, `testClasses`-only) `AGameTestPlatformInternal` + * to avoid a same-package class name collision on the runtime classpath - this reaches + * `archie-core`'s test class map via [AGameTestPlatform.testClasses] instead of touching that + * internal object directly. + */ +object AGameTestRegistrationBridge +{ + /** + * Inverse of [AGameTestPlatform.testClasses]: the owning mod for each registered test class. + * Used by `GameTestHooksMixin` to resolve a `@GameTest`-annotated method's template namespace + * when its `template()` string doesn't specify one explicitly. + */ + @JvmStatic + @get:JvmName("getTestClassToMod") + val testClassToMod: Map, Mod> + get() = buildMap { + AGameTestPlatform.testClasses.forEach { (mod, classes) -> + classes.forEach { put(it, mod) } + } + } + + /** + * No-ops unless [AGameTestPlatform.isGameTest]. For every mod selected by + * [AGameTestModFilter] from [AGametestEvents.MODS], subscribes to that mod's + * `RegisterGameTestsEvent`; when it fires, fires [AGametestEvents.REGISTER_GAME_TEST] for the + * mod and registers each resulting test class with NeoForge's event. + * + * Falls back to [NoOpGameTest] for a mod whose registration turns up no classes at all for the + * current [AGameTestPlatform.side] (e.g. a client-only mod's server invocation) - vanilla's + * `GameTestServer` refuses to boot with zero test functions registered anywhere. + */ + @JvmStatic + @JvmName("addEventHandlers") + fun addEventHandlers() + { + if (!AGameTestPlatform.isGameTest) return + + for (mod in AGameTestModFilter.selectMods(AGametestEvents.MODS)) + { + ModList.get().getModContainerById(mod.modId).ifPresent { + it.eventBus?.addListener { event -> + AGametestEvents.REGISTER_GAME_TEST.invoker()(mod) + val classes = AGameTestPlatform.testClasses.getOrPut(mod, ::mutableSetOf) + for (clazz in if (classes.isEmpty()) setOf(NoOpGameTest::class.java) else classes) + { + event.register(clazz) + } + } + } + } + } +} diff --git a/Archie-Core/gametest/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/Archie-Core/gametest/neoforge/src/main/resources/META-INF/neoforge.mods.toml new file mode 100644 index 000000000..982d6e9ce --- /dev/null +++ b/Archie-Core/gametest/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -0,0 +1,38 @@ +modLoader = "klf" +loaderVersion = "[${versions.kotlin_neoforge_range},)" +issueTrackerURL = "" +license = "${mod_license}" + +[[mods]] +modId = "${mod_id}_gametest" +version = "${mod_version}" +displayName = "${mod_display_name} GameTest" +authors = "${mod_authors}" +description = ''' +${mod_display_name}'s GameTest framework/harness - dev/test-time only, never shipped in a production jar. +''' +displayURL = "${mod_url}" + +[[mixins]] +config = "${mod_id}_gametest.mixins.json" + +[[dependencies."${mod_id}_gametest"]] +modId = "neoforge" +type = "required" +versionRange = "[${versions.neoforge_range},)" +ordering = "NONE" +side = "BOTH" + +[[dependencies."${mod_id}_gametest"]] +modId = "minecraft" +type = "required" +versionRange = "[${versions.minecraft}]" +ordering = "NONE" +side = "BOTH" + +[[dependencies."${mod_id}_gametest"]] +modId = "archie" +type = "required" +versionRange = "[${mod_version},)" +ordering = "AFTER" +side = "BOTH" diff --git a/Archie-Core/gametest/neoforge/src/main/resources/archie_gametest.mixins.json b/Archie-Core/gametest/neoforge/src/main/resources/archie_gametest.mixins.json new file mode 100644 index 000000000..ae0f33176 --- /dev/null +++ b/Archie-Core/gametest/neoforge/src/main/resources/archie_gametest.mixins.json @@ -0,0 +1,15 @@ +{ + "required": true, + "package": "net.kernelpanicsoft.archie.mixin.neoforge", + "compatibilityLevel": "JAVA_17", + "minVersion": "0.8", + "client": [ + "lifecycle.MinecraftClientMixin" + ], + "mixins": [ + "GameTestHooksMixin" + ], + "injectors": { + "defaultRequire": 1 + } +} diff --git a/Archie-Core/settings.gradle.kts b/Archie-Core/settings.gradle.kts index f9949d06b..844757759 100644 --- a/Archie-Core/settings.gradle.kts +++ b/Archie-Core/settings.gradle.kts @@ -39,6 +39,14 @@ includeCorePlatform("common") includeCorePlatform("fabric") includeCorePlatform("neoforge") +includeModule("datagen", "common") +includeModule("datagen", "fabric") +includeModule("datagen", "neoforge") + +includeModule("gametest", "common") +includeModule("gametest", "fabric") +includeModule("gametest", "neoforge") + fun includeModule(name: String, platform: String) { include("$name/$platform") project(":$name/$platform").name = "archie-$name-$platform" From 5f76758a81326a773958dc949397eef702c0d467 Mon Sep 17 00:00:00 2001 From: KernelPanic Date: Mon, 10 Aug 2026 21:51:02 -0400 Subject: [PATCH 3/9] Collapse Archie/Archie-Core/Archie-Test into one repo at root Finalizes the archie-core migration: archie-core's Loom-based build (from Archie-Core/) now IS the repo root, replacing the old Gradle composite build that wired together separate Archie/ and Archie-Test/ builds via includeBuild. - Folds Archie-Test in as a fourth product, archie-test-{common,fabric, neoforge}, using the exact same nested-module convention as core/datagen/ gametest - depends on the other three via plain project references (modApi(project(":archie-core-fabric")) etc.) instead of the old composite-build dependencySubstitution + raw "net.kernelpanicsoft:common" coordinate trick. AEvents references in its GameTest suite (stale from before AEvents was dissolved into ADatagenEvents/AGametestEvents) fixed along the way. - Wires in the dev.opensavvy.dokka-mkdocs docs pipeline at the new root (dokka(project(":archie-core-common")) etc. aggregation, embedDokkaInto- MkDocs, publishDocs/generateChangelog tasks) and ports mkdocs.yml + docs/ + CHANGELOG.md up from the old Archie/ root, fixing the mkdocs edit_uri path. modfusioner/modpublisher (CurseForge/Modrinth publishing, merged-jar fusion) are NOT ported yet - noted as a known gap in AGENTS.md. - Moves archie-core's settings.gradle.kts/build.gradle.kts/gradle.properties/ gradlew/wrapper up to become the repo's own; settings.gradle.kts's version catalog now resolves gradle/libs.versions.toml via Gradle's own default- location convention (dropped the now-redundant explicit dependencyResolutionManagement block, which double-registered it). rootProject.name -> "Archie". - Deletes the old Archie/, Archie-Test/, and now-empty Archie-Core/ directories entirely. - Updates .github/workflows/{check,docs,release-notes}.yaml, .github/scripts/generate_release_notes.py, README.md, AGENTS.md, and the sync-docs-after-overhaul skill for the new flat module layout and paths. - .gitignore: added a top-level .kotlin entry (the existing */.kotlin pattern only matched one level deep, missing the root-level .kotlin/ this layout now produces). Verified: full `./gradlew build -x test` succeeds at the new root across all 12 modules (archie-core/-datagen/-gametest/-test x common/fabric/neoforge). Co-Authored-By: Claude Sonnet 5 --- .../skills/sync-docs-after-overhaul/SKILL.md | 16 +- .github/scripts/generate_release_notes.py | 2 +- .github/workflows/check.yaml | 12 +- .github/workflows/docs.yaml | 3 +- .github/workflows/release-notes.yaml | 10 +- .gitignore | 1 + AGENTS.md | 124 +- Archie-Core/build.gradle.kts | 131 -- Archie-Core/gradle.properties | 12 - Archie-Core/gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - Archie-Core/gradlew | 251 --- Archie-Core/gradlew.bat | 94 - Archie-Core/settings.gradle.kts | 58 - .../.architectury-transformer/debug.log | 1 - Archie-Test/build.gradle.kts | 154 -- Archie-Test/common/build.gradle.kts | 98 - Archie-Test/fabric/build.gradle.kts | 228 --- Archie-Test/gradle.properties | 19 - Archie-Test/gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - Archie-Test/gradlew | 251 --- Archie-Test/gradlew.bat | 94 - Archie-Test/neoforge/build.gradle.kts | 250 --- Archie-Test/settings.gradle.kts | 48 - Archie/build.gradle.kts | 293 --- Archie/common/build.gradle.kts | 184 -- .../archie/data/internal/ArchieDatagen.kt | 39 - .../common/tags/AInternalBiomeTagsProvider.kt | 349 ---- .../common/tags/AInternalBlockTagsProvider.kt | 382 ---- .../tags/AInternalEntityTypeTagsProvider.kt | 43 - .../common/tags/AInternalFluidTagsProvider.kt | 46 - .../common/tags/AInternalItemTagsProvider.kt | 654 ------- .../gametest/internal/ArchieGameTest.kt | 49 - .../internal/tests/ArchieItemHandlerTests.kt | 77 - .../tests/BlockEntityNBTHolderTests.kt | 127 -- .../tests/BlockEntityStateManagerTests.kt | 86 - .../internal/tests/ComposeRenderingTests.kt | 102 - .../internal/tests/InputComponentsGameTest.kt | 303 --- .../tests/LayoutComponentsGameTest.kt | 211 --- .../internal/tests/ModalComponentsGameTest.kt | 210 --- .../gui/access/SlotLayerDepthContext.java | 52 - .../archie/APlatform.common.kt | 8 - .../net/kernelpanicsoft/archie/Archie.kt | 353 ---- .../archie/block/entity/NBTBlockEntity.kt | 60 - .../archie/config/CategorySpec.kt | 31 - .../archie/config/ClientConfigContainer.kt | 48 - .../archie/config/ClientConfigSpec.kt | 43 - .../archie/config/ClientDataSpec.kt | 1265 ------------- .../archie/config/CommonKeyCode.kt | 84 - .../archie/config/ConfigContainer.kt | 60 - .../archie/config/ConfigSpec.kt | 313 --- .../kernelpanicsoft/archie/config/DataSpec.kt | 1673 ----------------- .../archie/config/FieldType.kt | 188 -- .../archie/config/IConfigSerializer.kt | 77 - .../archie/config/builder/ColorListBuilder.kt | 38 - .../archie/config/builder/ColorMapBuilder.kt | 40 - .../config/builder/ConfigFieldBuilder.kt | 40 - .../archie/config/builder/DoubleMapBuilder.kt | 26 - .../config/builder/DropdownFieldBuilder.kt | 46 - .../archie/config/builder/FloatMapBuilder.kt | 26 - .../config/builder/IntegerMapBuilder.kt | 26 - .../config/builder/KeycodeListBuilder.kt | 61 - .../config/builder/KeycodeMapBuilder.kt | 56 - .../archie/config/builder/ListFieldBuilder.kt | 105 -- .../archie/config/builder/LongMapBuilder.kt | 26 - .../archie/config/builder/MapFieldBuilder.kt | 181 -- .../config/builder/RegistryFieldBuilder.kt | 56 - .../config/builder/RegistryListBuilder.kt | 34 - .../config/builder/RegistryMapBuilder.kt | 36 - .../archie/config/builder/SpecFieldBuilder.kt | 56 - .../archie/config/builder/SpecListBuilder.kt | 30 - .../archie/config/builder/SpecMapBuilder.kt | 31 - .../archie/config/builder/StringMapBuilder.kt | 26 - .../archie/config/builder/extensions.kt | 259 --- .../archie/config/entry/ConfigSpecEntry.kt | 111 -- .../archie/config/extensions.kt | 22 - .../serializer/Json5ConfigSerializer.kt | 31 - .../config/serializer/JsonConfigSerializer.kt | 32 - .../config/serializer/NullConfigSerializer.kt | 32 - .../config/serializer/TomlConfigSerializer.kt | 32 - .../archie/data/ADataGenerator.kt | 322 ---- .../data/ADataGeneratorPlatform.common.kt | 12 - .../archie/data/ADatagenEventObject.kt | 20 - .../archie/data/IADataProvider.kt | 41 - .../archie/data/client/ALanguageProvider.kt | 154 -- .../data/client/model/ABlockModelBuilder.kt | 8 - .../data/client/model/ABlockModelProvider.kt | 11 - .../data/client/model/ABlockStateProvider.kt | 1583 ---------------- .../data/client/model/AConfiguredModel.kt | 242 --- .../data/client/model/ACustomLoaderBuilder.kt | 84 - .../data/client/model/AItemModelBuilder.kt | 88 - .../data/client/model/AItemModelProvider.kt | 30 - .../archie/data/client/model/AModelBuilder.kt | 1386 -------------- .../archie/data/client/model/AModelFile.kt | 15 - .../data/client/model/AModelProvider.kt | 498 ----- .../model/AMultiPartBlockStateBuilder.kt | 271 --- .../client/model/AVariantBlockStateBuilder.kt | 328 ---- .../client/model/IAGeneratedBlockState.kt | 10 - .../data/common/conditions/AAndCondition.kt | 35 - .../common/conditions/ABuiltinConditions.kt | 30 - .../common/conditions/AConditionBuilder.kt | 62 - .../conditions/AConditionsPlatform.common.kt | 29 - .../common/conditions/AEqualsCondition.kt | 36 - .../data/common/conditions/AFalseCondition.kt | 26 - .../data/common/conditions/AGroupCondition.kt | 16 - .../common/conditions/AModLoadedCondition.kt | 46 - .../data/common/conditions/ANotCondition.kt | 34 - .../data/common/conditions/AOrCondition.kt | 35 - .../common/conditions/APlatformCondition.kt | 47 - .../common/conditions/ARegistryCondition.kt | 49 - .../data/common/conditions/ATrueCondition.kt | 25 - .../data/common/conditions/AXorCondition.kt | 35 - .../data/common/conditions/Extensions.kt | 18 - .../data/common/conditions/IACondition.kt | 84 - .../data/common/crafting/ARecipeProvider.kt | 109 -- .../crafting/ingredients/AAllIngredient.kt | 60 - .../crafting/ingredients/AAnyIngredient.kt | 51 - .../ingredients/ABuiltinIngredients.kt | 15 - .../ingredients/ACombinedIngredient.kt | 64 - .../ingredients/AComponentsIngredient.kt | 120 -- .../ingredients/ACustomDataIngredient.kt | 122 -- .../ACustomIngredientPlatform.common.kt | 9 - ...stomIngredientSerializerPlatform.common.kt | 8 - .../ingredients/IACustomIngredient.kt | 59 - .../ingredients/IACustomIngredientHolder.kt | 11 - .../IACustomIngredientSerializer.kt | 43 - .../recipies/ArchieCookingRecipeBuilder.kt | 109 -- .../recipies/ArchieShapedRecipeBuilder.kt | 133 -- .../recipies/ArchieShapelessRecipeBuilder.kt | 111 -- .../crafting/recipies/IARecipeBuilder.kt | 21 - .../archie/data/common/tags/ACommonTags.kt | 1090 ----------- .../archie/data/common/tags/ATagBuilder.kt | 169 -- .../common/tags/ATagBuilderPlatform.common.kt | 14 - .../archie/data/common/tags/ATagsProvider.kt | 423 ----- .../archie/data/common/tags/IATagBuilder.kt | 155 -- .../archie/data/util/TransformationHelper.kt | 396 ---- .../archie/events/ABasicEventObject.kt | 25 - .../archie/events/AEventObject.kt | 47 - .../kernelpanicsoft/archie/events/AEvents.kt | 185 -- .../archie/gametest/AClientGameTestHarness.kt | 1653 ---------------- .../ADedicatedServerPlatform.common.kt | 25 - .../archie/gametest/AGameTestEventObject.kt | 20 - .../gametest/AGameTestPlatform.common.kt | 78 - .../gametest/ComposeScreenTestContext.kt | 286 --- .../archie/gametest/GameTestAssertions.kt | 50 - .../archie/gametest/NoOpGameTest.kt | 21 - .../archie/gametest/ScreenshotComparer.kt | 101 - .../gametest/ScreenshotComparisonAlgorithm.kt | 211 --- .../archie/gametest/ScreenshotManager.kt | 54 - .../archie/gametest/ThreadingImpl.kt | 457 ----- .../archie/gametest/VerboseTestReporter.kt | 30 - .../gametest/junit/GameTestGradleExecutor.kt | 327 ---- .../junit/GameTestGradleInvocation.kt | 60 - .../archie/gametest/junit/GameTestRunner.kt | 159 -- .../archie/gui/AUIScopeManager.kt | 17 - .../archie/gui/ComposeBlockContainerMenu.kt | 74 - .../archie/gui/ComposeContainerMenuBase.kt | 483 ----- .../archie/gui/ComposeContainerScreen.kt | 482 ----- .../archie/gui/ComposeScreen.kt | 335 ---- .../net/kernelpanicsoft/archie/gui/Slot.kt | 295 --- .../gui/access/SlotHighlightClipProvider.kt | 17 - .../gui/access/SlotLayerDepthProvider.kt | 12 - .../archie/gui/animation/Animation.kt | 94 - .../BlockEntityStateComposables.kt | 44 - .../blockentity/BlockEntityStateContainer.kt | 171 -- .../blockentity/BlockEntityStateManager.kt | 162 -- .../gui/blockentity/BlockEntityStatePacket.kt | 102 - .../BlockEntityStatePacketRegistry.kt | 118 -- .../blockentity/BlockEntityUpdatePacket.kt | 37 - .../blockentity/ComposeBlockEntityState.kt | 148 -- .../archie/gui/composables/basic/Divider.kt | 54 - .../archie/gui/composables/basic/EnergyBar.kt | 45 - .../archie/gui/composables/basic/FluidTank.kt | 91 - .../archie/gui/composables/basic/Icon.kt | 43 - .../gui/composables/basic/ProgressBar.kt | 108 -- .../archie/gui/composables/basic/Spacer.kt | 37 - .../archie/gui/composables/basic/Text.kt | 99 - .../archie/gui/composables/basic/Texture.kt | 55 - .../gui/composables/containers/Collapsible.kt | 166 -- .../composables/containers/ContainerPanel.kt | 102 - .../gui/composables/containers/Panel.kt | 43 - .../composables/containers/RootContainer.kt | 36 - .../gui/composables/containers/Scrollable.kt | 267 --- .../gui/composables/containers/Surface.kt | 77 - .../composables/containers/TabContainer.kt | 410 ---- .../archie/gui/composables/input/Button.kt | 145 -- .../archie/gui/composables/input/Checkbox.kt | 129 -- .../archie/gui/composables/input/Clickable.kt | 92 - .../gui/composables/input/ColorPicker.kt | 179 -- .../archie/gui/composables/input/Radio.kt | 159 -- .../archie/gui/composables/input/Slider.kt | 203 -- .../archie/gui/composables/input/Switch.kt | 133 -- .../composables/input/textfield/TextField.kt | 221 --- .../input/textfield/TextFieldCore.kt | 313 --- .../input/textfield/TextFieldValue.kt | 61 - .../gui/composables/modal/ConfirmDialog.kt | 108 -- .../gui/composables/modal/DialogPrimitives.kt | 202 -- .../gui/composables/theme/TextureStates.kt | 26 - .../gui/composables/theme/WidgetState.kt | 84 - .../gui/item/ComposeItemContainerMenu.kt | 147 -- .../archie/gui/item/ComposeItemState.kt | 145 -- .../archie/gui/item/ItemContainerAccess.kt | 43 - .../archie/gui/item/ItemStateComposables.kt | 49 - .../archie/gui/item/ItemStateManager.kt | 53 - .../archie/gui/item/ItemStatePacket.kt | 38 - .../gui/item/ItemStatePacketRegistry.kt | 42 - .../archie/gui/item/ItemUpdatePacket.kt | 30 - .../archie/gui/item/SyncedItemHolder.kt | 33 - .../kernelpanicsoft/archie/gui/layer/Layer.kt | 53 - .../archie/gui/layer/LayerStackManager.kt | 382 ---- .../archie/gui/layout/Alignment.kt | 275 --- .../archie/gui/layout/Arrangement.kt | 689 ------- .../kernelpanicsoft/archie/gui/layout/Box.kt | 60 - .../archie/gui/layout/Column.kt | 87 - .../archie/gui/layout/Helpers.kt | 13 - .../archie/gui/layout/IntCoordinates.kt | 55 - .../archie/gui/layout/IntRect.kt | 44 - .../archie/gui/layout/Layout.kt | 60 - .../archie/gui/layout/LayoutDirection.kt | 19 - .../archie/gui/layout/LayoutNode.kt | 401 ---- .../archie/gui/layout/MeasurePolicy.kt | 123 -- .../kernelpanicsoft/archie/gui/layout/Row.kt | 80 - .../gui/layout/RowColumnMeasurePolicy.kt | 71 - .../kernelpanicsoft/archie/gui/layout/Size.kt | 16 - .../archie/gui/modifiers/Constraints.kt | 80 - .../archie/gui/modifiers/DebugModifier.kt | 55 - .../archie/gui/modifiers/DrawModifier.kt | 51 - .../gui/modifiers/LayoutChangingModifier.kt | 47 - .../archie/gui/modifiers/Modifier.kt | 153 -- .../modifiers/OnGloballyPositionedModifier.kt | 33 - .../gui/modifiers/OnSizeChangedModifier.kt | 28 - .../archie/gui/modifiers/SizeModifier.kt | 132 -- .../appearance/BackgroundModifier.kt | 97 - .../modifiers/appearance/BorderModifier.kt | 53 - .../modifiers/appearance/TextureModifier.kt | 26 - .../modifiers/appearance/TooltipModifier.kt | 29 - .../archie/gui/modifiers/input/InputEvent.kt | 112 -- .../modifiers/input/OnCharTypedModifier.kt | 34 - .../gui/modifiers/input/OnKeyEventModifier.kt | 36 - .../modifiers/input/OnPointerEventModifier.kt | 150 -- .../gui/modifiers/position/MarginModifier.kt | 90 - .../gui/modifiers/position/OffsetModifier.kt | 34 - .../gui/modifiers/position/PaddingModifier.kt | 96 - .../archie/gui/modifiers/position/ZIndex.kt | 33 - .../archie/gui/nodes/LayoutNodeApplier.kt | 42 - .../archie/gui/nodes/UINode.kt | 57 - .../gui/render/AFluidRenderPlatform.common.kt | 21 - .../archie/gui/theme/ComposableTheme.kt | 287 --- .../kernelpanicsoft/archie/gui/theme/Theme.kt | 193 -- .../archie/gui/util/HsvColor.kt | 55 - .../kernelpanicsoft/archie/gui/util/KColor.kt | 135 -- .../archie/gui/util/extension/GuiGraphics.kt | 164 -- .../archie/gui/util/extension/Screen.kt | 177 -- .../gui/util/extension/VertexConsumer.kt | 20 - .../archie/networking/ArchieNetworkChannel.kt | 27 - .../archie/networking/IPacketContext.kt | 35 - .../archie/networking/NetworkChannel.kt | 486 ----- .../AClientRegistrationPlatform.common.kt | 24 - .../archie/registries/ACreativeTabRegistry.kt | 11 - .../registries/ADeferredRegistryHolder.kt | 89 - .../archie/registries/BlockRegistryHelper.kt | 63 - .../registries/CreativeTabRegistryHelper.kt | 45 - .../archie/registries/RegistrarHelper.kt | 42 - .../archie/registries/RegistryHelper.kt | 66 - .../archie/registries/extensions.kt | 16 - .../SerializationReloadListener.kt | 88 - .../serialization/ArchieDataAttachmentImpl.kt | 24 - .../serialization/AttachmentRegistry.kt | 102 - .../archie/serialization/DataAttachment.kt | 106 -- .../serialization/FluidStackNBTHolderImpl.kt | 244 --- .../serialization/ItemStackNBTHolderImpl.kt | 348 ---- .../archie/serialization/KOps.kt | 674 ------- .../archie/serialization/NBT.kt | 270 --- .../archie/serialization/NBTHolder.kt | 119 -- .../archie/serialization/NBTHolderImpl.kt | 304 --- .../archie/serialization/ObservableList.kt | 98 - .../archie/serialization/ObservableMap.kt | 84 - .../serialization/SerializationManager.kt | 391 ---- .../archie/serialization/Sync.kt | 13 - .../archie/serialization/Utils.kt | 292 --- .../serializers/BuiltinSerializers.kt | 134 -- .../serializers/MinecraftSerializers.kt | 363 ---- .../transfer/ArchieCapabilityExposure.kt | 124 -- .../archie/transfer/ArchieEnergyStorage.kt | 153 -- .../archie/transfer/ArchieFluidSlot.kt | 192 -- .../archie/transfer/ArchieFluidStorage.kt | 126 -- .../archie/transfer/ArchieItemMenuSlot.kt | 64 - .../archie/transfer/ArchieItemSlot.kt | 175 -- .../archie/transfer/ArchieItemStorage.kt | 102 - .../archie/transfer/VanillaMenuSlot.kt | 69 - .../net/kernelpanicsoft/archie/util/Array.kt | 22 - .../kernelpanicsoft/archie/util/Component.kt | 244 --- .../net/kernelpanicsoft/archie/util/Env.kt | 39 - .../archie/util/MutableEntry.kt | 12 - .../kernelpanicsoft/archie/util/Properties.kt | 35 - .../kernelpanicsoft/archie/util/Reflect.kt | 39 - .../archie/util/ResourceLocation.kt | 19 - .../net/kernelpanicsoft/archie/util/Tile.kt | 18 - .../AbstractContainerScreenDepthMixin.java | 27 - .../gui/AbstractContainerScreenMixin.java | 112 -- .../mixin/client/gui/GuiGraphicsMixin.java | 40 - .../main/resources/archie-common.mixins.json | 16 - .../src/main/resources/archie.accesswidener | 335 ---- .../src/main/resources/archie.common.json | 3 - .../archie/archie_themes/java.theme.json | 8 - .../archie/archie_themes/java/button.json | 19 - .../archie/archie_themes/java/checkbox.json | 22 - .../archie_themes/java/dark/surface.json | 21 - .../archie/archie_themes/java/energy_bar.json | 13 - .../archie/archie_themes/java/fluid_tank.json | 13 - .../archie_themes/java/progress_bar.json | 13 - .../archie/archie_themes/java/radio.json | 25 - .../archie/archie_themes/java/slider.json | 19 - .../archie_themes/java/slider_handle.json | 19 - .../archie/archie_themes/java/slot.json | 15 - .../archie_themes/java/small_checkbox.json | 16 - .../archie/archie_themes/java/surface.json | 20 - .../archie_themes/java/switch_thumb.json | 16 - .../archie_themes/java/switch_track.json | 25 - .../archie/archie_themes/java/tab_game.json | 25 - .../archie/archie_themes/java/tab_menu.json | 25 - .../archie/archie_themes/java/text_field.json | 16 - .../resources/assets/archie/atlases/java.json | 9 - .../main/resources/assets/archie/banner.png | Bin 32385 -> 0 bytes .../src/main/resources/assets/archie/icon.png | Bin 68643 -> 0 bytes .../textures/gui/sprites/java/button.png | Bin 1432 -> 0 bytes .../gui/sprites/java/button.png.mcmeta | 10 - .../gui/sprites/java/button_disabled.png | Bin 1223 -> 0 bytes .../sprites/java/button_disabled.png.mcmeta | 10 - .../gui/sprites/java/button_highlighted.png | Bin 1448 -> 0 bytes .../java/button_highlighted.png.mcmeta | 10 - .../textures/gui/sprites/java/checkbox.png | Bin 408 -> 0 bytes .../gui/sprites/java/checkbox_clicked.png | Bin 482 -> 0 bytes .../java/checkbox_clicked_and_hovered.png | Bin 478 -> 0 bytes .../gui/sprites/java/checkbox_hovered.png | Bin 407 -> 0 bytes .../textures/gui/sprites/java/energy_bar.png | Bin 136 -> 0 bytes .../gui/sprites/java/energy_bar.png.mcmeta | 10 - .../textures/gui/sprites/java/fluid_tank.png | Bin 131 -> 0 bytes .../gui/sprites/java/fluid_tank.png.mcmeta | 10 - .../gui/sprites/java/progress_bar.png | Bin 115 -> 0 bytes .../gui/sprites/java/progress_bar.png.mcmeta | 10 - .../textures/gui/sprites/java/radio.png | Bin 469 -> 0 bytes .../gui/sprites/java/radio_clicked.png | Bin 468 -> 0 bytes .../java/radio_clicked_and_hovered.png | Bin 463 -> 0 bytes .../gui/sprites/java/radio_disabled.png | Bin 424 -> 0 bytes .../gui/sprites/java/radio_hovered.png | Bin 468 -> 0 bytes .../textures/gui/sprites/java/slider.png | Bin 1158 -> 0 bytes .../gui/sprites/java/slider.png.mcmeta | 10 - .../gui/sprites/java/slider_handle.png | Bin 242 -> 0 bytes .../gui/sprites/java/slider_handle.png.mcmeta | 15 - .../java/slider_handle_highlighted.png | Bin 237 -> 0 bytes .../java/slider_handle_highlighted.png.mcmeta | 15 - .../gui/sprites/java/slider_highlighted.png | Bin 1165 -> 0 bytes .../java/slider_highlighted.png.mcmeta | 10 - .../archie/textures/gui/sprites/java/slot.png | Bin 507 -> 0 bytes .../gui/sprites/java/small_checkbox.png | Bin 239 -> 0 bytes .../sprites/java/small_checkbox_clicked.png | Bin 320 -> 0 bytes .../textures/gui/sprites/java/surface.png | Bin 166 -> 0 bytes .../gui/sprites/java/surface.png.mcmeta | 10 - .../gui/sprites/java/surface_dark.png | Bin 173 -> 0 bytes .../gui/sprites/java/surface_dark.png.mcmeta | 10 - .../gui/sprites/java/surface_inset.png | Bin 366 -> 0 bytes .../gui/sprites/java/surface_inset.png.mcmeta | 10 - .../gui/sprites/java/surface_inset_dark.png | Bin 372 -> 0 bytes .../java/surface_inset_dark.png.mcmeta | 10 - .../gui/sprites/java/switch_thumb.png | Bin 126 -> 0 bytes .../sprites/java/switch_thumb_disabled.png | Bin 127 -> 0 bytes .../gui/sprites/java/switch_track.png | Bin 639 -> 0 bytes .../gui/sprites/java/switch_track_clicked.png | Bin 965 -> 0 bytes .../java/switch_track_clicked_and_hovered.png | Bin 964 -> 0 bytes .../sprites/java/switch_track_disabled.png | Bin 645 -> 0 bytes .../gui/sprites/java/switch_track_hovered.png | Bin 637 -> 0 bytes .../textures/gui/sprites/java/tab_game.png | Bin 147 -> 0 bytes .../gui/sprites/java/tab_game.png.mcmeta | 15 - .../gui/sprites/java/tab_game_clicked.png | Bin 163 -> 0 bytes .../sprites/java/tab_game_clicked.png.mcmeta | 15 - .../java/tab_game_clicked_and_hovered.png | Bin 162 -> 0 bytes .../tab_game_clicked_and_hovered.png.mcmeta | 15 - .../gui/sprites/java/tab_game_disabled.png | Bin 159 -> 0 bytes .../sprites/java/tab_game_disabled.png.mcmeta | 15 - .../gui/sprites/java/tab_game_hovered.png | Bin 148 -> 0 bytes .../sprites/java/tab_game_hovered.png.mcmeta | 15 - .../gui/sprites/java/tab_game_selected.png | Bin 163 -> 0 bytes .../sprites/java/tab_game_selected.png.mcmeta | 15 - .../java/tab_game_selected_highlighted.png | Bin 162 -> 0 bytes .../tab_game_selected_highlighted.png.mcmeta | 15 - .../textures/gui/sprites/java/tab_menu.png | Bin 178 -> 0 bytes .../gui/sprites/java/tab_menu.png.mcmeta | 10 - .../gui/sprites/java/tab_menu_clicked.png | Bin 192 -> 0 bytes .../sprites/java/tab_menu_clicked.png.mcmeta | 10 - .../java/tab_menu_clicked_and_hovered.png | Bin 186 -> 0 bytes .../tab_menu_clicked_and_hovered.png.mcmeta | 10 - .../gui/sprites/java/tab_menu_disabled.png | Bin 178 -> 0 bytes .../sprites/java/tab_menu_disabled.png.mcmeta | 10 - .../gui/sprites/java/tab_menu_hovered.png | Bin 184 -> 0 bytes .../sprites/java/tab_menu_hovered.png.mcmeta | 10 - .../gui/sprites/java/tab_menu_selected.png | Bin 192 -> 0 bytes .../sprites/java/tab_menu_selected.png.mcmeta | 10 - .../java/tab_menu_selected_highlighted.png | Bin 186 -> 0 bytes .../tab_menu_selected_highlighted.png.mcmeta | 10 - .../textures/gui/sprites/java/text_field.png | Bin 111 -> 0 bytes .../gui/sprites/java/text_field.png.mcmeta | 10 - .../sprites/java/text_field_highlighted.png | Bin 104 -> 0 bytes .../java/text_field_highlighted.png.mcmeta | 10 - .../data/archie/structure/gametest/empty.nbt | Bin 123 -> 0 bytes .../archie/testing/AnimationEasingTests.kt | 27 - .../archie/testing/ArrayUtilsTests.kt | 56 - .../archie/testing/CommonTests.kt | 18 - .../archie/testing/GameTests.kt | 16 - .../archie/testing/GuiClientHarnessTests.kt | 62 - .../archie/testing/InputPrimitiveTests.kt | 40 - .../archie/testing/LayoutCoreTests.kt | 68 - .../archie/testing/MutableEntryTests.kt | 64 - .../testing/NetworkChannelValidationTests.kt | 40 - .../archie/testing/PaddingMarginTests.kt | 125 -- .../archie/testing/ReflectionUtilsTests.kt | 66 - .../archie/testing/ResourceLocationTests.kt | 78 - .../archie/testing/ScrollableLayoutTests.kt | 24 - .../testing/ScrollableStateSmokeTests.kt | 32 - .../test/resources/junit-platform.properties | 17 - Archie/fabric/build.gradle.kts | 247 --- .../archie/APlatform.fabric.kt | 11 - .../kernelpanicsoft/archie/ArchieFabric.kt | 25 - .../archie/data/ADataGeneratorFabric.kt | 22 - .../data/ADataGeneratorPlatform.fabric.kt | 10 - .../data/ADataGeneratorPlatformInternal.kt | 64 - .../conditions/AConditionsPlatform.fabric.kt | 141 -- .../ACustomIngredientPlatform.fabric.kt | 48 - ...stomIngredientSerializerPlatform.fabric.kt | 69 - .../common/tags/ATagBuilderPlatform.fabric.kt | 23 - .../ADedicatedServerPlatform.fabric.kt | 105 -- .../ADedicatedServerPlatformInternal.kt | 46 - .../AGameTestClientHarnessInternal.kt | 51 - .../gametest/AGameTestPlatform.fabric.kt | 43 - .../gametest/AGameTestPlatformInternal.kt | 62 - .../gui/render/AFluidRenderPlatform.fabric.kt | 21 - .../AClientRegistrationPlatform.fabric.kt | 6 - .../mixin/fabric/ArchieMixinPlugin.java | 57 - .../fabric/FabricDataGenHelperMixin.java | 32 - .../fabric/FabricGameTestHelperMixin.java | 16 - .../FabricGameTestModInitializerMixin.java | 27 - .../lifecycle/MinecraftClientMixin.java | 32 - .../threading/MinecraftClientMixin.java | 44 - .../mixin/fabric/threading/ServerMixin.java | 33 - .../src/main/resources/archie.mixins.json | 20 - .../fabric/src/main/resources/fabric.mod.json | 55 - Archie/gradle.properties | 19 - Archie/gradle/wrapper/gradle-wrapper.jar | Bin 43583 -> 0 bytes .../gradle/wrapper/gradle-wrapper.properties | 7 - Archie/gradlew | 251 --- Archie/gradlew.bat | 94 - Archie/neoforge/build.gradle.kts | 259 --- Archie/neoforge/gradle.properties | 1 - Archie/neoforge/mkdocs.yml | 8 - .../archie/APlatform.neoforge.kt | 10 - .../kernelpanicsoft/archie/ArchieNeoForge.kt | 31 - .../archie/data/ADataGeneratorNeoForge.kt | 17 - .../data/ADataGeneratorPlatform.neoforge.kt | 10 - .../data/ADataGeneratorPlatformInternal.kt | 30 - .../AConditionsPlatform.neoforge.kt | 151 -- .../ACustomIngredientPlatform.neoforge.kt | 37 - ...omIngredientSerializerPlatform.neoforge.kt | 57 - .../tags/ATagBuilderPlatform.neoforge.kt | 19 - .../ADedicatedServerPlatform.neoforge.kt | 105 -- .../ADedicatedServerPlatformInternal.kt | 46 - .../AGameTestClientHarnessInternal.kt | 51 - .../gametest/AGameTestPlatform.neoforge.kt | 42 - .../gametest/AGameTestPlatformInternal.kt | 55 - .../render/AFluidRenderPlatform.neoforge.kt | 19 - .../AClientRegistrationPlatform.neoforge.kt | 18 - .../mixin/neoforge/DatagenModLoaderMixin.java | 31 - .../mixin/neoforge/GameTestHooksMixin.java | 64 - .../mixin/neoforge/GameTestRegistryMixin.java | 42 - .../StructureTemplateManagerMixin.java | 78 - .../AbstractContainerScreenDepthMixin.java | 24 - .../gui/AbstractContainerScreenMixin.java | 91 - .../neoforge/client/gui/GuiGraphicsMixin.java | 36 - .../lifecycle/MinecraftClientMixin.java | 31 - .../threading/MinecraftClientMixin.java | 44 - .../mixin/neoforge/threading/ServerMixin.java | 33 - .../resources/META-INF/neoforge.mods.toml | 58 - .../src/main/resources/archie.mixins.json | 20 - .../neoforge/src/main/resources/pack.mcmeta | 6 - Archie/settings.gradle.kts | 36 - Archie/CHANGELOG.md => CHANGELOG.md | 0 README.md | 74 +- build.gradle.kts | 208 +- .../core => core}/common/build.gradle.kts | 0 .../gui/access/SlotLayerDepthContext.java | 0 .../AbstractContainerScreenDepthMixin.java | 0 .../gui/AbstractContainerScreenMixin.java | 0 .../mixin/client/gui/GuiGraphicsMixin.java | 0 .../archie/APlatform.common.kt | 0 .../net/kernelpanicsoft/archie/Archie.kt | 0 .../kernelpanicsoft/archie/ArchieExtension.kt | 0 .../archie/block/entity/NBTBlockEntity.kt | 0 .../archie/config/CategorySpec.kt | 0 .../archie/config/ClientConfigContainer.kt | 0 .../archie/config/ClientConfigSpec.kt | 0 .../archie/config/ClientDataSpec.kt | 0 .../archie/config/CommonKeyCode.kt | 0 .../archie/config/ConfigContainer.kt | 0 .../archie/config/ConfigSpec.kt | 0 .../kernelpanicsoft/archie/config/DataSpec.kt | 0 .../archie/config/FieldType.kt | 0 .../archie/config/IConfigSerializer.kt | 0 .../archie/config/builder/ColorListBuilder.kt | 0 .../archie/config/builder/ColorMapBuilder.kt | 0 .../config/builder/ConfigFieldBuilder.kt | 0 .../archie/config/builder/DoubleMapBuilder.kt | 0 .../config/builder/DropdownFieldBuilder.kt | 0 .../archie/config/builder/FloatMapBuilder.kt | 0 .../config/builder/IntegerMapBuilder.kt | 0 .../config/builder/KeycodeListBuilder.kt | 0 .../config/builder/KeycodeMapBuilder.kt | 0 .../archie/config/builder/ListFieldBuilder.kt | 0 .../archie/config/builder/LongMapBuilder.kt | 0 .../archie/config/builder/MapFieldBuilder.kt | 0 .../config/builder/RegistryFieldBuilder.kt | 0 .../config/builder/RegistryListBuilder.kt | 0 .../config/builder/RegistryMapBuilder.kt | 0 .../archie/config/builder/SpecFieldBuilder.kt | 0 .../archie/config/builder/SpecListBuilder.kt | 0 .../archie/config/builder/SpecMapBuilder.kt | 0 .../archie/config/builder/StringMapBuilder.kt | 0 .../archie/config/builder/extensions.kt | 0 .../archie/config/entry/ConfigSpecEntry.kt | 0 .../archie/config/extensions.kt | 0 .../serializer/Json5ConfigSerializer.kt | 0 .../config/serializer/JsonConfigSerializer.kt | 0 .../config/serializer/NullConfigSerializer.kt | 0 .../config/serializer/TomlConfigSerializer.kt | 0 .../data/ADataGeneratorPlatform.common.kt | 0 .../data/common/conditions/AAndCondition.kt | 0 .../common/conditions/ABuiltinConditions.kt | 0 .../conditions/AConditionsPlatform.common.kt | 0 .../common/conditions/AEqualsCondition.kt | 0 .../data/common/conditions/AFalseCondition.kt | 0 .../data/common/conditions/AGroupCondition.kt | 0 .../common/conditions/AModLoadedCondition.kt | 0 .../data/common/conditions/ANotCondition.kt | 0 .../data/common/conditions/AOrCondition.kt | 0 .../common/conditions/APlatformCondition.kt | 0 .../common/conditions/ARegistryCondition.kt | 0 .../data/common/conditions/ATrueCondition.kt | 0 .../data/common/conditions/AXorCondition.kt | 0 .../data/common/conditions/IACondition.kt | 0 .../crafting/ingredients/AAllIngredient.kt | 0 .../crafting/ingredients/AAnyIngredient.kt | 0 .../ingredients/ABuiltinIngredients.kt | 0 .../ingredients/ACombinedIngredient.kt | 0 .../ingredients/AComponentsIngredient.kt | 0 .../ingredients/ACustomDataIngredient.kt | 0 .../ACustomIngredientPlatform.common.kt | 0 ...stomIngredientSerializerPlatform.common.kt | 0 .../ingredients/IACustomIngredient.kt | 0 .../ingredients/IACustomIngredientHolder.kt | 0 .../IACustomIngredientSerializer.kt | 0 .../archie/data/common/tags/ACommonTags.kt | 0 .../archie/events/ABasicEventObject.kt | 0 .../archie/events/AEventObject.kt | 0 .../ADedicatedServerPlatform.common.kt | 0 .../gametest/AGameTestPlatform.common.kt | 0 .../archie/gametest/ThreadingImpl.kt | 0 .../archie/gui/AUIScopeManager.kt | 0 .../archie/gui/ComposeBlockContainerMenu.kt | 0 .../archie/gui/ComposeContainerMenuBase.kt | 0 .../archie/gui/ComposeContainerScreen.kt | 0 .../archie/gui/ComposeScreen.kt | 0 .../net/kernelpanicsoft/archie/gui/Slot.kt | 0 .../gui/access/SlotHighlightClipProvider.kt | 0 .../gui/access/SlotLayerDepthProvider.kt | 0 .../archie/gui/animation/Animation.kt | 0 .../BlockEntityStateComposables.kt | 0 .../blockentity/BlockEntityStateContainer.kt | 0 .../blockentity/BlockEntityStateManager.kt | 0 .../gui/blockentity/BlockEntityStatePacket.kt | 0 .../BlockEntityStatePacketRegistry.kt | 0 .../blockentity/BlockEntityUpdatePacket.kt | 0 .../blockentity/ComposeBlockEntityState.kt | 0 .../archie/gui/composables/basic/Divider.kt | 0 .../archie/gui/composables/basic/EnergyBar.kt | 0 .../archie/gui/composables/basic/FluidTank.kt | 0 .../archie/gui/composables/basic/Icon.kt | 0 .../gui/composables/basic/ProgressBar.kt | 0 .../archie/gui/composables/basic/Spacer.kt | 0 .../archie/gui/composables/basic/Text.kt | 0 .../archie/gui/composables/basic/Texture.kt | 0 .../gui/composables/containers/Collapsible.kt | 0 .../composables/containers/ContainerPanel.kt | 0 .../gui/composables/containers/Panel.kt | 0 .../composables/containers/RootContainer.kt | 0 .../gui/composables/containers/Scrollable.kt | 0 .../gui/composables/containers/Surface.kt | 0 .../composables/containers/TabContainer.kt | 0 .../archie/gui/composables/input/Button.kt | 0 .../archie/gui/composables/input/Checkbox.kt | 0 .../archie/gui/composables/input/Clickable.kt | 0 .../gui/composables/input/ColorPicker.kt | 0 .../archie/gui/composables/input/Radio.kt | 0 .../archie/gui/composables/input/Slider.kt | 0 .../archie/gui/composables/input/Switch.kt | 0 .../composables/input/textfield/TextField.kt | 0 .../input/textfield/TextFieldCore.kt | 0 .../input/textfield/TextFieldValue.kt | 0 .../gui/composables/modal/ConfirmDialog.kt | 0 .../gui/composables/modal/DialogPrimitives.kt | 0 .../gui/composables/theme/TextureStates.kt | 0 .../gui/composables/theme/WidgetState.kt | 0 .../gui/item/ComposeItemContainerMenu.kt | 0 .../archie/gui/item/ComposeItemState.kt | 0 .../archie/gui/item/ItemContainerAccess.kt | 0 .../archie/gui/item/ItemStateComposables.kt | 0 .../archie/gui/item/ItemStateManager.kt | 0 .../archie/gui/item/ItemStatePacket.kt | 0 .../gui/item/ItemStatePacketRegistry.kt | 0 .../archie/gui/item/ItemUpdatePacket.kt | 0 .../archie/gui/item/SyncedItemHolder.kt | 0 .../kernelpanicsoft/archie/gui/layer/Layer.kt | 0 .../archie/gui/layer/LayerStackManager.kt | 0 .../archie/gui/layout/Alignment.kt | 0 .../archie/gui/layout/Arrangement.kt | 0 .../kernelpanicsoft/archie/gui/layout/Box.kt | 0 .../archie/gui/layout/Column.kt | 0 .../archie/gui/layout/Helpers.kt | 0 .../archie/gui/layout/IntCoordinates.kt | 0 .../archie/gui/layout/IntRect.kt | 0 .../archie/gui/layout/Layout.kt | 0 .../archie/gui/layout/LayoutDirection.kt | 0 .../archie/gui/layout/LayoutNode.kt | 0 .../archie/gui/layout/MeasurePolicy.kt | 0 .../kernelpanicsoft/archie/gui/layout/Row.kt | 0 .../gui/layout/RowColumnMeasurePolicy.kt | 0 .../kernelpanicsoft/archie/gui/layout/Size.kt | 0 .../archie/gui/modifiers/Constraints.kt | 0 .../archie/gui/modifiers/DebugModifier.kt | 0 .../archie/gui/modifiers/DrawModifier.kt | 0 .../gui/modifiers/LayoutChangingModifier.kt | 0 .../archie/gui/modifiers/Modifier.kt | 0 .../modifiers/OnGloballyPositionedModifier.kt | 0 .../gui/modifiers/OnSizeChangedModifier.kt | 0 .../archie/gui/modifiers/SizeModifier.kt | 0 .../appearance/BackgroundModifier.kt | 0 .../modifiers/appearance/BorderModifier.kt | 0 .../modifiers/appearance/TextureModifier.kt | 0 .../modifiers/appearance/TooltipModifier.kt | 0 .../archie/gui/modifiers/input/InputEvent.kt | 0 .../modifiers/input/OnCharTypedModifier.kt | 0 .../gui/modifiers/input/OnKeyEventModifier.kt | 0 .../modifiers/input/OnPointerEventModifier.kt | 0 .../gui/modifiers/position/MarginModifier.kt | 0 .../gui/modifiers/position/OffsetModifier.kt | 0 .../gui/modifiers/position/PaddingModifier.kt | 0 .../archie/gui/modifiers/position/ZIndex.kt | 0 .../archie/gui/nodes/LayoutNodeApplier.kt | 0 .../archie/gui/nodes/UINode.kt | 0 .../gui/render/AFluidRenderPlatform.common.kt | 0 .../archie/gui/theme/ComposableTheme.kt | 0 .../kernelpanicsoft/archie/gui/theme/Theme.kt | 0 .../archie/gui/util/HsvColor.kt | 0 .../kernelpanicsoft/archie/gui/util/KColor.kt | 0 .../archie/gui/util/extension/GuiGraphics.kt | 0 .../archie/gui/util/extension/Screen.kt | 0 .../gui/util/extension/VertexConsumer.kt | 0 .../archie/networking/ArchieNetworkChannel.kt | 0 .../archie/networking/IPacketContext.kt | 0 .../archie/networking/NetworkChannel.kt | 0 .../AClientRegistrationPlatform.common.kt | 0 .../archie/registries/ACreativeTabRegistry.kt | 0 .../registries/ADeferredRegistryHolder.kt | 0 .../archie/registries/BlockRegistryHelper.kt | 0 .../registries/CreativeTabRegistryHelper.kt | 0 .../archie/registries/RegistrarHelper.kt | 0 .../archie/registries/RegistryHelper.kt | 0 .../archie/registries/extensions.kt | 0 .../SerializationReloadListener.kt | 0 .../serialization/ArchieDataAttachmentImpl.kt | 0 .../serialization/AttachmentRegistry.kt | 0 .../archie/serialization/DataAttachment.kt | 0 .../serialization/FluidStackNBTHolderImpl.kt | 0 .../serialization/ItemStackNBTHolderImpl.kt | 0 .../archie/serialization/KOps.kt | 0 .../archie/serialization/NBT.kt | 0 .../archie/serialization/NBTHolder.kt | 0 .../archie/serialization/NBTHolderImpl.kt | 0 .../archie/serialization/ObservableList.kt | 0 .../archie/serialization/ObservableMap.kt | 0 .../serialization/SerializationManager.kt | 0 .../archie/serialization/Sync.kt | 0 .../archie/serialization/Utils.kt | 0 .../serializers/BuiltinSerializers.kt | 0 .../serializers/MinecraftSerializers.kt | 0 .../transfer/ArchieCapabilityExposure.kt | 0 .../archie/transfer/ArchieEnergyStorage.kt | 0 .../archie/transfer/ArchieFluidSlot.kt | 0 .../archie/transfer/ArchieFluidStorage.kt | 0 .../archie/transfer/ArchieItemMenuSlot.kt | 0 .../archie/transfer/ArchieItemSlot.kt | 0 .../archie/transfer/ArchieItemStorage.kt | 0 .../archie/transfer/VanillaMenuSlot.kt | 0 .../net/kernelpanicsoft/archie/util/Array.kt | 0 .../kernelpanicsoft/archie/util/Component.kt | 0 .../net/kernelpanicsoft/archie/util/Env.kt | 0 .../archie/util/MutableEntry.kt | 0 .../kernelpanicsoft/archie/util/Properties.kt | 0 .../kernelpanicsoft/archie/util/Reflect.kt | 0 .../archie/util/ResourceLocation.kt | 0 .../net/kernelpanicsoft/archie/util/Tile.kt | 0 .../main/resources/archie-common.mixins.json | 0 .../src/main/resources/archie.accesswidener | 0 .../src/main/resources/archie.common.json | 0 .../archie/archie_themes/java.theme.json | 0 .../archie/archie_themes/java/button.json | 0 .../archie/archie_themes/java/checkbox.json | 0 .../archie_themes/java/dark/surface.json | 0 .../archie/archie_themes/java/energy_bar.json | 0 .../archie/archie_themes/java/fluid_tank.json | 0 .../archie_themes/java/progress_bar.json | 0 .../archie/archie_themes/java/radio.json | 0 .../archie/archie_themes/java/slider.json | 0 .../archie_themes/java/slider_handle.json | 0 .../archie/archie_themes/java/slot.json | 0 .../archie_themes/java/small_checkbox.json | 0 .../archie/archie_themes/java/surface.json | 0 .../archie_themes/java/switch_thumb.json | 0 .../archie_themes/java/switch_track.json | 0 .../archie/archie_themes/java/tab_game.json | 0 .../archie/archie_themes/java/tab_menu.json | 0 .../archie/archie_themes/java/text_field.json | 0 .../resources/assets/archie/atlases/java.json | 0 .../main/resources/assets/archie/banner.png | Bin .../src/main/resources/assets/archie/icon.png | Bin .../textures/gui/sprites/java/button.png | Bin .../gui/sprites/java/button.png.mcmeta | 0 .../gui/sprites/java/button_disabled.png | Bin .../sprites/java/button_disabled.png.mcmeta | 0 .../gui/sprites/java/button_highlighted.png | Bin .../java/button_highlighted.png.mcmeta | 0 .../textures/gui/sprites/java/checkbox.png | Bin .../gui/sprites/java/checkbox_clicked.png | Bin .../java/checkbox_clicked_and_hovered.png | Bin .../gui/sprites/java/checkbox_hovered.png | Bin .../textures/gui/sprites/java/energy_bar.png | Bin .../gui/sprites/java/energy_bar.png.mcmeta | 0 .../textures/gui/sprites/java/fluid_tank.png | Bin .../gui/sprites/java/fluid_tank.png.mcmeta | 0 .../gui/sprites/java/progress_bar.png | Bin .../gui/sprites/java/progress_bar.png.mcmeta | 0 .../textures/gui/sprites/java/radio.png | Bin .../gui/sprites/java/radio_clicked.png | Bin .../java/radio_clicked_and_hovered.png | Bin .../gui/sprites/java/radio_disabled.png | Bin .../gui/sprites/java/radio_hovered.png | Bin .../textures/gui/sprites/java/slider.png | Bin .../gui/sprites/java/slider.png.mcmeta | 0 .../gui/sprites/java/slider_handle.png | Bin .../gui/sprites/java/slider_handle.png.mcmeta | 0 .../java/slider_handle_highlighted.png | Bin .../java/slider_handle_highlighted.png.mcmeta | 0 .../gui/sprites/java/slider_highlighted.png | Bin .../java/slider_highlighted.png.mcmeta | 0 .../archie/textures/gui/sprites/java/slot.png | Bin .../gui/sprites/java/small_checkbox.png | Bin .../sprites/java/small_checkbox_clicked.png | Bin .../textures/gui/sprites/java/surface.png | Bin .../gui/sprites/java/surface.png.mcmeta | 0 .../gui/sprites/java/surface_dark.png | Bin .../gui/sprites/java/surface_dark.png.mcmeta | 0 .../gui/sprites/java/surface_inset.png | Bin .../gui/sprites/java/surface_inset.png.mcmeta | 0 .../gui/sprites/java/surface_inset_dark.png | Bin .../java/surface_inset_dark.png.mcmeta | 0 .../gui/sprites/java/switch_thumb.png | Bin .../sprites/java/switch_thumb_disabled.png | Bin .../gui/sprites/java/switch_track.png | Bin .../gui/sprites/java/switch_track_clicked.png | Bin .../java/switch_track_clicked_and_hovered.png | Bin .../sprites/java/switch_track_disabled.png | Bin .../gui/sprites/java/switch_track_hovered.png | Bin .../textures/gui/sprites/java/tab_game.png | Bin .../gui/sprites/java/tab_game.png.mcmeta | 0 .../gui/sprites/java/tab_game_clicked.png | Bin .../sprites/java/tab_game_clicked.png.mcmeta | 0 .../java/tab_game_clicked_and_hovered.png | Bin .../tab_game_clicked_and_hovered.png.mcmeta | 0 .../gui/sprites/java/tab_game_disabled.png | Bin .../sprites/java/tab_game_disabled.png.mcmeta | 0 .../gui/sprites/java/tab_game_hovered.png | Bin .../sprites/java/tab_game_hovered.png.mcmeta | 0 .../gui/sprites/java/tab_game_selected.png | Bin .../sprites/java/tab_game_selected.png.mcmeta | 0 .../java/tab_game_selected_highlighted.png | Bin .../tab_game_selected_highlighted.png.mcmeta | 0 .../textures/gui/sprites/java/tab_menu.png | Bin .../gui/sprites/java/tab_menu.png.mcmeta | 0 .../gui/sprites/java/tab_menu_clicked.png | Bin .../sprites/java/tab_menu_clicked.png.mcmeta | 0 .../java/tab_menu_clicked_and_hovered.png | Bin .../tab_menu_clicked_and_hovered.png.mcmeta | 0 .../gui/sprites/java/tab_menu_disabled.png | Bin .../sprites/java/tab_menu_disabled.png.mcmeta | 0 .../gui/sprites/java/tab_menu_hovered.png | Bin .../sprites/java/tab_menu_hovered.png.mcmeta | 0 .../gui/sprites/java/tab_menu_selected.png | Bin .../sprites/java/tab_menu_selected.png.mcmeta | 0 .../java/tab_menu_selected_highlighted.png | Bin .../tab_menu_selected_highlighted.png.mcmeta | 0 .../textures/gui/sprites/java/text_field.png | Bin .../gui/sprites/java/text_field.png.mcmeta | 0 .../sprites/java/text_field_highlighted.png | Bin .../java/text_field_highlighted.png.mcmeta | 0 .../data/archie/structure/gametest/empty.nbt | Bin .../core => core}/fabric/build.gradle.kts | 0 .../mixin/fabric/ArchieMixinPlugin.java | 0 .../threading/MinecraftClientMixin.java | 0 .../mixin/fabric/threading/ServerMixin.java | 0 .../net/kernelpanicsoft/archie/APlatform.kt | 0 .../kernelpanicsoft/archie/ArchieFabric.kt | 0 .../data/ADataGeneratorPlatform.fabric.kt | 0 .../conditions/AConditionsPlatform.fabric.kt | 0 .../ACustomIngredientPlatform.fabric.kt | 0 ...stomIngredientSerializerPlatform.fabric.kt | 0 .../gametest/ADedicatedServerPlatform.kt | 0 .../ADedicatedServerPlatformInternal.kt | 0 .../archie/gametest/AGameTestPlatform.kt | 0 .../gametest/AGameTestPlatformInternal.kt | 0 .../archie/gui/render/AFluidRenderPlatform.kt | 0 .../registries/AClientRegistrationPlatform.kt | 0 .../src/main/resources/archie.mixins.json | 0 .../fabric/src/main/resources/fabric.mod.json | 0 .../core => core}/neoforge/build.gradle.kts | 0 .../core => core}/neoforge/gradle.properties | 0 .../threading/MinecraftClientMixin.java | 0 .../mixin/neoforge/threading/ServerMixin.java | 0 .../net/kernelpanicsoft/archie/APlatform.kt | 0 .../kernelpanicsoft/archie/ArchieNeoForge.kt | 0 .../data/ADataGeneratorPlatform.neoforge.kt | 0 .../AConditionsPlatform.neoforge.kt | 0 .../ACustomIngredientPlatform.neoforge.kt | 0 ...omIngredientSerializerPlatform.neoforge.kt | 0 .../gametest/ADedicatedServerPlatform.kt | 0 .../ADedicatedServerPlatformInternal.kt | 0 .../archie/gametest/AGameTestPlatform.kt | 0 .../gametest/AGameTestPlatformInternal.kt | 0 .../archie/gui/render/AFluidRenderPlatform.kt | 0 .../registries/AClientRegistrationPlatform.kt | 0 .../resources/META-INF/neoforge.mods.toml | 0 .../src/main/resources/archie.mixins.json | 0 .../common/build.gradle.kts | 0 .../archie/data/ADataGenerator.kt | 0 .../archie/data/ADatagenEventObject.kt | 0 .../archie/data/IADataProvider.kt | 0 .../archie/data/client/ALanguageProvider.kt | 0 .../data/client/model/ABlockModelBuilder.kt | 0 .../data/client/model/ABlockModelProvider.kt | 0 .../data/client/model/ABlockStateProvider.kt | 0 .../data/client/model/AConfiguredModel.kt | 0 .../data/client/model/ACustomLoaderBuilder.kt | 0 .../data/client/model/AItemModelBuilder.kt | 0 .../data/client/model/AItemModelProvider.kt | 0 .../archie/data/client/model/AModelBuilder.kt | 0 .../archie/data/client/model/AModelFile.kt | 0 .../data/client/model/AModelProvider.kt | 0 .../model/AMultiPartBlockStateBuilder.kt | 0 .../client/model/AVariantBlockStateBuilder.kt | 0 .../client/model/IAGeneratedBlockState.kt | 0 .../common/conditions/AConditionBuilder.kt | 0 .../ADatagenConditionsPlatform.common.kt | 0 .../data/common/conditions/Extensions.kt | 0 .../data/common/crafting/ARecipeProvider.kt | 0 .../recipies/ArchieCookingRecipeBuilder.kt | 0 .../recipies/ArchieShapedRecipeBuilder.kt | 0 .../recipies/ArchieShapelessRecipeBuilder.kt | 0 .../crafting/recipies/IARecipeBuilder.kt | 0 .../archie/data/common/tags/ATagBuilder.kt | 0 .../common/tags/ATagBuilderPlatform.common.kt | 0 .../archie/data/common/tags/ATagsProvider.kt | 0 .../archie/data/common/tags/IATagBuilder.kt | 0 .../archie/data/internal/ArchieDatagen.kt | 0 .../data/internal/DatagenArchieExtension.kt | 0 .../common/tags/AInternalBiomeTagsProvider.kt | 0 .../common/tags/AInternalBlockTagsProvider.kt | 0 .../tags/AInternalEntityTypeTagsProvider.kt | 0 .../common/tags/AInternalFluidTagsProvider.kt | 0 .../common/tags/AInternalItemTagsProvider.kt | 0 .../archie/data/util/TransformationHelper.kt | 0 .../archie/events/ADatagenEvents.kt | 0 ...net.kernelpanicsoft.archie.ArchieExtension | 0 .../fabric/build.gradle.kts | 0 .../fabric/FabricDataGenHelperMixin.java | 0 .../archie/data/ADataGeneratorFabric.kt | 0 .../data/ADataGeneratorPlatformInternal.kt | 0 .../ADatagenConditionsPlatform.fabric.kt | 0 .../data/common/tags/ATagBuilderPlatform.kt | 0 .../main/resources/archie_datagen.mixins.json | 0 .../fabric/src/main/resources/fabric.mod.json | 0 .../neoforge/build.gradle.kts | 0 .../neoforge/gradle.properties | 0 .../mixin/neoforge/DatagenModLoaderMixin.java | 0 .../archie/data/ADataGeneratorNeoForge.kt | 0 .../data/ADataGeneratorPlatformInternal.kt | 0 .../ADatagenConditionsPlatform.neoforge.kt | 0 .../data/common/tags/ATagBuilderPlatform.kt | 0 .../resources/META-INF/neoforge.mods.toml | 0 .../main/resources/archie_datagen.mixins.json | 0 {Archie/docs => docs}/assets/icon.svg | 0 {Archie/docs => docs}/config.md | 0 {Archie/docs => docs}/datagen.md | 8 +- {Archie/docs => docs}/events.md | 0 {Archie/docs => docs}/gametest.md | 10 +- {Archie/docs => docs}/gui.md | 0 {Archie/docs => docs}/index.md | 2 +- {Archie/docs => docs}/networking.md | 0 {Archie/docs => docs}/news/index.md | 0 {Archie/docs => docs}/overrides/main.html | 0 {Archie/docs => docs}/registries.md | 0 {Archie/docs => docs}/resource-packs.md | 0 {Archie/docs => docs}/serialization.md | 0 {Archie/docs => docs}/transfer.md | 0 .../common/build.gradle.kts | 0 .../archie/events/AGametestEvents.kt | 0 .../archie/gametest/AClientGameTestHarness.kt | 0 .../archie/gametest/AGameTestEventObject.kt | 0 .../gametest/ComposeScreenTestContext.kt | 0 .../archie/gametest/GameTestAssertions.kt | 0 .../archie/gametest/NoOpGameTest.kt | 0 .../archie/gametest/ScreenshotComparer.kt | 0 .../gametest/ScreenshotComparisonAlgorithm.kt | 0 .../archie/gametest/ScreenshotManager.kt | 0 .../archie/gametest/VerboseTestReporter.kt | 0 .../gametest/internal/ArchieGameTest.kt | 0 .../internal/GametestArchieExtension.kt | 0 .../internal/tests/ArchieItemHandlerTests.kt | 0 .../tests/BlockEntityNBTHolderTests.kt | 0 .../tests/BlockEntityStateManagerTests.kt | 0 .../internal/tests/ComposeRenderingTests.kt | 0 .../internal/tests/InputComponentsGameTest.kt | 0 .../tests/LayoutComponentsGameTest.kt | 0 .../internal/tests/ModalComponentsGameTest.kt | 0 .../gametest/junit/GameTestGradleExecutor.kt | 0 .../junit/GameTestGradleInvocation.kt | 0 .../archie/gametest/junit/GameTestRunner.kt | 0 ...net.kernelpanicsoft.archie.ArchieExtension | 0 .../fabric/build.gradle.kts | 0 .../fabric/FabricGameTestHelperMixin.java | 0 .../FabricGameTestModInitializerMixin.java | 0 .../lifecycle/MinecraftClientMixin.java | 0 .../AGameTestClientHarnessInternal.kt | 0 .../gametest/AGameTestRegistrationBridge.kt | 0 .../resources/archie_gametest.mixins.json | 0 .../fabric/src/main/resources/fabric.mod.json | 0 .../neoforge/build.gradle.kts | 0 .../neoforge/gradle.properties | 0 .../mixin/neoforge/GameTestHooksMixin.java | 0 .../lifecycle/MinecraftClientMixin.java | 0 .../AGameTestClientHarnessInternal.kt | 0 .../gametest/AGameTestRegistrationBridge.kt | 0 .../resources/META-INF/neoforge.mods.toml | 0 .../resources/archie_gametest.mixins.json | 0 gradle.properties | 1 - gradle/wrapper/gradle-wrapper.jar | Bin 43462 -> 43583 bytes gradlew | 6 +- gradlew.bat | 2 + Archie/mkdocs.yml => mkdocs.yml | 2 +- settings.gradle.kts | 48 +- test/common/build.gradle.kts | 48 + .../kernelpanicsoft/archie/test/ArchieTest.kt | 6 +- .../archie/test/BlockRegistry.kt | 0 .../archie/test/GuiRegistry.kt | 0 .../archie/test/ItemRegistry.kt | 0 .../kernelpanicsoft/archie/test/TestBlock.kt | 0 .../kernelpanicsoft/archie/test/TestItem.kt | 0 .../archie/test/TestItemContainerScreen.kt | 0 .../archie/test/TestItemMenu.kt | 0 .../archie/test/TestItemScreen.kt | 0 .../kernelpanicsoft/archie/test/TestKind.kt | 0 .../kernelpanicsoft/archie/test/TestMenu.kt | 0 .../kernelpanicsoft/archie/test/TestScreen.kt | 0 .../kernelpanicsoft/archie/test/TestTile.kt | 0 .../archie/test/TileRegistry.kt | 0 .../archie/test/data/ArchieTestDatagen.kt | 0 .../test/gametest/ArchieTestGameTest.kt | 6 +- .../gametest/CapabilityLookupTestFixtures.kt | 0 .../test/gametest/CapabilityLookupTests.kt | 0 .../ComposeItemContainerMenuClientTests.kt | 0 .../gametest/ComposeItemContainerMenuTests.kt | 0 .../gametest/DataAttachmentTestFixtures.kt | 0 .../test/gametest/DataAttachmentTests.kt | 0 .../test/gametest/TestScreenGameTest.kt | 0 .../main/resources/archie_test.accesswidener | 0 .../main/resources/archie_test.common.json | 0 .../resources/assets/archie_test/banner.png | Bin .../resources/assets/archie_test/icon.png | Bin .../archie/test/testing/GameTests.kt | 0 .../test/resources/junit-platform.properties | 0 test/fabric/build.gradle.kts | 154 ++ .../archie/test/ArchieTestFabric.kt | 0 .../fabric/src/main/resources/fabric.mod.json | 11 +- test/neoforge/build.gradle.kts | 171 ++ .../neoforge/gradle.properties | 1 - .../archie/test/ArchieTestNeoForge.kt | 0 .../resources/META-INF/neoforge.mods.toml | 25 +- .../neoforge/src/main/resources/pack.mcmeta | 0 1004 files changed, 766 insertions(+), 46537 deletions(-) delete mode 100644 Archie-Core/build.gradle.kts delete mode 100644 Archie-Core/gradle.properties delete mode 100644 Archie-Core/gradle/wrapper/gradle-wrapper.jar delete mode 100644 Archie-Core/gradle/wrapper/gradle-wrapper.properties delete mode 100755 Archie-Core/gradlew delete mode 100644 Archie-Core/gradlew.bat delete mode 100644 Archie-Core/settings.gradle.kts delete mode 100644 Archie-Test/.architectury-transformer/debug.log delete mode 100644 Archie-Test/build.gradle.kts delete mode 100644 Archie-Test/common/build.gradle.kts delete mode 100644 Archie-Test/fabric/build.gradle.kts delete mode 100644 Archie-Test/gradle.properties delete mode 100644 Archie-Test/gradle/wrapper/gradle-wrapper.jar delete mode 100644 Archie-Test/gradle/wrapper/gradle-wrapper.properties delete mode 100755 Archie-Test/gradlew delete mode 100644 Archie-Test/gradlew.bat delete mode 100644 Archie-Test/neoforge/build.gradle.kts delete mode 100644 Archie-Test/settings.gradle.kts delete mode 100644 Archie/build.gradle.kts delete mode 100644 Archie/common/build.gradle.kts delete mode 100644 Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt delete mode 100644 Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt delete mode 100644 Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt delete mode 100644 Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt delete mode 100644 Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt delete mode 100644 Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt delete mode 100644 Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt delete mode 100644 Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt delete mode 100644 Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt delete mode 100644 Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt delete mode 100644 Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ComposeRenderingTests.kt delete mode 100644 Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/InputComponentsGameTest.kt delete mode 100644 Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/LayoutComponentsGameTest.kt delete mode 100644 Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ModalComponentsGameTest.kt delete mode 100644 Archie/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.common.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/block/entity/NBTBlockEntity.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CategorySpec.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigContainer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigSpec.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientDataSpec.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CommonKeyCode.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigContainer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigSpec.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/DataSpec.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/FieldType.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/IConfigSerializer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorListBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorMapBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ConfigFieldBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DoubleMapBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DropdownFieldBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/FloatMapBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/IntegerMapBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeListBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeMapBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ListFieldBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/LongMapBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/MapFieldBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryFieldBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryListBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryMapBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecFieldBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecListBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecMapBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/StringMapBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/extensions.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/entry/ConfigSpecEntry.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/extensions.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/Json5ConfigSerializer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/JsonConfigSerializer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/NullConfigSerializer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/TomlConfigSerializer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/IADataProvider.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/ALanguageProvider.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelProvider.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockStateProvider.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AConfiguredModel.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ACustomLoaderBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelProvider.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelFile.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelProvider.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AMultiPartBlockStateBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AVariantBlockStateBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/IAGeneratedBlockState.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AAndCondition.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ABuiltinConditions.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AEqualsCondition.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AFalseCondition.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AGroupCondition.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AModLoadedCondition.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ANotCondition.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AOrCondition.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/APlatformCondition.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ARegistryCondition.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ATrueCondition.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AXorCondition.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAllIngredient.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAnyIngredient.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ABuiltinIngredients.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACombinedIngredient.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AComponentsIngredient.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomDataIngredient.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.common.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.common.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredient.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientHolder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientSerializer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieCookingRecipeBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapedRecipeBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapelessRecipeBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.common.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagsProvider.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/IATagBuilder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEvents.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ComposeScreenTestContext.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparisonAlgorithm.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotManager.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/VerboseTestReporter.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/AUIScopeManager.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeBlockContainerMenu.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerMenuBase.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerScreen.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/Slot.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotHighlightClipProvider.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthProvider.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/animation/Animation.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateComposables.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateContainer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateManager.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacket.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityUpdatePacket.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/ComposeBlockEntityState.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Divider.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Icon.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Spacer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Text.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Texture.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Collapsible.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/ContainerPanel.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Panel.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/RootContainer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Scrollable.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Surface.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/TabContainer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Button.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Checkbox.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Clickable.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/ColorPicker.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Radio.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Slider.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Switch.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextField.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldCore.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldValue.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/ConfirmDialog.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/DialogPrimitives.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/TextureStates.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/WidgetState.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemContainerMenu.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemState.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemContainerAccess.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateComposables.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateManager.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacket.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacketRegistry.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemUpdatePacket.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/SyncedItemHolder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/Layer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/LayerStackManager.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Alignment.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Arrangement.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Box.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Column.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Helpers.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntCoordinates.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntRect.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Layout.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutDirection.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutNode.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/MeasurePolicy.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Row.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/RowColumnMeasurePolicy.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Size.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Constraints.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DebugModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DrawModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/LayoutChangingModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Modifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnGloballyPositionedModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnSizeChangedModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/SizeModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BackgroundModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BorderModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TextureModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TooltipModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/InputEvent.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnCharTypedModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnKeyEventModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnPointerEventModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/MarginModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/OffsetModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/PaddingModifier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/ZIndex.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/LayoutNodeApplier.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/UINode.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/ComposableTheme.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/Theme.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/HsvColor.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/KColor.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/GuiGraphics.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/Screen.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/VertexConsumer.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/IPacketContext.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.common.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ACreativeTabRegistry.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ADeferredRegistryHolder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/BlockRegistryHelper.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/CreativeTabRegistryHelper.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistrarHelper.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistryHelper.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/extensions.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/resourcepacks/SerializationReloadListener.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ArchieDataAttachmentImpl.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/AttachmentRegistry.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/DataAttachment.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/KOps.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBT.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableList.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableMap.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/SerializationManager.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Sync.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Utils.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/BuiltinSerializers.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/MinecraftSerializers.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemMenuSlot.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemSlot.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemStorage.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/VanillaMenuSlot.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Array.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Component.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Env.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/MutableEntry.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Properties.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Reflect.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt delete mode 100644 Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Tile.kt delete mode 100644 Archie/common/src/main/mixin/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenDepthMixin.java delete mode 100644 Archie/common/src/main/mixin/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java delete mode 100644 Archie/common/src/main/mixin/net/kernelpanicsoft/archie/mixin/client/gui/GuiGraphicsMixin.java delete mode 100644 Archie/common/src/main/resources/archie-common.mixins.json delete mode 100644 Archie/common/src/main/resources/archie.accesswidener delete mode 100644 Archie/common/src/main/resources/archie.common.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java.theme.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/button.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/checkbox.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/dark/surface.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/radio.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/slider.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/slider_handle.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/slot.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/small_checkbox.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/surface.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/switch_thumb.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/switch_track.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/tab_game.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/tab_menu.json delete mode 100644 Archie/common/src/main/resources/assets/archie/archie_themes/java/text_field.json delete mode 100644 Archie/common/src/main/resources/assets/archie/atlases/java.json delete mode 100644 Archie/common/src/main/resources/assets/archie/banner.png delete mode 100644 Archie/common/src/main/resources/assets/archie/icon.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked_and_hovered.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_hovered.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked_and_hovered.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_disabled.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_hovered.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slot.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox_clicked.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb_disabled.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked_and_hovered.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_disabled.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_hovered.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png.mcmeta delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png delete mode 100644 Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png.mcmeta delete mode 100644 Archie/common/src/main/resources/data/archie/structure/gametest/empty.nbt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/AnimationEasingTests.kt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ArrayUtilsTests.kt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/CommonTests.kt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GuiClientHarnessTests.kt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/InputPrimitiveTests.kt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/LayoutCoreTests.kt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/MutableEntryTests.kt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/NetworkChannelValidationTests.kt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/PaddingMarginTests.kt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ReflectionUtilsTests.kt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ResourceLocationTests.kt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableLayoutTests.kt delete mode 100644 Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableStateSmokeTests.kt delete mode 100644 Archie/common/src/test/resources/junit-platform.properties delete mode 100644 Archie/fabric/build.gradle.kts delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.fabric.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorFabric.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.fabric.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.fabric.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.fabric.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.fabric.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.fabric.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.fabric.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.fabric.kt delete mode 100644 Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.fabric.kt delete mode 100644 Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/ArchieMixinPlugin.java delete mode 100644 Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java delete mode 100644 Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestHelperMixin.java delete mode 100644 Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java delete mode 100644 Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/lifecycle/MinecraftClientMixin.java delete mode 100644 Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java delete mode 100644 Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java delete mode 100644 Archie/fabric/src/main/resources/archie.mixins.json delete mode 100644 Archie/fabric/src/main/resources/fabric.mod.json delete mode 100644 Archie/gradle.properties delete mode 100644 Archie/gradle/wrapper/gradle-wrapper.jar delete mode 100644 Archie/gradle/wrapper/gradle-wrapper.properties delete mode 100755 Archie/gradlew delete mode 100644 Archie/gradlew.bat delete mode 100644 Archie/neoforge/build.gradle.kts delete mode 100644 Archie/neoforge/gradle.properties delete mode 100644 Archie/neoforge/mkdocs.yml delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.neoforge.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorNeoForge.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.neoforge.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.neoforge.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.neoforge.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.neoforge.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.neoforge.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.neoforge.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.neoforge.kt delete mode 100644 Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.neoforge.kt delete mode 100644 Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java delete mode 100644 Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java delete mode 100644 Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/GameTestRegistryMixin.java delete mode 100644 Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/StructureTemplateManagerMixin.java delete mode 100644 Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenDepthMixin.java delete mode 100644 Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenMixin.java delete mode 100644 Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/GuiGraphicsMixin.java delete mode 100644 Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/lifecycle/MinecraftClientMixin.java delete mode 100644 Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java delete mode 100644 Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java delete mode 100644 Archie/neoforge/src/main/resources/META-INF/neoforge.mods.toml delete mode 100644 Archie/neoforge/src/main/resources/archie.mixins.json delete mode 100644 Archie/neoforge/src/main/resources/pack.mcmeta delete mode 100644 Archie/settings.gradle.kts rename Archie/CHANGELOG.md => CHANGELOG.md (100%) rename {Archie-Core/core => core}/common/build.gradle.kts (100%) rename {Archie-Core/core => core}/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java (100%) rename {Archie-Core/core => core}/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenDepthMixin.java (100%) rename {Archie-Core/core => core}/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java (100%) rename {Archie-Core/core => core}/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/GuiGraphicsMixin.java (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.common.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/block/entity/NBTBlockEntity.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CategorySpec.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigContainer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigSpec.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientDataSpec.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CommonKeyCode.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigContainer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigSpec.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/DataSpec.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/FieldType.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/IConfigSerializer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorListBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorMapBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ConfigFieldBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DoubleMapBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DropdownFieldBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/FloatMapBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/IntegerMapBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeListBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeMapBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ListFieldBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/LongMapBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/MapFieldBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryFieldBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryListBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryMapBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecFieldBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecListBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecMapBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/StringMapBuilder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/extensions.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/entry/ConfigSpecEntry.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/extensions.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/Json5ConfigSerializer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/JsonConfigSerializer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/NullConfigSerializer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/TomlConfigSerializer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AAndCondition.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ABuiltinConditions.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AEqualsCondition.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AFalseCondition.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AGroupCondition.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AModLoadedCondition.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ANotCondition.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AOrCondition.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/APlatformCondition.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ARegistryCondition.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ATrueCondition.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AXorCondition.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAllIngredient.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAnyIngredient.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ABuiltinIngredients.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACombinedIngredient.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AComponentsIngredient.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomDataIngredient.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.common.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.common.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredient.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientHolder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientSerializer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/AUIScopeManager.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeBlockContainerMenu.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerMenuBase.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerScreen.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/Slot.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotHighlightClipProvider.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthProvider.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/animation/Animation.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateComposables.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateContainer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateManager.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacket.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityUpdatePacket.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/ComposeBlockEntityState.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Divider.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Icon.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Spacer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Text.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Texture.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Collapsible.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/ContainerPanel.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Panel.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/RootContainer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Scrollable.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Surface.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/TabContainer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Button.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Checkbox.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Clickable.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/ColorPicker.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Radio.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Slider.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Switch.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextField.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldCore.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldValue.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/ConfirmDialog.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/DialogPrimitives.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/TextureStates.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/WidgetState.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemContainerMenu.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemState.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemContainerAccess.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateComposables.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateManager.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacket.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacketRegistry.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemUpdatePacket.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/SyncedItemHolder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/Layer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/LayerStackManager.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Alignment.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Arrangement.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Box.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Column.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Helpers.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntCoordinates.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntRect.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Layout.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutDirection.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutNode.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/MeasurePolicy.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Row.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/RowColumnMeasurePolicy.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Size.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Constraints.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DebugModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DrawModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/LayoutChangingModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Modifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnGloballyPositionedModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnSizeChangedModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/SizeModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BackgroundModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BorderModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TextureModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TooltipModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/InputEvent.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnCharTypedModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnKeyEventModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnPointerEventModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/MarginModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/OffsetModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/PaddingModifier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/ZIndex.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/LayoutNodeApplier.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/UINode.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/ComposableTheme.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/Theme.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/HsvColor.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/KColor.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/GuiGraphics.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/Screen.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/VertexConsumer.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/IPacketContext.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.common.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ACreativeTabRegistry.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ADeferredRegistryHolder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/BlockRegistryHelper.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/CreativeTabRegistryHelper.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistrarHelper.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistryHelper.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/extensions.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/resourcepacks/SerializationReloadListener.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ArchieDataAttachmentImpl.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/AttachmentRegistry.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/DataAttachment.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/KOps.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBT.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableList.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableMap.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/SerializationManager.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Sync.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Utils.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/BuiltinSerializers.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/MinecraftSerializers.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemMenuSlot.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemSlot.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemStorage.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/VanillaMenuSlot.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Array.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Component.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Env.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/util/MutableEntry.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Properties.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Reflect.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt (100%) rename {Archie-Core/core => core}/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Tile.kt (100%) rename {Archie-Core/core => core}/common/src/main/resources/archie-common.mixins.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/archie.accesswidener (100%) rename {Archie-Core/core => core}/common/src/main/resources/archie.common.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java.theme.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/button.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/checkbox.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/dark/surface.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/radio.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/slider.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/slider_handle.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/slot.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/small_checkbox.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/surface.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/switch_thumb.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/switch_track.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/tab_game.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/tab_menu.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/archie_themes/java/text_field.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/atlases/java.json (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/banner.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/icon.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked_and_hovered.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_hovered.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked_and_hovered.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_disabled.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_hovered.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/slot.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox_clicked.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb_disabled.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked_and_hovered.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_disabled.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_hovered.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png (100%) rename {Archie-Core/core => core}/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png.mcmeta (100%) rename {Archie-Core/core => core}/common/src/main/resources/data/archie/structure/gametest/empty.nbt (100%) rename {Archie-Core/core => core}/fabric/build.gradle.kts (100%) rename {Archie-Core/core => core}/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/ArchieMixinPlugin.java (100%) rename {Archie-Core/core => core}/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java (100%) rename {Archie-Core/core => core}/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java (100%) rename {Archie-Core/core => core}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt (100%) rename {Archie-Core/core => core}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt (100%) rename {Archie-Core/core => core}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt (100%) rename {Archie-Core/core => core}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.fabric.kt (100%) rename {Archie-Core/core => core}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.fabric.kt (100%) rename {Archie-Core/core => core}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.fabric.kt (100%) rename {Archie-Core/core => core}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt (100%) rename {Archie-Core/core => core}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt (100%) rename {Archie-Core/core => core}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt (100%) rename {Archie-Core/core => core}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt (100%) rename {Archie-Core/core => core}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt (100%) rename {Archie-Core/core => core}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt (100%) rename {Archie-Core/core => core}/fabric/src/main/resources/archie.mixins.json (100%) rename {Archie-Core/core => core}/fabric/src/main/resources/fabric.mod.json (100%) rename {Archie-Core/core => core}/neoforge/build.gradle.kts (100%) rename {Archie-Core/core => core}/neoforge/gradle.properties (100%) rename {Archie-Core/core => core}/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java (100%) rename {Archie-Core/core => core}/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java (100%) rename {Archie-Core/core => core}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt (100%) rename {Archie-Core/core => core}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt (100%) rename {Archie-Core/core => core}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt (100%) rename {Archie-Core/core => core}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.neoforge.kt (100%) rename {Archie-Core/core => core}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.neoforge.kt (100%) rename {Archie-Core/core => core}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.neoforge.kt (100%) rename {Archie-Core/core => core}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt (100%) rename {Archie-Core/core => core}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt (100%) rename {Archie-Core/core => core}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt (100%) rename {Archie-Core/core => core}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt (100%) rename {Archie-Core/core => core}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt (100%) rename {Archie-Core/core => core}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt (100%) rename {Archie-Core/core => core}/neoforge/src/main/resources/META-INF/neoforge.mods.toml (100%) rename {Archie-Core/core => core}/neoforge/src/main/resources/archie.mixins.json (100%) rename {Archie-Core/datagen => datagen}/common/build.gradle.kts (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/IADataProvider.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/ALanguageProvider.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelBuilder.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelProvider.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockStateProvider.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AConfiguredModel.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ACustomLoaderBuilder.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelBuilder.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelProvider.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelFile.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelProvider.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AMultiPartBlockStateBuilder.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AVariantBlockStateBuilder.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/IAGeneratedBlockState.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.common.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieCookingRecipeBuilder.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapedRecipeBuilder.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapelessRecipeBuilder.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilder.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.common.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagsProvider.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/IATagBuilder.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/DatagenArchieExtension.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ADatagenEvents.kt (100%) rename {Archie-Core/datagen => datagen}/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension (100%) rename {Archie-Core/datagen => datagen}/fabric/build.gradle.kts (100%) rename {Archie-Core/datagen => datagen}/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java (100%) rename {Archie-Core/datagen => datagen}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorFabric.kt (100%) rename {Archie-Core/datagen => datagen}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt (100%) rename {Archie-Core/datagen => datagen}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.fabric.kt (100%) rename {Archie-Core/datagen => datagen}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt (100%) rename {Archie-Core/datagen => datagen}/fabric/src/main/resources/archie_datagen.mixins.json (100%) rename {Archie-Core/datagen => datagen}/fabric/src/main/resources/fabric.mod.json (100%) rename {Archie-Core/datagen => datagen}/neoforge/build.gradle.kts (100%) rename {Archie-Core/datagen => datagen}/neoforge/gradle.properties (100%) rename {Archie-Core/datagen => datagen}/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java (100%) rename {Archie-Core/datagen => datagen}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorNeoForge.kt (100%) rename {Archie-Core/datagen => datagen}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt (100%) rename {Archie-Core/datagen => datagen}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.neoforge.kt (100%) rename {Archie-Core/datagen => datagen}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt (100%) rename {Archie-Core/datagen => datagen}/neoforge/src/main/resources/META-INF/neoforge.mods.toml (100%) rename {Archie-Core/datagen => datagen}/neoforge/src/main/resources/archie_datagen.mixins.json (100%) rename {Archie/docs => docs}/assets/icon.svg (100%) rename {Archie/docs => docs}/config.md (100%) rename {Archie/docs => docs}/datagen.md (98%) rename {Archie/docs => docs}/events.md (100%) rename {Archie/docs => docs}/gametest.md (98%) rename {Archie/docs => docs}/gui.md (100%) rename {Archie/docs => docs}/index.md (97%) rename {Archie/docs => docs}/networking.md (100%) rename {Archie/docs => docs}/news/index.md (100%) rename {Archie/docs => docs}/overrides/main.html (100%) rename {Archie/docs => docs}/registries.md (100%) rename {Archie/docs => docs}/resource-packs.md (100%) rename {Archie/docs => docs}/serialization.md (100%) rename {Archie/docs => docs}/transfer.md (100%) rename {Archie-Core/gametest => gametest}/common/build.gradle.kts (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AGametestEvents.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ComposeScreenTestContext.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparer.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparisonAlgorithm.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotManager.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/VerboseTestReporter.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/GametestArchieExtension.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ComposeRenderingTests.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/InputComponentsGameTest.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/LayoutComponentsGameTest.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ModalComponentsGameTest.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt (100%) rename {Archie-Core/gametest => gametest}/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension (100%) rename {Archie-Core/gametest => gametest}/fabric/build.gradle.kts (100%) rename {Archie-Core/gametest => gametest}/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestHelperMixin.java (100%) rename {Archie-Core/gametest => gametest}/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java (100%) rename {Archie-Core/gametest => gametest}/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/lifecycle/MinecraftClientMixin.java (100%) rename {Archie-Core/gametest => gametest}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt (100%) rename {Archie-Core/gametest => gametest}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt (100%) rename {Archie-Core/gametest => gametest}/fabric/src/main/resources/archie_gametest.mixins.json (100%) rename {Archie-Core/gametest => gametest}/fabric/src/main/resources/fabric.mod.json (100%) rename {Archie-Core/gametest => gametest}/neoforge/build.gradle.kts (100%) rename {Archie-Core/gametest => gametest}/neoforge/gradle.properties (100%) rename {Archie-Core/gametest => gametest}/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java (100%) rename {Archie-Core/gametest => gametest}/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/lifecycle/MinecraftClientMixin.java (100%) rename {Archie-Core/gametest => gametest}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt (100%) rename {Archie-Core/gametest => gametest}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt (100%) rename {Archie-Core/gametest => gametest}/neoforge/src/main/resources/META-INF/neoforge.mods.toml (100%) rename {Archie-Core/gametest => gametest}/neoforge/src/main/resources/archie_gametest.mixins.json (100%) rename Archie/mkdocs.yml => mkdocs.yml (98%) create mode 100644 test/common/build.gradle.kts rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt (91%) rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/BlockRegistry.kt (100%) rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/GuiRegistry.kt (100%) rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ItemRegistry.kt (100%) rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestBlock.kt (100%) rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItem.kt (100%) rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemContainerScreen.kt (100%) rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemMenu.kt (100%) rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemScreen.kt (100%) rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestKind.kt (100%) rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestMenu.kt (100%) rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestScreen.kt (100%) rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestTile.kt (100%) rename {Archie-Test => test}/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TileRegistry.kt (100%) rename {Archie-Test/common/src/main/datagen => test/common/src/main/kotlin}/net/kernelpanicsoft/archie/test/data/ArchieTestDatagen.kt (100%) rename {Archie-Test/common/src/main/gametest => test/common/src/main/kotlin}/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt (69%) rename {Archie-Test/common/src/main/gametest => test/common/src/main/kotlin}/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTestFixtures.kt (100%) rename {Archie-Test/common/src/main/gametest => test/common/src/main/kotlin}/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTests.kt (100%) rename {Archie-Test/common/src/main/gametest => test/common/src/main/kotlin}/net/kernelpanicsoft/archie/test/gametest/ComposeItemContainerMenuClientTests.kt (100%) rename {Archie-Test/common/src/main/gametest => test/common/src/main/kotlin}/net/kernelpanicsoft/archie/test/gametest/ComposeItemContainerMenuTests.kt (100%) rename {Archie-Test/common/src/main/gametest => test/common/src/main/kotlin}/net/kernelpanicsoft/archie/test/gametest/DataAttachmentTestFixtures.kt (100%) rename {Archie-Test/common/src/main/gametest => test/common/src/main/kotlin}/net/kernelpanicsoft/archie/test/gametest/DataAttachmentTests.kt (100%) rename {Archie-Test/common/src/main/gametest => test/common/src/main/kotlin}/net/kernelpanicsoft/archie/test/gametest/TestScreenGameTest.kt (100%) rename {Archie-Test => test}/common/src/main/resources/archie_test.accesswidener (100%) rename {Archie-Test => test}/common/src/main/resources/archie_test.common.json (100%) rename {Archie-Test => test}/common/src/main/resources/assets/archie_test/banner.png (100%) rename {Archie-Test => test}/common/src/main/resources/assets/archie_test/icon.png (100%) rename {Archie-Test => test}/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt (100%) rename {Archie-Test => test}/common/src/test/resources/junit-platform.properties (100%) create mode 100644 test/fabric/build.gradle.kts rename {Archie-Test => test}/fabric/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestFabric.kt (100%) rename {Archie-Test => test}/fabric/src/main/resources/fabric.mod.json (78%) create mode 100644 test/neoforge/build.gradle.kts rename {Archie-Test => test}/neoforge/gradle.properties (95%) rename {Archie-Test => test}/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestNeoForge.kt (100%) rename {Archie-Test => test}/neoforge/src/main/resources/META-INF/neoforge.mods.toml (61%) rename {Archie-Test => test}/neoforge/src/main/resources/pack.mcmeta (100%) diff --git a/.claude/skills/sync-docs-after-overhaul/SKILL.md b/.claude/skills/sync-docs-after-overhaul/SKILL.md index 02e91d6e1..67c161933 100644 --- a/.claude/skills/sync-docs-after-overhaul/SKILL.md +++ b/.claude/skills/sync-docs-after-overhaul/SKILL.md @@ -1,6 +1,6 @@ --- name: sync-docs-after-overhaul -description: Bring KDoc comments and the markdown guides under Archie/docs/ back in sync after a significant refactor or architecture overhaul in this repo (renamed classes, restructured hierarchies, new/removed APIs, changed method signatures). Use this whenever the user says they "overhauled", "refactored", "restructured", or "rewrote" a system and asks to update docs, or whenever you notice staged/unstaged changes that rename or gut core classes in an area that has a doc guide (config, events, gui, networking, registries, resource-packs, serialization, transfer). Don't reach for this for a small fix or single-file change — it's for the case where the shape of a system changed enough that existing docs now describe something that no longer exists. +description: Bring KDoc comments and the markdown guides under docs/ back in sync after a significant refactor or architecture overhaul in this repo (renamed classes, restructured hierarchies, new/removed APIs, changed method signatures). Use this whenever the user says they "overhauled", "refactored", "restructured", or "rewrote" a system and asks to update docs, or whenever you notice staged/unstaged changes that rename or gut core classes in an area that has a doc guide (config, events, gui, networking, registries, resource-packs, serialization, transfer). Don't reach for this for a small fix or single-file change — it's for the case where the shape of a system changed enough that existing docs now describe something that no longer exists. --- # Sync docs after an overhaul @@ -35,19 +35,19 @@ critically, the same as an external markdown guide. Check all of these, not just the obvious one: -- **The dedicated markdown guide**, if this feature area has one — `Archie/docs/config.md`, +- **The dedicated markdown guide**, if this feature area has one — `docs/config.md`, `events.md`, `gui.md`, `networking.md`, `registries.md`, `resource-packs.md`, - `serialization.md`, `transfer.md`. Find it with `ls Archie/docs/`; don't guess a name. -- **`Archie/docs/index.md`**, which has a one-line feature-overview table entry per area — usually + `serialization.md`, `transfer.md`. Find it with `ls docs/`; don't guess a name. +- **`docs/index.md`**, which has a one-line feature-overview table entry per area — usually just needs a phrase added/adjusted, rarely a rewrite. - **Class- and member-level KDoc** in the overhauled files themselves, *and* in any other file that references the overhauled types (grep for the old and new type names across - `Archie/common/src/main/kotlin` and the loader modules to catch call sites whose doc comments - now describe stale behavior). + `core/common/src/main/kotlin` and the loader/datagen/gametest/test modules to catch call sites + whose doc comments now describe stale behavior). - **Grep the whole repo for old names** (`grep -rn OldClassName`) to catch stragglers a targeted read would miss — renames especially leave orphaned references in comments that don't affect compilation and so never surface as errors. -- **README.md / AGENTS.md** — usually just point at `Archie/docs/`, so low priority, but check if +- **README.md / AGENTS.md** — usually just point at `docs/`, so low priority, but check if either names a specific API that changed. - **Archie-Test** — the dev-playground module sometimes demonstrates a feature area directly; check whether it does before ruling it out. @@ -82,7 +82,7 @@ rest of the file, mismatched brackets in a code fence, etc. After editing source for markdown-only edits), run the relevant compile task, e.g.: ``` -./gradlew :Archie:common:compileKotlin -q +./gradlew :archie-core-common:compileKotlin -q ``` Treat a clean compile as confirmation the edits are syntactically sound — it says nothing about diff --git a/.github/scripts/generate_release_notes.py b/.github/scripts/generate_release_notes.py index 26cc2fe05..413e1b2d6 100644 --- a/.github/scripts/generate_release_notes.py +++ b/.github/scripts/generate_release_notes.py @@ -13,7 +13,7 @@ - Post-tag (.github/workflows/release-notes.yaml): --new-tag is a real, already-pushed tag; used as both the git ref to end the walk at and the version display string. Writes both the changelog section and a news post. - - Pre-publish (Archie/build.gradle.kts's `generateChangelog` task): the tag doesn't exist yet at + - Pre-publish (build.gradle.kts's `generateChangelog` task): the tag doesn't exist yet at this point, so pass --new-tag as the *intended* version (e.g. `v1.2.0`, not yet a real ref) together with --range-end HEAD (or another real ref) to walk up to. Only pass --changelog-path, not --posts-dir, in this mode - modpublisher's `changelog = file(...)` needs CHANGELOG.md diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 6e2082fc9..7bedfdd49 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -83,10 +83,14 @@ jobs: with: name: test-reports-${{ matrix.loader }} path: | - Archie/**/build/reports/tests/** - Archie/**/build/tmp/junit-gametest-runner/** - Archie-Test/**/build/reports/tests/** - Archie-Test/**/build/tmp/junit-gametest-runner/** + core/**/build/reports/tests/** + core/**/build/tmp/junit-gametest-runner/** + datagen/**/build/reports/tests/** + datagen/**/build/tmp/junit-gametest-runner/** + gametest/**/build/reports/tests/** + gametest/**/build/tmp/junit-gametest-runner/** + test/**/build/reports/tests/** + test/**/build/tmp/junit-gametest-runner/** build/reports/problems/** if-no-files-found: ignore retention-days: 7 diff --git a/.github/workflows/docs.yaml b/.github/workflows/docs.yaml index dd64c5efb..23287d943 100644 --- a/.github/workflows/docs.yaml +++ b/.github/workflows/docs.yaml @@ -31,7 +31,7 @@ jobs: - uses: actions/cache@v6 with: key: mkdocs-material-${{ env.cache_id }} - path: Archie/.cache + path: .cache restore-keys: | mkdocs-material- - run: sudo apt-get update && sudo apt-get install -y pngquant @@ -39,6 +39,5 @@ jobs: - run: git fetch origin gh-pages --depth=1 - name: Build Documentation run: ./gradlew publishDocs - working-directory: Archie env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/release-notes.yaml b/.github/workflows/release-notes.yaml index d8274e022..ab112e090 100644 --- a/.github/workflows/release-notes.yaml +++ b/.github/workflows/release-notes.yaml @@ -1,6 +1,6 @@ name: release-notes -# Generates Archie/CHANGELOG.md and a dated Archie/docs/news/posts/ blog entry for a tagged +# Generates CHANGELOG.md and a dated docs/news/posts/ blog entry for a tagged # release, then opens a PR with both. See .github/scripts/generate_release_notes.py for how # entries are sourced (merged PR titles, parsed for a Conventional Commits prefix) and # .github/workflows/pr-title-lint.yml for how those titles are kept in that shape. @@ -54,14 +54,14 @@ jobs: python .github/scripts/generate_release_notes.py \ --repo "${{ github.repository }}" \ --new-tag "${{ github.ref_name }}" \ - --changelog-path Archie/CHANGELOG.md \ - --posts-dir Archie/docs/news/posts + --changelog-path CHANGELOG.md \ + --posts-dir docs/news/posts - name: Open pull request env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - if git diff --quiet -- Archie/CHANGELOG.md Archie/docs/news/posts; then + if git diff --quiet -- CHANGELOG.md docs/news/posts; then echo "Nothing new to record since the previous tag - skipping PR." exit 0 fi @@ -71,7 +71,7 @@ jobs: BRANCH_NAME="release-notes/${{ github.ref_name }}" git checkout -b "$BRANCH_NAME" - git add Archie/CHANGELOG.md Archie/docs/news/posts + git add CHANGELOG.md docs/news/posts git commit -m "docs: release notes for ${{ github.ref_name }}" git push origin "$BRANCH_NAME" diff --git a/.gitignore b/.gitignore index d161eef74..c9497c7ef 100644 --- a/.gitignore +++ b/.gitignore @@ -24,6 +24,7 @@ local.properties docs/api site .architectury-transformer +.kotlin */.kotlin __pycache__/ *.pyc diff --git a/AGENTS.md b/AGENTS.md index 7da1aa989..19fff5645 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -1,75 +1,93 @@ # AGENTS Guide for Archie ## Repository shape -- The repo root is a Gradle **composite build** (`settings.gradle.kts`) that includes two independent - builds: `Archie/` (the library — this is what's published) and `Archie-Test/` (a playground mod that - substitutes in `Archie`'s project sources, used to exercise the library during development). Run all - Gradle commands from inside `Archie/` or `Archie-Test/`, not the repo root. -- Inside `Archie/`: multi-module Architectury mod: `common` (shared API/logic), `fabric`, `neoforge` - (`Archie/settings.gradle.kts`). +- The repo root is a single Gradle build (`settings.gradle.kts`) on Architectury Loom, with four + products, each nested `/` and flattened to a single-level project name + (e.g. `core/fabric` -> `archie-core-fabric`): `core` (the library, published), `datagen` + (Archie's datagen DSL, own separate mod `archie_datagen`, dev-time only), `gametest` (Archie's + GameTest framework/harness, own separate mod `archie_gametest`, dev/test-time only), `test` (a + playground mod `archie_test` that depends on the other three via plain project references, used + to exercise the library during development). Run all Gradle commands from the repo root. +- Each product's own layout: `common` (shared API/logic), `fabric`, `neoforge` + (`settings.gradle.kts`'s `includeCorePlatform`/`includeModule` helpers). - `common` is the source of truth; loader modules mostly provide bootstrapping, loader deps, and `actual` implementations. -- Main entrypoint flow is `Archie.init()` -> register events/network/config/datagen/gametest gates (`Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt`). +- Main entrypoint flow is `Archie.init()` -> register events/network/config, then activate + datagen/gametest via a `ServiceLoader`-based `ArchieExtension` hook (`net.kernelpanicsoft.archie. + ArchieExtension`, `core/common/.../ArchieExtension.kt`) if `archie-datagen`/`archie-gametest` are + present on the classpath - `core` never has a compile-time dependency on either + (`core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt`). +- `core` never depends on `datagen`/`gametest`, even for things the datagen DSL also touches: + condition/ingredient registration and common tags run at runtime (`Archie.kt` calls + `ABuiltinConditions.init()`/`ABuiltinIngredients.init()`/`ACommonTags.init()` directly), so they + live in `core`; only the datagen-only half of each (e.g. `ADatagenConditionsPlatform`'s + `withCondition`/`fabricRecipeProvider`, which reference the datagen-only `ARecipeProvider` type) + lives in `datagen`. ## Architecture patterns to preserve - Cross-loader abstractions use Kotlin `expect/actual` files named `*.common.kt`, `*.fabric.kt`, `*.neoforge.kt` (example: `APlatform`, `ADataGeneratorPlatform`, `AGameTestPlatform`). - Loader entrypoints must only delegate into common init methods: - - Fabric: `ArchieFabric.onInitialize*` (`Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt`) - - NeoForge: bus listeners in `ArchieNeoForge` (`Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt`) -- Networking is centralized via `NetworkChannel`; packets must be `@Serializable data class` and registered before `register()` (`Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt`). + - Fabric: `ArchieFabric.onInitialize*` (`core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt`) + - NeoForge: bus listeners in `ArchieNeoForge` (`core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt`) +- Networking is centralized via `NetworkChannel`; packets must be `@Serializable data class` and registered before `register()` (`core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt`). - `ArchieNetworkChannel.init()` is the canonical registration order example (register packet producers/consumers, then call `register()`). ## Build and run workflows -All commands below are run from inside `Archie/` (`cd Archie` first). -- Build all modules + merged artifact: `./gradlew build` (`build`/`assemble` finalize with `fusejars` in `Archie/build.gradle.kts`). -- Loader-specific dev runs: `./gradlew fabric:runClient`, `./gradlew neoforge:runClient`. -- Datagen runs are explicit tasks: `./gradlew fabric:runDatagen` / `./gradlew neoforge:runDatagen`. -- GameTest runs: `./gradlew fabric:runGametest` / `./gradlew neoforge:runGametest` (server-side suite), - `./gradlew fabric:runGametestClient` / `./gradlew neoforge:runGametestClient` (client GUI harness suite). -- Docs pipeline: `embedDokkaIntoMkDocs` then `publishDocs` (calls `mike deploy ...`); `Archie/mkdocs.yml` +All commands below are run from the repo root. +- Build everything: `./gradlew build`. +- Loader-specific dev runs: `./gradlew archie-core-fabric:runClient`, `./gradlew archie-core-neoforge:runClient`. +- Datagen runs are explicit tasks: `./gradlew archie-datagen-fabric:runDatagen` / `./gradlew archie-datagen-neoforge:runDatagen`. +- GameTest runs: `./gradlew archie-gametest-fabric:runGametest` / `./gradlew archie-gametest-neoforge:runGametest` (server-side suite), + `./gradlew archie-gametest-fabric:runGametestClient` / `./gradlew archie-gametest-neoforge:runGametestClient` (client GUI harness suite). +- Docs pipeline: `embedDokkaIntoMkDocs` then `publishDocs` (calls `mike deploy ...`); root `mkdocs.yml` contains `# !!! EMBEDDED DOKKA ... DO NOT COMMIT !!!` markers. CI (`.github/workflows/docs.yaml`) runs - `./gradlew publishDocs` with `working-directory: Archie`. + `./gradlew publishDocs` from the repo root. +- **Not currently wired** (a known gap from the single-repo migration, not yet ported): + `modfusioner` (`fusejars`, merged Fabric+NeoForge artifact) and `modpublisher` + (CurseForge/Modrinth/GitHub publishing) - see "Dependency and integration touchpoints" below. ## Project-specific conventions -- Keep resource/manifests tokenized using Gradle properties (`${mod_id}`, `${versions.*}`) in `fabric.mod.json` and `neoforge.mods.toml`. +- Keep resource/manifests tokenized using Gradle properties (`${mod_id}`, `${versions.*}`) in `fabric.mod.json` and `neoforge.mods.toml`. `datagen`/`gametest`/`test` each ship as their own mod, so their own modId is `${mod_id}_datagen`/`${mod_id}_gametest`/`${mod_id}_test`, not the bare `${mod_id}`. - Shared assets are merged from `common` into loader modules via `processResources`; do not duplicate `assets/archie/**` directly in loader modules unless loader-specific. -- `common/build.gradle.kts` intentionally uses `modImplementation(libs.fabric.loader)` only for annotations/mixin deps; avoid importing random Fabric-only classes in common code. +- `core/common/build.gradle.kts` intentionally uses `modImplementation(libs.fabric.loader)` only for annotations/mixin deps; avoid importing random Fabric-only classes in common code. - Utility operators are used pervasively for IDs (`Archie % "main"`, `mod % "path"`, `"namespace" % "path"`) - from `Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt`. + from `core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt`. - PR titles must follow [Conventional Commits](https://www.conventionalcommits.org/) (`feat:`, `fix:`, `docs:`, `refactor:`, `perf:`, `test:`, `build:`, `ci:`, `chore:`, `style:`, `revert:`, optionally scoped `type(scope):`) - enforced by `.github/workflows/pr-title-lint.yml`. Individual commits within a PR don't need to conform, but a direct push to a release branch (no PR) does, since it's read the same way. This isn't just style: cutting a release (`git tag vX.Y.Z`) triggers `.github/workflows/release-notes.yaml`, which walks merged PR titles since the last tag to generate - `Archie/CHANGELOG.md` and a `Archie/docs/news/posts/` entry, grouped by this prefix - an unparsed title + `CHANGELOG.md` and a `docs/news/posts/` entry, grouped by this prefix - an unparsed title doesn't break anything, it just lands in the catch-all "Other Changes" section instead of a real one. ## Dependency and integration touchpoints - Versions and plugin IDs are centralized in `gradle/libs.versions.toml` (repo root); update there first. -- Packaging/publishing is configured at the `Archie/` build root via `modfusioner` (`fusejars`) and - `modpublisher` (CurseForge/Modrinth/GitHub IDs and required deps, tasks `publishCurseforge`/ - `publishModrinth`/`publishGitHub`/`publishMod`) in `Archie/build.gradle.kts` - `modpublisher` reads its - changelog text straight off disk from `Archie/CHANGELOG.md` when a publish task runs, so those four - tasks `dependsOn` a `generateChangelog` task (same file, same script, same `Archie/build.gradle.kts`) - that regenerates it synchronously first. This is deliberately *not* left to the reactive, tag-triggered - `release-notes.yaml` workflow: if `modpublisher` auto-tags as part of the same `./gradlew publish*` - invocation, that workflow can't possibly have generated this release's entry yet by the time - `changelog` is read, and if that invocation runs in CI under the default `GITHUB_TOKEN`, the tag it - creates won't even fire the workflow (GitHub's anti-recursion rule for that token). `release-notes.yaml` - still owns the `Archie/docs/news/posts/` blog entry, which has no such ordering requirement. -- Mixins are split by scope: loader mixins in `Archie/fabric/src/main/resources/archie.mixins.json` and - `Archie/neoforge/src/main/resources/archie.mixins.json`, common mixin config in - `Archie/common/src/main/resources/archie-common.mixins.json`. +- `generateChangelog` (root `build.gradle.kts`) regenerates `CHANGELOG.md` synchronously from + `.github/scripts/generate_release_notes.py` - kept separate from the reactive, tag-triggered + `release-notes.yaml` workflow for the same reasons as before the migration (see the task's own + comment in `build.gradle.kts`). Packaging/publishing itself (`modfusioner`/`modpublisher`, the + tasks that used to `dependsOn` `generateChangelog`) isn't wired into the new build yet - port + from git history if reviving it. +- Mixins are split by scope: loader mixins in `core/fabric/src/main/resources/archie.mixins.json` and + `core/neoforge/src/main/resources/archie.mixins.json`, common mixin config in + `core/common/src/main/resources/archie-common.mixins.json`. `datagen`/`gametest` each have their + own small per-loader mixins.json too (`archie_datagen.mixins.json`/`archie_gametest.mixins.json`), + since they're separate mods. ## Safe edit boundaries -- For new gameplay/library logic: start in `Archie/common/src/main/kotlin/...`, then add loader `actual`/bootstrap only when APIs differ. -- When adding packets/events/config sections, mirror existing object-singleton style (`Archie`, `AEvents`, `ArchieNetworkChannel`) rather than introducing DI/service containers. +- For new gameplay/library logic: start in `core/common/src/main/kotlin/...`, then add loader `actual`/bootstrap only when APIs differ. +- When adding packets/config sections, mirror existing object-singleton style (`Archie`, `ArchieNetworkChannel`) rather than introducing DI/service containers. For datagen/gametest event wiring specifically, mirror `ADatagenEvents`/`AGametestEvents` (in `datagen`/`gametest` respectively - the generic `AEventObject`/`Handler`/`HandlerConstructor` base plumbing lives in `core`). - If adding new runtime libraries to shipped jars, use `bundleRuntimeLibrary(...)` / `bundleMod(...)` in loader `build.gradle.kts` files (not plain `implementation` only). ## GameTest structure and conventions -GameTests are located in `Archie/common/src/main/gametest/` (a separate Gradle source set from -`src/main/kotlin`) and organized by scope (`common`, `client`, `server`). Test infrastructure and -registration live in `ArchieGameTest.kt` (`.../gametest/internal/ArchieGameTest.kt`). +Archie's own self-test GameTest suite lives in `gametest/common/src/main/kotlin/net/kernelpanicsoft/ +archie/gametest/internal/tests/` (a normal source dir - the old `Archie/common/src/main/gametest/` +separate-Gradle-source-set trick is gone now that `gametest` is its own module/mod entirely, +`archie_gametest`), organized by scope (`common`, `client`, `server`). Test infrastructure and +registration live in `ArchieGameTest.kt` (`gametest/common/.../gametest/internal/ArchieGameTest.kt`). +The GameTest *framework itself* (harness, assertions, junit runner) is `gametest/common/.../gametest/` +(one level up from `internal/`) - this is what `archie-test`'s own GameTests, and any consuming mod's, +build on. ### Test file organization - **Test discovery**: Each test class must be registered in `ArchieGameTest.kt`'s `archieGameTests()` @@ -145,13 +163,17 @@ every subsequent test in that run then failed too, since the client's main-threa never got a chance to run again (`client.screen` frozen at whatever it was when the hang started). `kotlinx-coroutines-test` (the dependency this needs) is dev/test-only - `compileOnly` in -`common/build.gradle.kts`, `runtimeLibrary(...)` (present for local runs like -`runGametestClient`, never bundled into the shipped jar) in the loader modules. `ComposeScreen` -itself never references `kotlinx.coroutines.test.*` symbols directly (only `AClientGameTestHarness`'s -method bodies do, and those only run under `AGameTestPlatform.isGameTest`) - it only holds a plain -`CoroutineDispatcher?` and a `(() -> Unit)?` pump callback, both already-bundled core/stdlib types, -specifically so a real player's game (which never has the test dependency on its classpath) never -needs to resolve it. +`core/common/build.gradle.kts` (since `ComposeScreen.kt` itself lives in `core`, which ships to +every real player, and must never resolve the symbol), plain `implementation` in +`gametest/common/build.gradle.kts` (`gametest` is dev/test-only already, no need for `compileOnly` +there) and `runtimeLibrary(...)` (present for local runs like `runGametestClient`, never bundled +into the shipped jar) in the loader modules. `ComposeScreen` itself never references +`kotlinx.coroutines.test.*` symbols directly (only `AClientGameTestHarness`'s method bodies do, and +those only run under `AGameTestPlatform.isGameTest`, and live in `gametest` not `core`) - `core`'s +`ComposeScreen` only holds a plain `CoroutineDispatcher?` and a `(() -> Unit)?` pump callback (both +already-bundled core/stdlib types) on its public `ComposeTestClockOverride` object, specifically so +a real player's game never needs to resolve the coroutines-test symbol, while `gametest` (a +different module) can still reach in and set it. ### Current test coverage - `ArchieItemHandlerTests` (`server`) – item storage/handler behavior @@ -160,15 +182,15 @@ needs to resolve it. - `ComposeRenderingTests` (`client`) – GUI framework layout/rendering via the client harness ### Adding new tests -1. Create a new test class in `Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/`. +1. Create a new test class in `gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/`. 2. Name it `XyzTests.kt` (following existing convention). 3. For server/common tests: methods as `fun GameTestHelper.testFeatureName()` with `@GameTest(template = EMPTY)`. For client tests: methods as `fun ClientGameTestContext.testFeatureName()` with `@ClientGameTest`. 4. Use assertion helpers from `GameTestAssertions.kt` (server/common) or `ClientGameTestContext`'s own assertion methods (client). 5. Register the class in `ArchieGameTest.kt`'s `archieGameTests()` under the appropriate scope block. -6. Run tests locally with `./gradlew fabric:runGametest` / `neoforge:runGametest` (server/common), or - `./gradlew fabric:runGametestClient` / `neoforge:runGametestClient` (client). +6. Run tests locally with `./gradlew archie-gametest-fabric:runGametest` / `archie-gametest-neoforge:runGametest` (server/common), or + `./gradlew archie-gametest-fabric:runGametestClient` / `archie-gametest-neoforge:runGametestClient` (client). IMPORTANT: When applicable, prefer using intellij-index MCP tools for code navigation and refactoring. IMPORTANT: When debugging, prefer using intellij-debugger MCP tools to interact with the IDE debugger. diff --git a/Archie-Core/build.gradle.kts b/Archie-Core/build.gradle.kts deleted file mode 100644 index c8ac21c3c..000000000 --- a/Archie-Core/build.gradle.kts +++ /dev/null @@ -1,131 +0,0 @@ -import net.fabricmc.loom.api.LoomGradleExtensionAPI -import org.jetbrains.kotlin.konan.properties.loadProperties - -plugins { - java - alias(libs.plugins.architectury) - id("net.kernelpanicsoft.actualizer") version "0.1.0" apply false - alias(libs.plugins.architectury.loom) apply false - alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.kotlin.serialization) - alias(libs.plugins.kotlin.compose) - alias(libs.plugins.compose) -} - -architectury.minecraft = libs.versions.minecraft.get() - -val sharedProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("gradle.properties") - val sharedPropsFile = rootDir.resolve("../gradle.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val String.prop: String? - get() = sharedProperties?.get(this)?.toString() - -val String.localOrEnv: String? - get() = System.getenv(this.uppercase()) - -subprojects { - apply(plugin = "dev.architectury.loom") - apply(plugin = "net.kernelpanicsoft.actualizer") - - val loom = project.extensions.getByName("loom") - - configure { - silentMojangMappingsLicense() - } - - repositories { - val githubUsername = "github_actor".localOrEnv - val githubToken = "github_token".localOrEnv - mavenCentral() - mavenLocal() - google { - content { - includeGroupByRegex("androidx\\..*") - includeGroupByRegex("com\\.android.*") - } - } - maven { - name = "kernelpanic releases" - url = uri("https://maven.kernelpanicsoft.net/releases") - } - maven { - name = "kernelpanic snapshots" - url = uri("https://maven.kernelpanicsoft.net/snapshots") - } - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") - maven("https://maven.parchmentmc.org") - maven("https://maven.fabricmc.net/") - maven("https://maven.neoforged.net/releases/") - maven("https://maven.terraformersmc.com/releases/") - maven("https://repo.nyon.dev/releases") - maven("https://maven.isxander.dev/releases") { - name = "Xander Maven" - } - maven("https://maven.resourcefulbees.com/repository/maven-public/") { - content { - includeGroup("earth.terrarium.common_storage_lib") - } - } - maven { - url = uri("https://maven.pkg.github.com/MrCrayfish/Maven") - credentials { - username = githubUsername - password = githubToken - } - } - maven { - url = uri("https://www.cursemaven.com") - content { - includeGroup("curse.maven") - } - } - } - - @Suppress("UnstableApiUsage") - dependencies { - "minecraft"(rootProject.libs.minecraft) - "mappings"(loom.layered { - officialMojangMappings() - parchment(rootProject.libs.parchment) - }) - - compileOnly("org.jetbrains:annotations:24.1.0") - } -} - -allprojects { - apply(plugin = "java") - apply(plugin = "org.jetbrains.kotlin.jvm") - apply(plugin = "org.jetbrains.kotlin.plugin.serialization") - apply(plugin = "org.jetbrains.kotlin.plugin.compose") - apply(plugin = "org.jetbrains.compose") - apply(plugin = "architectury-plugin") - - version = "mod_version".prop ?: "0.0.1-SNAPSHOT" - group = "mod_group".prop ?: "net.kernelpanicsoft" - base.archivesName = "archie-core" - - tasks.withType().configureEach { - options.encoding = "UTF-8" - options.release.set(21) - } - - kotlin { - compilerOptions { - freeCompilerArgs.add("-Xexpect-actual-classes") - } - } - - architectury { - compileOnly() - } - - java.withSourcesJar() -} diff --git a/Archie-Core/gradle.properties b/Archie-Core/gradle.properties deleted file mode 100644 index c842d3012..000000000 --- a/Archie-Core/gradle.properties +++ /dev/null @@ -1,12 +0,0 @@ -org.gradle.jvmargs=-Xmx8G -kotlin.incremental=false -mod_id=archie -mod_group=net.kernelpanicsoft -mod_version=1.0.0 -mod_display_name=Archie -mod_description=A library mod for Kernel Panic's mods -mod_authors=Kernel Panic -mod_credits=Both the NeoForge and fabric teams for the code I ported to Architectury and Kotlin -mod_url=https://github.com/kernel-panic-codecave/Archie -mod_source=https://github.com/kernel-panic-codecave/Archie -mod_license=GPL-3.0-or-later diff --git a/Archie-Core/gradle/wrapper/gradle-wrapper.jar b/Archie-Core/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index a4b76b9530d66f5e68d973ea569d8e19de379189..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43583 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 GZ~q4|mN%#X diff --git a/Archie-Core/gradle/wrapper/gradle-wrapper.properties b/Archie-Core/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index cea7a793a..000000000 --- a/Archie-Core/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/Archie-Core/gradlew b/Archie-Core/gradlew deleted file mode 100755 index f3b75f3b0..000000000 --- a/Archie-Core/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/Archie-Core/gradlew.bat b/Archie-Core/gradlew.bat deleted file mode 100644 index 9d21a2183..000000000 --- a/Archie-Core/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/Archie-Core/settings.gradle.kts b/Archie-Core/settings.gradle.kts deleted file mode 100644 index 844757759..000000000 --- a/Archie-Core/settings.gradle.kts +++ /dev/null @@ -1,58 +0,0 @@ -enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") - -rootProject.name = "Archie-Core" - -pluginManagement { - repositories { - maven("https://maven.fabricmc.net/") - maven("https://maven.architectury.dev/") - maven("https://maven.minecraftforge.net/") - maven("https://maven.neoforged.net/releases/") - maven("https://maven.firstdarkdev.xyz/releases") - maven { - name = "kernelpanic releases" - url = uri("https://maven.kernelpanicsoft.net/releases") - } - maven { - name = "kernelpanic snapshots" - url = uri("https://maven.kernelpanicsoft.net/snapshots") - } - mavenLocal() - gradlePluginPortal() - } -} - -dependencyResolutionManagement { - versionCatalogs { - create("libs") { - from(files("../gradle/libs.versions.toml")) - } - } -} - -// Matches terrarium-earth/Common-Storage-Lib's settings.gradle.kts layout: one nested -// / directory per platform, flattened into a single-level Gradle project name -// (e.g. core/fabric -> archie-core-fabric). archie-core is today's module; archie-datagen and -// archie-gametest (and eventually the test mod) join later via more includeModule(...) calls, -// without needing any further settings.gradle.kts restructuring. -includeCorePlatform("common") -includeCorePlatform("fabric") -includeCorePlatform("neoforge") - -includeModule("datagen", "common") -includeModule("datagen", "fabric") -includeModule("datagen", "neoforge") - -includeModule("gametest", "common") -includeModule("gametest", "fabric") -includeModule("gametest", "neoforge") - -fun includeModule(name: String, platform: String) { - include("$name/$platform") - project(":$name/$platform").name = "archie-$name-$platform" -} - -fun includeCorePlatform(platform: String) { - include("core/$platform") - project(":core/$platform").name = "archie-core-$platform" -} diff --git a/Archie-Test/.architectury-transformer/debug.log b/Archie-Test/.architectury-transformer/debug.log deleted file mode 100644 index 0748d5fae..000000000 --- a/Archie-Test/.architectury-transformer/debug.log +++ /dev/null @@ -1 +0,0 @@ -[Architectury Transformer DEBUG] Closed File Systems for /home/kernelpanic/IdeaProjects/Archie/Archie/common/build/libs/archie-common-1.0.0.jar diff --git a/Archie-Test/build.gradle.kts b/Archie-Test/build.gradle.kts deleted file mode 100644 index d755833ba..000000000 --- a/Archie-Test/build.gradle.kts +++ /dev/null @@ -1,154 +0,0 @@ -import net.fabricmc.loom.api.LoomGradleExtensionAPI -import org.jetbrains.kotlin.konan.properties.loadProperties -import java.util.Properties -import kotlin.collections.forEach - -plugins { - java - alias(libs.plugins.architectury) - id("net.kernelpanicsoft.actualizer") version "0.1.0" apply false -// alias(libs.plugins.architectury.kotlin) - alias(libs.plugins.architectury.loom) apply false - alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.kotlin.serialization) - alias(libs.plugins.kotlin.compose) - alias(libs.plugins.compose) -} - -architectury.minecraft = libs.versions.minecraft.get() - -val localProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("local.properties") - val sharedPropsFile = rootDir.resolve("../local.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val sharedProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("gradle.properties") - val sharedPropsFile = rootDir.resolve("../gradle.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val String.prop: String? - get() = sharedProperties?.get(this)?.toString() - -val String.local: String? - get() = localProperties?.get(this)?.toString() - -val String.env: String? - get() = System.getenv(this) - -val String.localOrEnv: String? - get() = localProperties?.get(this)?.toString() ?: System.getenv(this.uppercase()) - -subprojects { - apply(plugin = "dev.architectury.loom") - apply(plugin = "net.kernelpanicsoft.actualizer") - - val loom = project.extensions.getByName("loom") - - configure { - silentMojangMappingsLicense() - } - - repositories { - val githubUsername = "github_actor".localOrEnv - val githubToken = "github_token".localOrEnv - mavenCentral() - mavenLocal() - google { - content { - includeGroupByRegex("androidx\\..*") - includeGroupByRegex("com\\.android.*") - } - } - maven { - name = "kernelpanic releases" - url = uri("https://maven.kernelpanicsoft.net/releases") - } - maven { - name = "kernelpanic snapshots" - url = uri("https://maven.kernelpanicsoft.net/snapshots") - } - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") - maven("https://maven.parchmentmc.org") - maven("https://maven.fabricmc.net/") - maven("https://maven.neoforged.net/releases/") - maven("https://maven.terraformersmc.com/releases/") - maven("https://repo.nyon.dev/releases") - maven("https://maven.isxander.dev/releases") { - name = "Xander Maven" - } - maven("https://maven.resourcefulbees.com/repository/maven-public/") { - content { - includeGroup("earth.terrarium.common_storage_lib") - } - } - maven { - url = uri("https://maven.pkg.github.com/MrCrayfish/Maven") - credentials { - username = githubUsername - password = githubToken - } - } - maven { - url = uri("https://www.cursemaven.com") - content { - includeGroup("curse.maven") - } - } - } - - @Suppress("UnstableApiUsage") - dependencies { - "minecraft"(rootProject.libs.minecraft) - "mappings"(loom.layered { - officialMojangMappings() - parchment(rootProject.libs.parchment) - }) - - compileOnly("org.jetbrains:annotations:24.1.0") - } -} - -allprojects { - apply(plugin = "java") - apply(plugin = "org.jetbrains.kotlin.jvm") - apply(plugin = "org.jetbrains.kotlin.plugin.serialization") - apply(plugin = "org.jetbrains.kotlin.plugin.compose") - apply(plugin = "org.jetbrains.compose") - apply(plugin = "architectury-plugin") - apply(plugin = "maven-publish") - - version = "mod_version".prop!! - group = "mod_group".prop!! - base.archivesName = "mod_id".prop!!.replace("_", "-") - - tasks.withType().configureEach { - options.encoding = "UTF-8" - options.release.set(21) - } - - architectury { - compileOnly() - } - - java.withSourcesJar() -} - -tasks { - check { - dependsOn(project(":common-test").tasks.check) - dependsOn(project(":fabric-test").tasks.check) - dependsOn(project(":neoforge-test").tasks.check) - } -} - diff --git a/Archie-Test/common/build.gradle.kts b/Archie-Test/common/build.gradle.kts deleted file mode 100644 index 05d88e214..000000000 --- a/Archie-Test/common/build.gradle.kts +++ /dev/null @@ -1,98 +0,0 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.jetbrains.kotlin.konan.properties.loadProperties - -architectury { - common("fabric", "neoforge") -} - -actualizer { - stubUnfulfilledExpects() -} - -val localProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("local.properties") - val sharedPropsFile = rootDir.resolve("../local.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val sharedProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("gradle.properties") - val sharedPropsFile = rootDir.resolve("../gradle.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val String.prop: String? - get() = sharedProperties?.get(this)?.toString() - -val String.local: String? - get() = localProperties?.get(this)?.toString() - -val String.env: String? - get() = System.getenv(this) - -val String.localOrEnv: String? - get() = localProperties?.get(this)?.toString() ?: System.getenv(this.uppercase()) - - - -loom { - log4jConfigs.from(rootDir.resolve("../log4j-dev.xml")) - accessWidenerPath = file("src/main/resources/${"mod_id".prop}.accesswidener") - enableTransitiveAccessWideners = true -} - -sourceSets { - main { - kotlin { - srcDir("src/main/gametest") - srcDir("src/main/datagen") - } - java { - srcDir("src/main/mixin") - } - } -} - -dependencies { - // namedElements is the mapped-name variant - without it this resolves to the intermediary-mapped - // variant that remapJar publishes (see fabric/neoforge's "common"(...) dependency for the same fix), - // which is fine at compile time but breaks reflection over Minecraft types at test runtime. - api("net.kernelpanicsoft:common") { targetConfiguration = "namedElements" } - testImplementation(libs.junit.jupiter.api) - testImplementation(kotlin("reflect")) - testRuntimeOnly(libs.junit.jupiter.engine) - // We depend on fabric loader here to use the fabric @Environment annotations and get the mixin dependencies - // Do NOT use other classes from fabric loader - modImplementation(libs.fabric.loader) - - modApi(libs.architectury.common) - modApi(libs.rei.common) - modApi(libs.storage.common) - modApi(libs.storage.resources.common) -} - -tasks { - base.archivesName.set(base.archivesName.get() + "-common") - - test { - testClassesDirs = sourceSets.test.get().output.classesDirs - classpath = sourceSets.test.get().runtimeClasspath - useJUnitPlatform() - systemProperty("archie.junit.gametest", "true") - // See the matching comment in Archie/common/build.gradle.kts. - systemProperty( - "archie.junit.gametest.matrix", - System.getProperty("archie.junit.gametest.matrix") ?: "fabric:server,fabric:client,neoforge:server,neoforge:client", - ) - systemProperty("archie.junit.gametest.timeoutMinutes", "20") - systemProperty("archie.junit.gametest.root", rootProject.rootDir.absolutePath) - } -} diff --git a/Archie-Test/fabric/build.gradle.kts b/Archie-Test/fabric/build.gradle.kts deleted file mode 100644 index 282236da9..000000000 --- a/Archie-Test/fabric/build.gradle.kts +++ /dev/null @@ -1,228 +0,0 @@ -import net.kernelpanicsoft.archie.plugin.bundleMod -import net.kernelpanicsoft.archie.plugin.bundleRuntimeLibrary -import org.jetbrains.kotlin.konan.properties.loadProperties - - -plugins { - alias(libs.plugins.shadow) - alias(libs.plugins.archie) -} - -architectury { - platformSetupLoomIde() - fabric() -} - -actualizer { - actualizes(project(":common-test")) - actualizes("net.kernelpanicsoft:common") -} - -val localProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("local.properties") - val sharedPropsFile = rootDir.resolve("../local.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val sharedProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("gradle.properties") - val sharedPropsFile = rootDir.resolve("../gradle.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val String.prop: String? - get() = sharedProperties?.get(this)?.toString() - -val String.local: String? - get() = localProperties?.get(this)?.toString() - -val String.env: String? - get() = System.getenv(this) - -val String.localOrEnv: String? - get() = localProperties?.get(this)?.toString() ?: System.getenv(this.uppercase()) - - -configurations { - create("common") - create("archie") - create("shadowCommon") - compileClasspath.get().extendsFrom(configurations["common"], configurations["archie"]) - runtimeClasspath.get().extendsFrom(configurations["common"], configurations["archie"]) - testCompileClasspath.get().extendsFrom(compileClasspath.get()) - testRuntimeClasspath.get().extendsFrom(runtimeClasspath.get()) -// getByName("developmentFabric").extendsFrom(configurations["common"]) -} - -loom { - log4jConfigs.from(project(":common-test").loom.log4jConfigs) - accessWidenerPath.set(project(":common-test").loom.accessWidenerPath) - - mods { - maybeCreate("main").apply { - sourceSet(sourceSets.main.get()) - } - } - - runs { - getByName("client") { - name = "Minecraft Client" - source(sourceSets.main.get()) - vmArg("-XX:+AllowEnhancedClassRedefinition") - } - getByName("server") { - name = "Minecraft Server" - source(sourceSets.main.get()) - vmArgs("-XX:+AllowEnhancedClassRedefinition") - } - // This adds a new gradle task that runs the datagen API: "gradlew runDatagen" - create("datagen") { - client() - name = "Minecraft Datagen" - property("archie.datagen", "true") - property("archie.datagen.client", providers.gradleProperty("client_datagen").orElse("true").get()) - property("archie.datagen.server", providers.gradleProperty("server_datagen").orElse("true").get()) - property("fabric-api.datagen") - property("fabric-api.datagen.modid", providers.gradleProperty("mod_id").orElse("archie").get()) - property("fabric-api.datagen.output-dir", file("src/main/generated").absolutePath) - - runDir = "build/datagen" - } - create("gametest") { - server() - name = "Minecraft GameTest" - property("fabric-api.gametest") - property("archie.gametest", "true") - property("archie.gametest.side", "server") - property("archie.gametest.modid", providers.gradleProperty("mod_id").orElse("archie_test").get()) - } - create("gametestClient") { - client() - name = "Minecraft GameTest Client" - property("fabric-api.gametest") - property("archie.gametest", "true") - property("archie.gametest.side", "client") - property("archie.gametest.modid", providers.gradleProperty("mod_id").orElse("archie_test").get()) - } - } -} - -fabricApi.configureDataGeneration { - createRunConfiguration = false - outputDirectory.set(file("src/main/generated")) -} - -sourceSets { - main { - resources { - } - kotlin { - srcDir("src/main/gametest") - } - java { - srcDir("src/main/mixin") - } - } -} - -dependencies { - "archie"("net.kernelpanicsoft:fabric") { targetConfiguration = "namedElements" } - modImplementation(libs.fabric.loader) - modApi(libs.fabric.api) - modApi(libs.architectury.fabric) - modImplementation(libs.kotlin.fabric) - modLocalRuntime(libs.rei.fabric) - modLocalRuntime(libs.catalogue.fabric) - modLocalRuntime(libs.menulogue.fabric) - modLocalRuntime(libs.clothConfig.fabric) - bundleMod(libs.storage.fabric) - - "common"(project(":common-test", "namedElements")) { isTransitive = false } - "common"("net.kernelpanicsoft:common") { targetConfiguration = "namedElements" } - "shadowCommon"(project(":common-test", "transformProductionFabric")) { isTransitive = false } -} - -modResources { - filesMatching.add("fabric.mod.json") -} - - -tasks { - base.archivesName.set(base.archivesName.get() + "-fabric") - - test { - useJUnitPlatform() - } - - processResources { - from(project(":common-test").sourceSets.main.get().resources) { - include("assets/${"mod_id".prop}/**") - include("data/${"mod_id".prop}/**") - include("${"mod_id".prop}-common.mixins.json") - include("${"mod_id".prop}.common.json") - include("${"mod_id".prop}.accesswidener") - } - dependsOn(processTestResources) - } - - processTestResources { - } - - classes { - finalizedBy(testClasses) - } - - shadowJar { - configurations = - listOf(project.configurations.getByName("shadowCommon"), project.configurations.getByName("shadow")) - archiveClassifier.set("dev-shadow") - } - - remapJar { - injectAccessWidener.set(true) - inputFile.set(shadowJar.get().archiveFile) - dependsOn(shadowJar) - } - - jar.get().archiveClassifier.set("dev") - - sourcesJar { - val commonSources = project(":common-test").tasks.sourcesJar - dependsOn(commonSources) - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from(commonSources.get().archiveFile.map { zipTree(it) }) - } -} - -//publishing { -// publications.create("mavenFabric") { -// artifactId = base.archivesName.get() -// from(components["java"]) -// } -// -// repositories { -// mavenLocal() -// maven { -// val releasesRepoUrl = "https://example.com/releases" -// val snapshotsRepoUrl = "https://example.com/snapshots" -// url = uri( -// if (project.version.toString().endsWith("SNAPSHOT") || project.version.toString() -// .startsWith("0") -// ) snapshotsRepoUrl else releasesRepoUrl -// ) -// name = "ExampleRepo" -// credentials { -// username = project.properties["repoLogin"]?.toString() -// password = project.properties["repoPassword"]?.toString() -// } -// } -// } -//} \ No newline at end of file diff --git a/Archie-Test/gradle.properties b/Archie-Test/gradle.properties deleted file mode 100644 index ac947a374..000000000 --- a/Archie-Test/gradle.properties +++ /dev/null @@ -1,19 +0,0 @@ -org.gradle.jvmargs=-Xmx8G -kotlin.incremental=false -org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled -org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true - -mod_id=archie_test -mod_group=net.kernelpanicsoft -mod_version=1.0.0 -mod_display_name=Archie Test Mod -mod_description=A library mod for Kernel Panic's mods -mod_authors=Kernel Panic -mod_credits=Both the NeoForge and fabric teams for the code I ported to Architectury and Kotlin -mod_url=https://github.com/kernel-panic-codecave/Archie -mod_source=https://github.com/kernel-panic-codecave/Archie -mod_license=GPL-3.0-or-later - -client_datagen=true -server_datagen=true - diff --git a/Archie-Test/gradle/wrapper/gradle-wrapper.jar b/Archie-Test/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index a4b76b9530d66f5e68d973ea569d8e19de379189..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43583 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 GZ~q4|mN%#X diff --git a/Archie-Test/gradle/wrapper/gradle-wrapper.properties b/Archie-Test/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index cea7a793a..000000000 --- a/Archie-Test/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/Archie-Test/gradlew b/Archie-Test/gradlew deleted file mode 100755 index f3b75f3b0..000000000 --- a/Archie-Test/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/Archie-Test/gradlew.bat b/Archie-Test/gradlew.bat deleted file mode 100644 index 9b42019c7..000000000 --- a/Archie-Test/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/Archie-Test/neoforge/build.gradle.kts b/Archie-Test/neoforge/build.gradle.kts deleted file mode 100644 index 943c1f4f5..000000000 --- a/Archie-Test/neoforge/build.gradle.kts +++ /dev/null @@ -1,250 +0,0 @@ -import net.fabricmc.loom.api.LoomGradleExtensionAPI -import net.fabricmc.loom.util.ModPlatform -import net.kernelpanicsoft.archie.plugin.bundleMod -import net.kernelpanicsoft.archie.plugin.bundleRuntimeLibrary -import net.kernelpanicsoft.archie.plugin.runtimeLibrary -import org.jetbrains.compose.compose -import org.jetbrains.kotlin.konan.properties.loadProperties - - -plugins { - alias(libs.plugins.shadow) - alias(libs.plugins.archie) -} - -architectury { - platformSetupLoomIde() - neoForge() -} - -actualizer { - actualizes(project(":common-test")) - actualizes("net.kernelpanicsoft:common") -} - -val localProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("local.properties") - val sharedPropsFile = rootDir.resolve("../local.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val sharedProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("gradle.properties") - val sharedPropsFile = rootDir.resolve("../gradle.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val String.prop: String? - get() = sharedProperties?.get(this)?.toString() - -val String.local: String? - get() = localProperties?.get(this)?.toString() - -val String.env: String? - get() = System.getenv(this) - -val String.localOrEnv: String? - get() = localProperties?.get(this)?.toString() ?: System.getenv(this.uppercase()) - - -configurations { - create("common") - create("archie") - create("shadowCommon") - configureEach { - // Keep NeoForge Kotlin runtime provided by KotlinLangForge only. - exclude(group = "thedarkcolour", module = "kotlinforforge-neoforge") - exclude(group = "remapped.thedarkcolour", module = "kotlinforforge-neoforge-1d1bcbf2") - } - compileClasspath.get().extendsFrom(configurations["common"], configurations["archie"]) - runtimeClasspath.get().extendsFrom(configurations["common"], configurations["archie"]) - testCompileClasspath.get().extendsFrom(compileClasspath.get()) - testRuntimeClasspath.get().extendsFrom(runtimeClasspath.get()) -// getByName("developmentNeoForge").extendsFrom(configurations["common"]) -} - -loom { - log4jConfigs.from(project(":common-test").loom.log4jConfigs) - accessWidenerPath.set(project(":common-test").loom.accessWidenerPath) - - mods { - maybeCreate("main").apply { - sourceSet(sourceSets.main.get()) - } - } - - runs { - getByName("client") { - name = "Minecraft Client" - source(sourceSets.main.get()) - vmArgs("-XX:+AllowEnhancedClassRedefinition") - property("kotlinx.coroutines.debug", "off") - } - getByName("server") { - name = "Minecraft Server" - source(sourceSets.main.get()) - property("kotlinx.coroutines.debug", "off") - vmArgs("-XX:+AllowEnhancedClassRedefinition") - } - create("datagen") { - data() - name = "Minecraft Datagen" - property("archie.datagen", "true") - property("archie.datagen.client", providers.gradleProperty("client_datagen").orElse("true").get()) - property("archie.datagen.server", providers.gradleProperty("server_datagen").orElse("true").get()) - property("kotlinx.coroutines.debug", "off") - programArgs("--all", "--mod", providers.gradleProperty("mod_id").orElse("archie").get()) - programArgs("--output", file("src/main/generated").absolutePath) - } - - create("gametest") { - server() - name = "Minecraft GameTest" - property("neoforge.enableGameTest", "true") - property("neoforge.gameTestServer", "true") - property("archie.gametest", "true") - property("archie.gametest.modid", providers.gradleProperty("mod_id").orElse("archie_test").get()) - property("kotlinx.coroutines.debug", "off") - providers.gradleProperty("archie.junit.gametest.function").orNull?.let { property("archie.junit.gametest.function", it) } - } - - create("gametestClient") { - client() - name = "Minecraft GameTest Client" - property("neoforge.enableGameTest", "true") - property("archie.gametest.side", "client") - property("archie.gametest", "true") - property("archie.gametest.modid", providers.gradleProperty("mod_id").orElse("archie_test").get()) - property("kotlinx.coroutines.debug", "off") - providers.gradleProperty("archie.junit.gametest.function").orNull?.let { property("archie.junit.gametest.function", it) } - } - } - -} - -sourceSets { - main { - resources { - srcDir("src/main/generated") - } - kotlin { - srcDir("src/main/gametest") - } - java { - srcDir("src/main/mixin") - } - } -} - -dependencies { - "archie"("net.kernelpanicsoft:neoforge") { targetConfiguration = "namedElements" } - neoForge(libs.neoforge) - modApi(libs.architectury.neoforge) - implementation(libs.kotlin.neoforge) - modRuntimeOnly(libs.rei.neoforge) - modRuntimeOnly(libs.catalogue.neoforge) - modRuntimeOnly(libs.clothConfig.neoforge) - bundleMod(libs.storage.neoforge) { exclude(group = "curse.maven") } - - "common"(project(":common-test", "namedElements")) { isTransitive = false } - "common"("net.kernelpanicsoft:common") { targetConfiguration = "namedElements" } - "shadowCommon"(project(":common-test", "transformProductionNeoForge")) { isTransitive = false } -} - -modResources { - filesMatching.add("META-INF/neoforge.mods.toml") -} - -tasks { - base.archivesName.set(base.archivesName.get() + "-neoforge") - - test { - useJUnitPlatform() - } - - processResources { - from(project(":common-test").sourceSets.main.get().resources) { - include("assets/${"mod_id".prop}/**") - include("data/${"mod_id".prop}/**") - include("${"mod_id".prop}-common.mixins.json") - include("${"mod_id".prop}.common.json") - include("${"mod_id".prop}.accesswidener") - } - dependsOn(processTestResources) - } - - processTestResources { - } - - classes { - finalizedBy(testClasses) - } - - shadowJar { - exclude("fabric.mod.json") - configurations = - listOf(project.configurations.getByName("shadowCommon"), project.configurations.getByName("shadow")) - archiveClassifier.set("dev-shadow") - } - - remapJar { - inputFile.set(shadowJar.get().archiveFile) - atAccessWideners.set(setOf(loom.accessWidenerPath.get().asFile.name)) - dependsOn(shadowJar) - } - - jar.get().archiveClassifier.set("dev") - - jar { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from(project(":common-test").sourceSets.main.get().output) - } - - sourcesJar { - val commonSources = project(":common-test").tasks.sourcesJar - dependsOn(commonSources) - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from(commonSources.get().archiveFile.map { zipTree(it) }) - } - -// task("printRuntimeClasspath") { -// val runtimeClasspath = sourceSets.test.get().runtimeClasspath -// inputs.files( runtimeClasspath ) -// doLast { -// println(runtimeClasspath.joinToString("\n") { it.path }) -// } -// } -} - -//publishing { -// publications.create("mavenNeoForge") { -// artifactId = base.archivesName.get() -// from(components["java"]) -// } -// -// repositories { -// mavenLocal() -// maven { -// val releasesRepoUrl = "https://example.com/releases" -// val snapshotsRepoUrl = "https://example.com/snapshots" -// url = uri( -// if (project.version.toString().endsWith("SNAPSHOT") || project.version.toString() -// .startsWith("0") -// ) snapshotsRepoUrl else releasesRepoUrl -// ) -// name = "ExampleRepo" -// credentials { -// username = project.properties["repoLogin"]?.toString() -// password = project.properties["repoPassword"]?.toString() -// } -// } -// } -//} \ No newline at end of file diff --git a/Archie-Test/settings.gradle.kts b/Archie-Test/settings.gradle.kts deleted file mode 100644 index 24329e8de..000000000 --- a/Archie-Test/settings.gradle.kts +++ /dev/null @@ -1,48 +0,0 @@ -import org.gradle.kotlin.dsl.maven - -enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") - -pluginManagement { - repositories { - maven("https://maven.fabricmc.net/") - maven("https://maven.architectury.dev/") - maven("https://maven.minecraftforge.net/") - maven("https://maven.neoforged.net/releases/") - maven("https://maven.firstdarkdev.xyz/releases") - maven { - name = "kernelpanic releases" - url = uri("https://maven.kernelpanicsoft.net/releases") - } - maven { - name = "kernelpanic snapshots" - url = uri("https://maven.kernelpanicsoft.net/snapshots") - } - gradlePluginPortal() - mavenLocal() - } -} - -dependencyResolutionManagement { - versionCatalogs { - create("libs") { - from(files("../gradle/libs.versions.toml")) - } - } -} - -includeBuild("../Archie") { - dependencySubstitution { - substitute(module("net.kernelpanicsoft:common")).using(project(":common")) - substitute(module("net.kernelpanicsoft:fabric")).using(project(":fabric")) - substitute(module("net.kernelpanicsoft:neoforge")).using(project(":neoforge")) - } -} - -include("common", "fabric", "neoforge") - -rootProject.name = "Archie-Test" - -project(":common").name = "common-test" -project(":fabric").name = "fabric-test" -project(":neoforge").name = "neoforge-test" - diff --git a/Archie/build.gradle.kts b/Archie/build.gradle.kts deleted file mode 100644 index 0fa47ab4d..000000000 --- a/Archie/build.gradle.kts +++ /dev/null @@ -1,293 +0,0 @@ -import net.fabricmc.loom.api.LoomGradleExtensionAPI -import org.gradle.api.publish.PublishingExtension -import org.jetbrains.kotlin.konan.properties.loadProperties -import org.gradle.api.publish.maven.MavenPublication -import java.util.Properties - -plugins { - java - alias(libs.plugins.architectury) - id("net.kernelpanicsoft.actualizer") version "0.1.0" apply false -// alias(libs.plugins.architectury.kotlin) - alias(libs.plugins.architectury.loom) apply false - alias(libs.plugins.kotlin.jvm) - alias(libs.plugins.kotlin.serialization) - alias(libs.plugins.kotlin.compose) - alias(libs.plugins.compose) - alias(libs.plugins.dokka.mkdocs) - alias(libs.plugins.modfusioner) - alias(libs.plugins.modpublisher) -} - -architectury.minecraft = libs.versions.minecraft.get() - -val localProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("local.properties") - val sharedPropsFile = rootDir.resolve("../local.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val sharedProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("gradle.properties") - val sharedPropsFile = rootDir.resolve("../gradle.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val String.prop: String? - get() = sharedProperties?.get(this)?.toString() - -val String.local: String? - get() = localProperties?.get(this)?.toString() - -val String.env: String? - get() = System.getenv(this) - -val String.localOrEnv: String? - get() = localProperties?.get(this)?.toString() ?: System.getenv(this.uppercase()) - -subprojects { - apply(plugin = "dev.architectury.loom") - apply(plugin = "maven-publish") - apply(plugin = "net.kernelpanicsoft.actualizer") - - val loom = project.extensions.getByName("loom") - - configure { - silentMojangMappingsLicense() - } - - repositories { - val githubUsername = "github_actor".localOrEnv - val githubToken = "github_token".localOrEnv - mavenCentral() - mavenLocal() - google { - content { - includeGroupByRegex("androidx\\..*") - includeGroupByRegex("com\\.android.*") - } - } - maven { - name = "kernelpanic releases" - url = uri("https://maven.kernelpanicsoft.net/releases") - } - maven { - name = "kernelpanic snapshots" - url = uri("https://maven.kernelpanicsoft.net/snapshots") - } - maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") - maven("https://maven.parchmentmc.org") - maven("https://maven.fabricmc.net/") - maven("https://maven.neoforged.net/releases/") - maven("https://maven.terraformersmc.com/releases/") -// maven("https://thedarkcolour.github.io/KotlinForForge/") - maven("https://repo.nyon.dev/releases") - maven("https://maven.isxander.dev/releases") { - name = "Xander Maven" - } - maven("https://maven.resourcefulbees.com/repository/maven-public/") { - content { - includeGroup("earth.terrarium.common_storage_lib") - } - } - maven { - url = uri("https://maven.pkg.github.com/MrCrayfish/Maven") - credentials { - username = githubUsername - password = githubToken - } - } - maven { - url = uri("https://www.cursemaven.com") - content { - includeGroup("curse.maven") - } - } - - } - - @Suppress("UnstableApiUsage") - dependencies { - "minecraft"(rootProject.libs.minecraft) - "mappings"(loom.layered { - officialMojangMappings() - parchment(rootProject.libs.parchment) - }) - - compileOnly("org.jetbrains:annotations:24.1.0") - } - - extensions.configure("publishing") { - publications { - create("mavenJava") { - artifactId = "${"mod_id".prop}-${base.archivesName.get()}" - from(components["java"]) - } - } - - repositories { - mavenLocal() - maven { - name = "kernelpanicReleases" - url = uri("https://maven.kernelpanicsoft.net/releases") - credentials { - username = "repoLogin".localOrEnv - ?: "maven_username".localOrEnv - ?: "maven_user".localOrEnv - password = "repoPassword".localOrEnv - ?: "maven_password".localOrEnv - ?: "maven_pass".localOrEnv - } - } - } - } - -} - -allprojects { - apply(plugin = "java") - apply(plugin = "org.jetbrains.kotlin.jvm") - apply(plugin = "org.jetbrains.kotlin.plugin.serialization") - apply(plugin = "org.jetbrains.kotlin.plugin.compose") - apply(plugin = "org.jetbrains.compose") - apply(plugin = "dev.opensavvy.dokka-mkdocs") - apply(plugin = "architectury-plugin") - apply(plugin = "maven-publish") - - version = "mod_version".prop!! - group = "mod_group".prop!! - base.archivesName = "mod_id".prop!! - - tasks.withType().configureEach { - options.encoding = "UTF-8" - options.release.set(21) - } - - kotlin { - compilerOptions { - freeCompilerArgs.add("-Xexpect-actual-classes") - } - } - - architectury { - compileOnly() - } - - dokka { - dokkaGeneratorIsolation = ClassLoaderIsolation() -// pluginsConfiguration.html { -// footerMessage = "(c) 2025 Kernel Panic" -// } - } - - java.withSourcesJar() -} - -dependencies { - dokka(project(":common")) { isTransitive = false } - dokka(project(":fabric")) { isTransitive = false } - dokka(project(":neoforge")) { isTransitive = false } -} - -fusioner { - packageGroup = project.group.toString() - mergedJarName = "${project.base.archivesName.get()}-merged-${libs.versions.minecraft.get()}" - jarVersion = project.version.toString() - outputDirectory = "build/artifacts" - - fabric { - inputTaskName = "remapJar" - } - - neoforge { - inputTaskName = "remapJar" - } -} - -tasks { - build { - finalizedBy(fusejars) - } - assemble { - finalizedBy(fusejars) - } -} - -publisher { - apiKeys { - curseforge("curseforge_api_key".localOrEnv) - modrinth("modrinth_api_key".localOrEnv) - } - - debug = true - - curseID = "1029738" - modrinthID = "archie" - githubRepo = "https://github.com/kernel-panic-codecave/Archie" - - projectVersion = "${libs.versions.minecraft.get()}-${project.version}" - displayName = "Archie-Merged-${projectVersion.get()}" - gameVersions = listOf("1.21.1") - loaders = listOf("neoforge", "fabric") - curseEnvironment = "both" - versionType = "alpha" - artifact = tasks.fusejars.get() - javaVersions = listOf(JavaVersion.VERSION_21) - - changelog = file("CHANGELOG.md") - - curseDepends { - required = listOf("fabric-api", "fabric-language-kotlin", "kotlinlangforge", "architectury-api", "cloth-config") - } - - modrinthDepends { - required = listOf("fabric-api", "fabric-language-kotlin", "kotlin-lang-forge", "architectury-api", "cloth-config") - } -} - -tasks { - named("publish") { - dependsOn(publishMod) - } - check { - dependsOn(project(":common").tasks.check) - dependsOn(project(":fabric").tasks.check) - dependsOn(project(":neoforge").tasks.check) - } - register("publishDocs") { - dependsOn(getByName("embedDokkaIntoMkDocs")) - group = "publishing" - val tag = rootProject.version.toString().substringBeforeLast(".") - workingDir = rootDir - commandLine("mike", "deploy", "--push", "--update-aliases", tag, "latest") - } - // modpublisher's `changelog = file("CHANGELOG.md")` (see the `publisher { }` block above) - // reads that file straight off disk when a publish task runs - it doesn't know about git tags - // or PRs. .github/workflows/release-notes.yaml (reactive, post-tag) can't help here: by the - // time it would generate this release's entry, the publish task attached to the tag has - // already read (and shipped) whatever was on disk before. This task closes that gap by - // generating CHANGELOG.md synchronously, right before any publish task reads it - see - // .github/scripts/generate_release_notes.py's module docstring for the two call shapes. - register("generateChangelog") { - group = "publishing" - workingDir = rootDir - commandLine( - "python3", "../.github/scripts/generate_release_notes.py", - "--repo", "mod_source".prop!!.removePrefix("https://github.com/"), - "--new-tag", "v${project.version}", - "--range-end", "HEAD", - "--changelog-path", "CHANGELOG.md", - ) - } - listOf("publishCurseforge", "publishModrinth", "publishGitHub", "publishMod").forEach { - named(it) { dependsOn(getByName("generateChangelog")) } - } -} diff --git a/Archie/common/build.gradle.kts b/Archie/common/build.gradle.kts deleted file mode 100644 index a0a793032..000000000 --- a/Archie/common/build.gradle.kts +++ /dev/null @@ -1,184 +0,0 @@ -import org.gradle.api.tasks.testing.logging.TestExceptionFormat -import org.jetbrains.kotlin.konan.properties.loadProperties - -architectury { - common("fabric", "neoforge") -} - -actualizer { - stubUnfulfilledExpects() -} - -val localProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("local.properties") - val sharedPropsFile = rootDir.resolve("../local.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val sharedProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("gradle.properties") - val sharedPropsFile = rootDir.resolve("../gradle.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val String.prop: String? - get() = sharedProperties?.get(this)?.toString() - -val String.local: String? - get() = localProperties?.get(this)?.toString() - -val String.env: String? - get() = System.getenv(this) - -val String.localOrEnv: String? - get() = localProperties?.get(this)?.toString() ?: System.getenv(this.uppercase()) - - -loom { - log4jConfigs.from(rootDir.resolve("../log4j-dev.xml")) - accessWidenerPath = file("src/main/resources/${"mod_id".prop}.accesswidener") -} - -sourceSets { - main { - kotlin { - srcDir("src/main/gametest") - srcDir("src/main/datagen") - } - java { - srcDir("src/main/mixin") - } - } -} - -dependencies { - compileOnly(kotlin("reflect")) - implementation(libs.junit.jupiter.api) - // Used by the client GameTest harness only (AClientGameTestHarness.kt) to give ComposeScreen a - // virtual clock/dispatcher during tests. compileOnly (not implementation/api) deliberately - - // this must never end up in the shipped jar. ComposeScreen itself never references - // kotlinx.coroutines.test.* symbols directly (only AClientGameTestHarness.kt's method bodies - // do, and those only ever run under AGameTestPlatform.isGameTest), so a real player's game - - // which never has this on its classpath - never needs to resolve it. Loader modules add it - // back as runtimeOnly (not bundled - see AGENTS.md) so local `runGametestClient` has it. - compileOnly(libs.kotlinx.coroutines.test) - testImplementation(libs.junit.jupiter.api) - testImplementation(kotlin("reflect")) - testRuntimeOnly(libs.junit.jupiter.engine) - api(libs.kotlinx.serialization) - api(libs.kotlinx.serialization.json) - api(libs.kotlinx.serialization.nbt) { isTransitive = false } - api(libs.kotlinx.serialization.toml) { isTransitive = false } - api(libs.kotlinx.serialization.json5) { isTransitive = false } - api(libs.kotlinx.serialization.cbor) { isTransitive = false } - api(compose.runtime) - // We depend on fabric loader here to use the fabric @Environment annotations and get the mixin dependencies - // Do NOT use other classes from fabric loader - modImplementation(libs.fabric.loader) - - modApi(libs.rei.common) - modCompileOnly(libs.catalogue.common) - modCompileOnly(libs.clothConfig.common) - modCompileOnly(libs.yacl.common) - modApi(libs.architectury.common) - modApi(libs.storage.common) - modApi(libs.storage.resources.common) -} - -tasks { - base.archivesName.set(base.archivesName.get() + "-common") - - val verifyGuiSpriteAssets by registering { - group = "verification" - description = "Verifies GUI sprite metadata files have matching PNG assets." - - doLast { - val spritesDir = file("src/main/resources/assets/archie/textures/gui/sprites") - if (!spritesDir.exists()) return@doLast - - val missingPng = spritesDir - .walkTopDown() - .filter { it.isFile && it.name.endsWith(".png.mcmeta") } - .map { it to file(it.path.removeSuffix(".mcmeta")) } - .filter { (_, png) -> !png.exists() } - .map { (meta, _) -> meta.relativeTo(projectDir).invariantSeparatorsPath } - .toList() - - if (missingPng.isNotEmpty()) { - val details = missingPng.joinToString(separator = "\n") { " - $it" } - throw GradleException( - "Found GUI sprite metadata files without matching PNGs:\n$details" - ) - } - } - } - - named("check") { - dependsOn(verifyGuiSpriteAssets) - } - - // Keep stubUnfulfilledExpects()'s generated throwing-actual stubs out of what gets published - - // a consumer with both this jar and a real actual on its classpath must only ever see the - // real one, or Kotlin's actual-resolution can end up preferring the stub. - jar { - from(sourceSets.main.get().output) - exclude("**/*StubKt.class") - } - - sourcesJar { - exclude("**/*Stub.kt") - } - - test { - testClassesDirs = sourceSets.test.get().output.classesDirs - classpath = sourceSets.test.get().runtimeClasspath - useJUnitPlatform() - systemProperty("archie.junit.gametest", "true") - // Overridable via -Darchie.junit.gametest.matrix=... (a plain JVM system property, not a - // Gradle project property, since -P doesn't propagate across includeBuild() boundaries in - // this composite build - a CI job matrix needs to reach both Archie's and Archie-Test's - // :common:test at once). - systemProperty( - "archie.junit.gametest.matrix", - System.getProperty("archie.junit.gametest.matrix") ?: "fabric:server,fabric:client,neoforge:server,neoforge:client", - ) - systemProperty("archie.junit.gametest.timeoutMinutes", "20") - systemProperty("archie.junit.gametest.root", rootProject.rootDir.absolutePath) - testLogging { - exceptionFormat = TestExceptionFormat.FULL - } - } -} - -publishing { - publications.create("mavenCommon") { - artifactId = base.archivesName.get() - from(components["java"]) - } - - repositories { - mavenLocal() - maven { - name = "Reposilite" - val releasesUrl = "https://maven.kernelpanicsoft.net/releases" - val snapshotsUrl = "https://maven.kernelpanicsoft.net/snapshots" - - url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsUrl else releasesUrl) - - credentials { - username = localProperties?.getProperty("reposilite.username") - ?: System.getenv("REPOSILITE_USERNAME") - password = localProperties?.getProperty("reposilite.password") - ?: System.getenv("REPOSILITE_PASSWORD") - } - } - } -} diff --git a/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt b/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt deleted file mode 100644 index 91a3573ce..000000000 --- a/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.kernelpanicsoft.archie.data.internal - -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.data.ADataGenerator -import net.kernelpanicsoft.archie.data.ADatagenEventObject -import net.kernelpanicsoft.archie.data.common.conditions.withCondition -import net.kernelpanicsoft.archie.data.common.crafting.ingredients.AComponentsIngredient -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags -import net.kernelpanicsoft.archie.data.internal.common.tags.* -import net.minecraft.core.component.DataComponents -import net.minecraft.data.recipes.RecipeCategory -import net.minecraft.network.chat.Component -import net.minecraft.world.item.Items -import net.minecraft.world.item.crafting.Ingredient -import net.minecraft.world.level.block.Blocks - -/** - * Archie's own datagen registration, used both to populate the vanilla-derived common ("c") tags - * ([AInternalBlockTagsProvider] and friends) that ship with the library and as a smoke test for - * the datagen DSL itself (e.g. the emerald-from-diamond shapeless recipe below). - */ -internal object ArchieDatagen : ADatagenEventObject(Archie.MOD) -{ - override fun ADataGenerator.handler() - { - client { - languages { - add("archie.networking.config.no_permissions", "You do not have the required permissions to edit the server config") - } - } - common { - blockTags(::AInternalBlockTagsProvider) - itemTags(::AInternalItemTagsProvider) - biomeTags(::AInternalBiomeTagsProvider) - entityTags(::AInternalEntityTypeTagsProvider) - fluidTags(::AInternalFluidTagsProvider) - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt b/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt deleted file mode 100644 index 52e0f7ee2..000000000 --- a/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt +++ /dev/null @@ -1,349 +0,0 @@ -package net.kernelpanicsoft.archie.data.internal.common.tags - -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags -import net.minecraft.core.HolderLookup -import net.minecraft.data.PackOutput -import net.minecraft.tags.BiomeTags -import net.minecraft.world.level.biome.Biomes -import java.util.concurrent.CompletableFuture - -/** - * Populates Archie's vanilla-derived common ("c") biome tags (see [ACommonTags.Biomes]) with - * their vanilla biome members, so downstream mods can depend on the `c` tag convention without - * every mod having to redeclare it. - */ -class AInternalBiomeTagsProvider( - output: PackOutput, registriesFuture: CompletableFuture -) : ATagsProvider.BiomeTagsProvider(output, Archie.MOD, registriesFuture, false) -{ - override fun generate(registries: HolderLookup.Provider) - { - ACommonTags.Biomes.NO_DEFAULT_MONSTERS += listOf( - Biomes.MUSHROOM_FIELDS, Biomes.DEEP_DARK - ) - ACommonTags.Biomes.HIDDEN_FROM_LOCATOR_SELECTION() // Create tag file for visibility - - ACommonTags.Biomes.IS_VOID += Biomes.THE_VOID - - ACommonTags.Biomes.IS_END += BiomeTags.IS_END - ACommonTags.Biomes.IS_NETHER += BiomeTags.IS_NETHER - ACommonTags.Biomes.IS_OVERWORLD += BiomeTags.IS_OVERWORLD - - ACommonTags.Biomes.IS_HOT_OVERWORLD += listOf( - Biomes.SWAMP, - Biomes.MANGROVE_SWAMP, - Biomes.JUNGLE, - Biomes.BAMBOO_JUNGLE, - Biomes.SPARSE_JUNGLE, - Biomes.DESERT, - Biomes.ERODED_BADLANDS, - Biomes.SAVANNA, - Biomes.SAVANNA_PLATEAU, - Biomes.WINDSWEPT_SAVANNA, - Biomes.STONY_PEAKS, - Biomes.WARM_OCEAN - ) - ACommonTags.Biomes.IS_HOT_NETHER += listOf( - Biomes.NETHER_WASTES, - Biomes.CRIMSON_FOREST, - Biomes.WARPED_FOREST, - Biomes.SOUL_SAND_VALLEY, - Biomes.BASALT_DELTAS - ) - ACommonTags.Biomes.IS_HOT_END() - ACommonTags.Biomes.IS_HOT { - addTags( - ACommonTags.Biomes.IS_HOT_OVERWORLD, - ACommonTags.Biomes.IS_HOT_NETHER - ) - addOptionalTag(ACommonTags.Biomes.IS_HOT_END) - } - - ACommonTags.Biomes.IS_COLD_OVERWORLD += listOf( - Biomes.TAIGA, - Biomes.OLD_GROWTH_PINE_TAIGA, - Biomes.SNOWY_PLAINS, - Biomes.ICE_SPIKES, - Biomes.GROVE, - Biomes.SNOWY_SLOPES, - Biomes.JAGGED_PEAKS, - Biomes.FROZEN_PEAKS, - Biomes.SNOWY_BEACH, - Biomes.SNOWY_TAIGA, - Biomes.FROZEN_RIVER, - Biomes.COLD_OCEAN, - Biomes.FROZEN_OCEAN, - Biomes.DEEP_COLD_OCEAN, - Biomes.DEEP_FROZEN_OCEAN - ) - ACommonTags.Biomes.IS_COLD_NETHER() - ACommonTags.Biomes.IS_COLD_END += listOf( - Biomes.THE_END, - Biomes.SMALL_END_ISLANDS, - Biomes.END_MIDLANDS, - Biomes.END_HIGHLANDS, - Biomes.END_BARRENS - ) - ACommonTags.Biomes.IS_COLD { - addTags( - ACommonTags.Biomes.IS_COLD_OVERWORLD, - ACommonTags.Biomes.IS_COLD_END - ) - addOptionalTag(ACommonTags.Biomes.IS_COLD_NETHER.location()) - } - - ACommonTags.Biomes.IS_SPARSE_VEGETATION_OVERWORLD += listOf( - Biomes.WOODED_BADLANDS, - Biomes.ERODED_BADLANDS, - Biomes.SAVANNA, - Biomes.SAVANNA_PLATEAU, - Biomes.WINDSWEPT_SAVANNA, - Biomes.WINDSWEPT_FOREST, - Biomes.WINDSWEPT_HILLS, - Biomes.WINDSWEPT_GRAVELLY_HILLS, - Biomes.SNOWY_SLOPES, - Biomes.JAGGED_PEAKS, - Biomes.FROZEN_PEAKS - ) - ACommonTags.Biomes.IS_SPARSE_VEGETATION_NETHER() - ACommonTags.Biomes.IS_SPARSE_VEGETATION_END() - ACommonTags.Biomes.IS_SPARSE_VEGETATION { - addTag(ACommonTags.Biomes.IS_SPARSE_VEGETATION_OVERWORLD) - addOptionalTags( - ACommonTags.Biomes.IS_SPARSE_VEGETATION_NETHER, - ACommonTags.Biomes.IS_SPARSE_VEGETATION_END - ) - } - - ACommonTags.Biomes.IS_DENSE_VEGETATION_OVERWORLD += listOf( - Biomes.DARK_FOREST, - Biomes.OLD_GROWTH_BIRCH_FOREST, - Biomes.OLD_GROWTH_SPRUCE_TAIGA, - Biomes.JUNGLE - ) - ACommonTags.Biomes.IS_DENSE_VEGETATION_NETHER() - ACommonTags.Biomes.IS_DENSE_VEGETATION_END() - ACommonTags.Biomes.IS_DENSE_VEGETATION { - addTag(ACommonTags.Biomes.IS_DENSE_VEGETATION_OVERWORLD) - addOptionalTags( - ACommonTags.Biomes.IS_DENSE_VEGETATION_NETHER, - ACommonTags.Biomes.IS_DENSE_VEGETATION_END - ) - } - - ACommonTags.Biomes.IS_WET_OVERWORLD += listOf( - Biomes.SWAMP, - Biomes.MANGROVE_SWAMP, - Biomes.JUNGLE, - Biomes.BAMBOO_JUNGLE, - Biomes.SPARSE_JUNGLE, - Biomes.BEACH, - Biomes.LUSH_CAVES, - Biomes.DRIPSTONE_CAVES - ) - ACommonTags.Biomes.IS_WET_NETHER() - ACommonTags.Biomes.IS_WET_END() - ACommonTags.Biomes.IS_WET { - addTag(ACommonTags.Biomes.IS_WET_OVERWORLD) - addOptionalTags( - ACommonTags.Biomes.IS_WET_NETHER, - ACommonTags.Biomes.IS_WET_END - ) - } - - ACommonTags.Biomes.IS_DRY_OVERWORLD += listOf( - Biomes.DESERT, - Biomes.BADLANDS, - Biomes.WOODED_BADLANDS, - Biomes.ERODED_BADLANDS, - Biomes.SAVANNA, - Biomes.SAVANNA_PLATEAU, - Biomes.WINDSWEPT_SAVANNA - ) - ACommonTags.Biomes.IS_DRY_NETHER += listOf( - Biomes.NETHER_WASTES, - Biomes.CRIMSON_FOREST, - Biomes.WARPED_FOREST, - Biomes.SOUL_SAND_VALLEY, - Biomes.BASALT_DELTAS - ) - ACommonTags.Biomes.IS_DRY_END += listOf( - Biomes.THE_END, - Biomes.SMALL_END_ISLANDS, - Biomes.END_MIDLANDS, - Biomes.END_HIGHLANDS, - Biomes.END_BARRENS - ) - ACommonTags.Biomes.IS_DRY += listOf( - ACommonTags.Biomes.IS_DRY_OVERWORLD, - ACommonTags.Biomes.IS_DRY_NETHER, - ACommonTags.Biomes.IS_DRY_END - ) - - ACommonTags.Biomes.IS_CONIFEROUS_TREE += ACommonTags.Biomes.IS_TAIGA - ACommonTags.Biomes.IS_CONIFEROUS_TREE += Biomes.GROVE - - ACommonTags.Biomes.IS_SAVANNA_TREE += ACommonTags.Biomes.IS_SAVANNA - ACommonTags.Biomes.IS_JUNGLE_TREE += ACommonTags.Biomes.IS_JUNGLE - ACommonTags.Biomes.IS_DECIDUOUS_TREE += listOf( - Biomes.FOREST, - Biomes.FLOWER_FOREST, - Biomes.BIRCH_FOREST, - Biomes.DARK_FOREST, - Biomes.OLD_GROWTH_BIRCH_FOREST, - Biomes.WINDSWEPT_FOREST - ) - - ACommonTags.Biomes.IS_MOUNTAIN_SLOPE += listOf( - Biomes.SNOWY_SLOPES, - Biomes.MEADOW, - Biomes.GROVE, - Biomes.CHERRY_GROVE - ) - ACommonTags.Biomes.IS_MOUNTAIN_PEAK += listOf( - Biomes.JAGGED_PEAKS, - Biomes.FROZEN_PEAKS, - Biomes.STONY_PEAKS - ) - ACommonTags.Biomes.IS_MOUNTAIN += listOf( - BiomeTags.IS_MOUNTAIN, - ACommonTags.Biomes.IS_MOUNTAIN_PEAK, - ACommonTags.Biomes.IS_MOUNTAIN_SLOPE - ) - - ACommonTags.Biomes.IS_FOREST += BiomeTags.IS_FOREST - ACommonTags.Biomes.IS_BIRCH_FOREST += listOf( - Biomes.BIRCH_FOREST, - Biomes.OLD_GROWTH_BIRCH_FOREST - ) - ACommonTags.Biomes.IS_FLOWER_FOREST += Biomes.FLOWER_FOREST - ACommonTags.Biomes.IS_FLORAL += ACommonTags.Biomes.IS_FLOWER_FOREST - ACommonTags.Biomes.IS_FLORAL += listOf( - Biomes.SUNFLOWER_PLAINS, - Biomes.CHERRY_GROVE, - Biomes.MEADOW - ) - ACommonTags.Biomes.IS_BEACH += BiomeTags.IS_BEACH - ACommonTags.Biomes.IS_STONY_SHORES += Biomes.STONY_SHORE - ACommonTags.Biomes.IS_DESERT += Biomes.DESERT - ACommonTags.Biomes.IS_BADLANDS += BiomeTags.IS_BADLANDS - ACommonTags.Biomes.IS_PLAINS += listOf( - Biomes.PLAINS, - Biomes.SUNFLOWER_PLAINS - ) - ACommonTags.Biomes.IS_SNOWY_PLAINS += Biomes.SNOWY_PLAINS - ACommonTags.Biomes.IS_TAIGA += BiomeTags.IS_TAIGA - ACommonTags.Biomes.IS_HILL += BiomeTags.IS_HILL - ACommonTags.Biomes.IS_WINDSWEPT += listOf( - Biomes.WINDSWEPT_HILLS, - Biomes.WINDSWEPT_GRAVELLY_HILLS, - Biomes.WINDSWEPT_FOREST, - Biomes.WINDSWEPT_SAVANNA - ) - ACommonTags.Biomes.IS_SAVANNA += BiomeTags.IS_SAVANNA - ACommonTags.Biomes.IS_JUNGLE += BiomeTags.IS_JUNGLE - ACommonTags.Biomes.IS_SNOWY += listOf( - Biomes.SNOWY_BEACH, - Biomes.SNOWY_PLAINS, - Biomes.ICE_SPIKES, - Biomes.SNOWY_TAIGA, - Biomes.GROVE, - Biomes.SNOWY_SLOPES, - Biomes.JAGGED_PEAKS, - Biomes.FROZEN_PEAKS - ) - ACommonTags.Biomes.IS_ICY += listOf( - Biomes.ICE_SPIKES, - Biomes.FROZEN_PEAKS - ) - ACommonTags.Biomes.IS_SWAMP += listOf( - Biomes.SWAMP, - Biomes.MANGROVE_SWAMP - ) - ACommonTags.Biomes.IS_OLD_GROWTH += listOf( - Biomes.OLD_GROWTH_BIRCH_FOREST, - Biomes.OLD_GROWTH_PINE_TAIGA, - Biomes.OLD_GROWTH_SPRUCE_TAIGA - ) - ACommonTags.Biomes.IS_LUSH += Biomes.LUSH_CAVES - ACommonTags.Biomes.IS_SANDY += listOf( - Biomes.DESERT, - Biomes.BADLANDS, - Biomes.WOODED_BADLANDS, - Biomes.ERODED_BADLANDS, - Biomes.BEACH - ) - ACommonTags.Biomes.IS_MUSHROOM += Biomes.MUSHROOM_FIELDS - ACommonTags.Biomes.IS_PLATEAU += listOf( - Biomes.WOODED_BADLANDS, - Biomes.SAVANNA_PLATEAU, - Biomes.CHERRY_GROVE, - Biomes.MEADOW - ) - ACommonTags.Biomes.IS_SPOOKY += listOf( - Biomes.DARK_FOREST, - Biomes.DEEP_DARK - ) - ACommonTags.Biomes.IS_WASTELAND() - ACommonTags.Biomes.IS_RARE += listOf( - Biomes.SUNFLOWER_PLAINS, - Biomes.FLOWER_FOREST, - Biomes.OLD_GROWTH_BIRCH_FOREST, - Biomes.OLD_GROWTH_SPRUCE_TAIGA, - Biomes.BAMBOO_JUNGLE, - Biomes.SPARSE_JUNGLE, - Biomes.ERODED_BADLANDS, - Biomes.SAVANNA_PLATEAU, - Biomes.WINDSWEPT_SAVANNA, - Biomes.ICE_SPIKES, - Biomes.WINDSWEPT_GRAVELLY_HILLS, - Biomes.MUSHROOM_FIELDS, - Biomes.DEEP_DARK - ) - - ACommonTags.Biomes.IS_RIVER += BiomeTags.IS_RIVER - ACommonTags.Biomes.IS_SHALLOW_OCEAN += listOf( - Biomes.OCEAN, - Biomes.LUKEWARM_OCEAN, - Biomes.WARM_OCEAN, - Biomes.COLD_OCEAN, - Biomes.FROZEN_OCEAN - ) - ACommonTags.Biomes.IS_DEEP_OCEAN += BiomeTags.IS_DEEP_OCEAN - ACommonTags.Biomes.IS_OCEAN += listOf( - BiomeTags.IS_OCEAN, - ACommonTags.Biomes.IS_SHALLOW_OCEAN, - ACommonTags.Biomes.IS_DEEP_OCEAN - ) - ACommonTags.Biomes.IS_AQUATIC_ICY += listOf( - Biomes.FROZEN_RIVER, - Biomes.DEEP_FROZEN_OCEAN, - Biomes.FROZEN_OCEAN - ) - ACommonTags.Biomes.IS_AQUATIC += listOf( - ACommonTags.Biomes.IS_OCEAN, - ACommonTags.Biomes.IS_RIVER - ) - - ACommonTags.Biomes.IS_CAVE += listOf( - Biomes.LUSH_CAVES, - Biomes.DRIPSTONE_CAVES, - Biomes.DEEP_DARK - ) - ACommonTags.Biomes.IS_UNDERGROUND += ACommonTags.Biomes.IS_CAVE - - ACommonTags.Biomes.IS_NETHER_FOREST += listOf( - Biomes.CRIMSON_FOREST, - Biomes.WARPED_FOREST - ) - ACommonTags.Biomes.IS_OUTER_END_ISLAND += listOf( - Biomes.END_HIGHLANDS, - Biomes.END_MIDLANDS, - Biomes.END_BARRENS - ) - - } - -} \ No newline at end of file diff --git a/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt b/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt deleted file mode 100644 index 53bf4cd82..000000000 --- a/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt +++ /dev/null @@ -1,382 +0,0 @@ -package net.kernelpanicsoft.archie.data.internal.common.tags - -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags -import dev.architectury.platform.Platform -import net.minecraft.core.HolderLookup -import net.minecraft.core.registries.BuiltInRegistries -import net.minecraft.data.PackOutput -import net.minecraft.resources.ResourceLocation -import net.minecraft.tags.BlockTags -import net.minecraft.tags.TagKey -import net.minecraft.world.item.DyeColor -import net.minecraft.world.level.block.Block -import net.minecraft.world.level.block.Blocks -import java.util.concurrent.CompletableFuture -import java.util.function.Consumer - -/** - * Populates Archie's vanilla-derived common ("c") block tags (see [ACommonTags.Blocks]) with - * their vanilla block members, so downstream mods can depend on the `c` tag convention without - * every mod having to redeclare it. - */ -class AInternalBlockTagsProvider( - output: PackOutput, - lookupProvider: CompletableFuture -) : - ATagsProvider.BlockTagsProvider( - output, Archie.MOD, - lookupProvider, - false - ) -{ - override fun generate(registries: HolderLookup.Provider) - { - if (Platform.isNeoForge()) - { - ACommonTags.Blocks.ENDERMAN_PLACE_ON_BLACKLIST() - } - ACommonTags.Blocks.BARRELS += ACommonTags.Blocks.BARRELS_WOODEN - ACommonTags.Blocks.BARRELS_WOODEN += Blocks.BARREL - ACommonTags.Blocks.BOOKSHELVES += Blocks.BOOKSHELF - ACommonTags.Blocks.BUDDING_BLOCKS += Blocks.BUDDING_AMETHYST - ACommonTags.Blocks.BUDS += listOf( - Blocks.SMALL_AMETHYST_BUD, - Blocks.MEDIUM_AMETHYST_BUD, - Blocks.LARGE_AMETHYST_BUD - ) - ACommonTags.Blocks.CHAINS += Blocks.CHAIN - ACommonTags.Blocks.CHESTS += listOf( - ACommonTags.Blocks.CHESTS_ENDER, - ACommonTags.Blocks.CHESTS_TRAPPED, - ACommonTags.Blocks.CHESTS_WOODEN - ) - ACommonTags.Blocks.CHESTS_ENDER += Blocks.ENDER_CHEST - ACommonTags.Blocks.CHESTS_TRAPPED += Blocks.TRAPPED_CHEST - ACommonTags.Blocks.CHESTS_WOODEN += listOf( - Blocks.CHEST, - Blocks.TRAPPED_CHEST - ) - ACommonTags.Blocks.CLUSTERS += Blocks.AMETHYST_CLUSTER - ACommonTags.Blocks.SHULKER_BOXES += listOf( - Blocks.SHULKER_BOX, - Blocks.BLUE_SHULKER_BOX, - Blocks.BROWN_SHULKER_BOX, - Blocks.CYAN_SHULKER_BOX, - Blocks.GRAY_SHULKER_BOX, - Blocks.GREEN_SHULKER_BOX, - Blocks.LIGHT_BLUE_SHULKER_BOX, - Blocks.LIGHT_GRAY_SHULKER_BOX, - Blocks.LIME_SHULKER_BOX, - Blocks.MAGENTA_SHULKER_BOX, - Blocks.ORANGE_SHULKER_BOX, - Blocks.PINK_SHULKER_BOX, - Blocks.PURPLE_SHULKER_BOX, - Blocks.RED_SHULKER_BOX, - Blocks.WHITE_SHULKER_BOX, - Blocks.YELLOW_SHULKER_BOX, - Blocks.BLACK_SHULKER_BOX - ) - ACommonTags.Blocks.COBBLESTONES += listOf( - ACommonTags.Blocks.COBBLESTONES_NORMAL, - ACommonTags.Blocks.COBBLESTONES_INFESTED, - ACommonTags.Blocks.COBBLESTONES_MOSSY, - ACommonTags.Blocks.COBBLESTONES_DEEPSLATE - ) - ACommonTags.Blocks.COBBLESTONES_NORMAL += Blocks.COBBLESTONE - ACommonTags.Blocks.COBBLESTONES_INFESTED += Blocks.INFESTED_COBBLESTONE - ACommonTags.Blocks.COBBLESTONES_MOSSY += Blocks.MOSSY_COBBLESTONE - ACommonTags.Blocks.COBBLESTONES_DEEPSLATE += Blocks.COBBLED_DEEPSLATE - ACommonTags.Blocks.END_STONES += Blocks.END_STONE - ACommonTags.Blocks.FENCE_GATES += ACommonTags.Blocks.FENCE_GATES_WOODEN - ACommonTags.Blocks.FENCE_GATES_WOODEN += listOf( - Blocks.OAK_FENCE_GATE, - Blocks.SPRUCE_FENCE_GATE, - Blocks.BIRCH_FENCE_GATE, - Blocks.JUNGLE_FENCE_GATE, - Blocks.ACACIA_FENCE_GATE, - Blocks.DARK_OAK_FENCE_GATE, - Blocks.CRIMSON_FENCE_GATE, - Blocks.WARPED_FENCE_GATE, - Blocks.MANGROVE_FENCE_GATE, - Blocks.BAMBOO_FENCE_GATE, - Blocks.CHERRY_FENCE_GATE - ) - ACommonTags.Blocks.FENCES += listOf( - ACommonTags.Blocks.FENCES_NETHER_BRICK, - ACommonTags.Blocks.FENCES_WOODEN - ) - ACommonTags.Blocks.FENCES_NETHER_BRICK += Blocks.NETHER_BRICK_FENCE - ACommonTags.Blocks.FENCES_WOODEN += BlockTags.WOODEN_FENCES - ACommonTags.Blocks.GLASS_BLOCKS += listOf( - ACommonTags.Blocks.GLASS_BLOCKS_COLORLESS, - ACommonTags.Blocks.GLASS_BLOCKS_STAINED, - ACommonTags.Blocks.GLASS_BLOCKS_TINTED - ) - ACommonTags.Blocks.GLASS_BLOCKS_COLORLESS += Blocks.GLASS - ACommonTags.Blocks.GLASS_BLOCKS_CHEAP += listOf( - ACommonTags.Blocks.GLASS_BLOCKS_COLORLESS, - ACommonTags.Blocks.GLASS_BLOCKS_STAINED - ) - ACommonTags.Blocks.GLASS_BLOCKS_TINTED += Blocks.TINTED_GLASS - ACommonTags.Blocks.GLASS_PANES += listOf( - ACommonTags.Blocks.GLASS_PANES_COLORLESS, - ACommonTags.Blocks.GLASS_PANES_STAINED - ) - ACommonTags.Blocks.GLASS_PANES_COLORLESS += Blocks.GLASS_PANE - addColoredFlat(ACommonTags.Blocks.GLASS_BLOCKS_STAINED, "{color}_stained_glass") - addColoredFlat(ACommonTags.Blocks.GLASS_PANES_STAINED, "{color}_stained_glass_pane") - addColored(ACommonTags.Blocks.DYED, "{color}_banner") - addColored(ACommonTags.Blocks.DYED, "{color}_bed") - addColored(ACommonTags.Blocks.DYED, "{color}_candle") - addColored(ACommonTags.Blocks.DYED, "{color}_carpet") - addColored(ACommonTags.Blocks.DYED, "{color}_concrete") - addColored(ACommonTags.Blocks.DYED, "{color}_concrete_powder") - addColored(ACommonTags.Blocks.DYED, "{color}_glazed_terracotta") - addColored(ACommonTags.Blocks.DYED, "{color}_shulker_box") - addColored(ACommonTags.Blocks.DYED, "{color}_stained_glass") - addColored(ACommonTags.Blocks.DYED, "{color}_stained_glass_pane") - addColored(ACommonTags.Blocks.DYED, "{color}_terracotta") - addColored(ACommonTags.Blocks.DYED, "{color}_wall_banner") - addColored(ACommonTags.Blocks.DYED, "{color}_wool") - ACommonTags.Blocks.HIDDEN_FROM_RECIPE_VIEWERS() - ACommonTags.Blocks.GRAVELS += Blocks.GRAVEL - ACommonTags.Blocks.SKULLS += listOf( - Blocks.SKELETON_SKULL, - Blocks.SKELETON_WALL_SKULL, - Blocks.WITHER_SKELETON_SKULL, - Blocks.WITHER_SKELETON_WALL_SKULL, - Blocks.PLAYER_HEAD, - Blocks.PLAYER_WALL_HEAD, - Blocks.ZOMBIE_HEAD, - Blocks.ZOMBIE_WALL_HEAD, - Blocks.CREEPER_HEAD, - Blocks.CREEPER_WALL_HEAD, - Blocks.PIGLIN_HEAD, - Blocks.PIGLIN_WALL_HEAD, - Blocks.DRAGON_HEAD, - Blocks.DRAGON_WALL_HEAD - ) - - ACommonTags.Blocks.NETHERRACKS += Blocks.NETHERRACK - ACommonTags.Blocks.OBSIDIANS += Blocks.OBSIDIAN - ACommonTags.Blocks.ORE_BEARING_GROUND_DEEPSLATE += Blocks.DEEPSLATE - ACommonTags.Blocks.ORE_BEARING_GROUND_NETHERRACK += Blocks.NETHERRACK - ACommonTags.Blocks.ORE_BEARING_GROUND_STONE += Blocks.STONE - ACommonTags.Blocks.ORE_RATES_DENSE += listOf( - Blocks.COPPER_ORE, - Blocks.DEEPSLATE_COPPER_ORE, - Blocks.DEEPSLATE_LAPIS_ORE, - Blocks.DEEPSLATE_REDSTONE_ORE, - Blocks.LAPIS_ORE, - Blocks.REDSTONE_ORE - ) - ACommonTags.Blocks.ORE_RATES_SINGULAR += listOf( - Blocks.ANCIENT_DEBRIS, - Blocks.COAL_ORE, - Blocks.DEEPSLATE_COAL_ORE, - Blocks.DEEPSLATE_DIAMOND_ORE, - Blocks.DEEPSLATE_EMERALD_ORE, - Blocks.DEEPSLATE_GOLD_ORE, - Blocks.DEEPSLATE_IRON_ORE, - Blocks.DIAMOND_ORE, - Blocks.EMERALD_ORE, - Blocks.GOLD_ORE, - Blocks.IRON_ORE, - Blocks.NETHER_QUARTZ_ORE - ) - ACommonTags.Blocks.ORE_RATES_SPARSE += Blocks.NETHER_GOLD_ORE - ACommonTags.Blocks.ORES += listOf( - ACommonTags.Blocks.ORES_COAL, - ACommonTags.Blocks.ORES_COPPER, - ACommonTags.Blocks.ORES_DIAMOND, - ACommonTags.Blocks.ORES_EMERALD, - ACommonTags.Blocks.ORES_GOLD, - ACommonTags.Blocks.ORES_IRON, - ACommonTags.Blocks.ORES_LAPIS, - ACommonTags.Blocks.ORES_REDSTONE, - ACommonTags.Blocks.ORES_QUARTZ, - ACommonTags.Blocks.ORES_NETHERITE_SCRAP - ) - ACommonTags.Blocks.ORES_COAL += BlockTags.COAL_ORES - ACommonTags.Blocks.ORES_COPPER += BlockTags.COPPER_ORES - ACommonTags.Blocks.ORES_DIAMOND += BlockTags.DIAMOND_ORES - ACommonTags.Blocks.ORES_EMERALD += BlockTags.EMERALD_ORES - ACommonTags.Blocks.ORES_GOLD += BlockTags.GOLD_ORES - ACommonTags.Blocks.ORES_IRON += BlockTags.IRON_ORES - ACommonTags.Blocks.ORES_LAPIS += BlockTags.LAPIS_ORES - ACommonTags.Blocks.ORES_QUARTZ += Blocks.NETHER_QUARTZ_ORE - ACommonTags.Blocks.ORES_REDSTONE += BlockTags.REDSTONE_ORES - ACommonTags.Blocks.ORES_NETHERITE_SCRAP += Blocks.ANCIENT_DEBRIS - ACommonTags.Blocks.ORES_IN_GROUND_DEEPSLATE += listOf( - Blocks.DEEPSLATE_COAL_ORE, - Blocks.DEEPSLATE_COPPER_ORE, - Blocks.DEEPSLATE_DIAMOND_ORE, - Blocks.DEEPSLATE_EMERALD_ORE, - Blocks.DEEPSLATE_GOLD_ORE, - Blocks.DEEPSLATE_IRON_ORE, - Blocks.DEEPSLATE_LAPIS_ORE, - Blocks.DEEPSLATE_REDSTONE_ORE - ) - ACommonTags.Blocks.ORES_IN_GROUND_NETHERRACK += listOf( - Blocks.NETHER_GOLD_ORE, - Blocks.NETHER_QUARTZ_ORE - ) - ACommonTags.Blocks.ORES_IN_GROUND_STONE += listOf( - Blocks.COAL_ORE, - Blocks.COPPER_ORE, - Blocks.DIAMOND_ORE, - Blocks.EMERALD_ORE, - Blocks.GOLD_ORE, - Blocks.IRON_ORE, - Blocks.LAPIS_ORE, - Blocks.REDSTONE_ORE - ) - ACommonTags.Blocks.PLAYER_WORKSTATIONS_CRAFTING_TABLES += Blocks.CRAFTING_TABLE - ACommonTags.Blocks.PLAYER_WORKSTATIONS_FURNACES += Blocks.FURNACE - ACommonTags.Blocks.RELOCATION_NOT_SUPPORTED() - ACommonTags.Blocks.ROPES() - ACommonTags.Blocks.SANDS += listOf( - ACommonTags.Blocks.SANDS_COLORLESS, - ACommonTags.Blocks.SANDS_RED - ) - ACommonTags.Blocks.SANDS_COLORLESS += Blocks.SAND - ACommonTags.Blocks.SANDS_RED += Blocks.RED_SAND - ACommonTags.Blocks.SANDSTONE_RED_BLOCKS += listOf( - Blocks.RED_SANDSTONE, - Blocks.CUT_RED_SANDSTONE, - Blocks.CHISELED_RED_SANDSTONE, - Blocks.SMOOTH_RED_SANDSTONE - ) - ACommonTags.Blocks.SANDSTONE_UNCOLORED_BLOCKS += listOf( - Blocks.SANDSTONE, - Blocks.CUT_SANDSTONE, - Blocks.CHISELED_SANDSTONE, - Blocks.SMOOTH_SANDSTONE - ) - ACommonTags.Blocks.SANDSTONE_BLOCKS += listOf( - ACommonTags.Blocks.SANDSTONE_RED_BLOCKS, - ACommonTags.Blocks.SANDSTONE_UNCOLORED_BLOCKS - ) - ACommonTags.Blocks.SANDSTONE_RED_SLABS += listOf( - Blocks.RED_SANDSTONE_SLAB, - Blocks.CUT_RED_SANDSTONE_SLAB, - Blocks.SMOOTH_RED_SANDSTONE_SLAB - ) - ACommonTags.Blocks.SANDSTONE_UNCOLORED_SLABS += listOf( - Blocks.SANDSTONE_SLAB, - Blocks.CUT_SANDSTONE_SLAB, - Blocks.SMOOTH_SANDSTONE_SLAB - ) - ACommonTags.Blocks.SANDSTONE_SLABS += listOf( - ACommonTags.Blocks.SANDSTONE_RED_SLABS, - ACommonTags.Blocks.SANDSTONE_UNCOLORED_SLABS - ) - ACommonTags.Blocks.SANDSTONE_RED_STAIRS += listOf( - Blocks.RED_SANDSTONE_STAIRS, - Blocks.SMOOTH_RED_SANDSTONE_STAIRS - ) - ACommonTags.Blocks.SANDSTONE_UNCOLORED_STAIRS += listOf( - Blocks.SANDSTONE_STAIRS, - Blocks.SMOOTH_SANDSTONE_STAIRS - ) - ACommonTags.Blocks.SANDSTONE_STAIRS += listOf( - ACommonTags.Blocks.SANDSTONE_RED_STAIRS, - ACommonTags.Blocks.SANDSTONE_UNCOLORED_STAIRS - ) - ACommonTags.Blocks.STONES += listOf( - Blocks.ANDESITE, - Blocks.DIORITE, - Blocks.GRANITE, - Blocks.STONE, - Blocks.DEEPSLATE, - Blocks.TUFF - ) - ACommonTags.Blocks.STORAGE_BLOCKS += listOf( - ACommonTags.Blocks.STORAGE_BLOCKS_BONE_MEAL, - ACommonTags.Blocks.STORAGE_BLOCKS_AMETHYST, - ACommonTags.Blocks.STORAGE_BLOCKS_COAL, - ACommonTags.Blocks.STORAGE_BLOCKS_COPPER, - ACommonTags.Blocks.STORAGE_BLOCKS_DIAMOND, - ACommonTags.Blocks.STORAGE_BLOCKS_DRIED_KELP, - ACommonTags.Blocks.STORAGE_BLOCKS_EMERALD, - ACommonTags.Blocks.STORAGE_BLOCKS_GOLD, - ACommonTags.Blocks.STORAGE_BLOCKS_IRON, - ACommonTags.Blocks.STORAGE_BLOCKS_LAPIS, - ACommonTags.Blocks.STORAGE_BLOCKS_QUARTZ, - ACommonTags.Blocks.STORAGE_BLOCKS_RAW_COPPER, - ACommonTags.Blocks.STORAGE_BLOCKS_RAW_GOLD, - ACommonTags.Blocks.STORAGE_BLOCKS_RAW_IRON, - ACommonTags.Blocks.STORAGE_BLOCKS_REDSTONE, - ACommonTags.Blocks.STORAGE_BLOCKS_NETHERITE, - ACommonTags.Blocks.STORAGE_BLOCKS_SLIME, - ACommonTags.Blocks.STORAGE_BLOCKS_WHEAT - ) - ACommonTags.Blocks.STORAGE_BLOCKS_BONE_MEAL += Blocks.BONE_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_AMETHYST += Blocks.AMETHYST_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_COAL += Blocks.COAL_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_COPPER += Blocks.COPPER_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_DIAMOND += Blocks.DIAMOND_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_DRIED_KELP += Blocks.DRIED_KELP_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_EMERALD += Blocks.EMERALD_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_GOLD += Blocks.GOLD_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_IRON += Blocks.IRON_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_LAPIS += Blocks.LAPIS_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_QUARTZ += Blocks.QUARTZ_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_RAW_COPPER += Blocks.RAW_COPPER_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_RAW_GOLD += Blocks.RAW_GOLD_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_RAW_IRON += Blocks.RAW_IRON_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_REDSTONE += Blocks.REDSTONE_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_NETHERITE += Blocks.NETHERITE_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_SLIME += Blocks.SLIME_BLOCK - ACommonTags.Blocks.STORAGE_BLOCKS_WHEAT += Blocks.HAY_BLOCK - ACommonTags.Blocks.VILLAGER_JOB_SITES += listOf( - Blocks.BARREL, Blocks.BLAST_FURNACE, Blocks.BREWING_STAND, Blocks.CARTOGRAPHY_TABLE, - Blocks.CAULDRON, Blocks.WATER_CAULDRON, Blocks.LAVA_CAULDRON, Blocks.POWDER_SNOW_CAULDRON, - Blocks.COMPOSTER, Blocks.FLETCHING_TABLE, Blocks.GRINDSTONE, Blocks.LECTERN, - Blocks.LOOM, Blocks.SMITHING_TABLE, Blocks.SMOKER, Blocks.STONECUTTER - ) - } - - /** - * For each [DyeColor], resolves the vanilla block named by substituting `{color}` into - * `pattern` and adds it to the per-color common tag `c:{group path}/{color}` (via [getCommonTag]). - */ - private fun addColored(group: TagKey, pattern: String, consumer: Consumer = Consumer {}) - { - val prefix = group.location().path.lowercase() + '/' - for (color in DyeColor.entries) - { - val key = ResourceLocation.fromNamespaceAndPath("minecraft", pattern.replace("{color}", color.getName().lowercase())) - val tag = getCommonTag(prefix + color.getName().lowercase()) - val block = BuiltInRegistries.BLOCK[key] - check(block !== Blocks.AIR) { "Unknown vanilla block: $key" } - tag += block - consumer.accept(block) - } - } - - /** - * For each [DyeColor], resolves the vanilla block named by substituting `{color}` into - * `pattern` and adds it directly to [tag] (unlike [addColored], all colors share one tag). - */ - private fun addColoredFlat(tag: TagKey, pattern: String, consumer: Consumer = Consumer {}) - { - for (color in DyeColor.entries) - { - val key = ResourceLocation.fromNamespaceAndPath("minecraft", pattern.replace("{color}", color.getName().lowercase())) - val block = BuiltInRegistries.BLOCK[key] - check(block !== Blocks.AIR) { "Unknown vanilla block: $key" } - tag += block - consumer.accept(block) - } - } - - /** Looks up a common ("c") block tag by [name], throwing if it isn't declared in [ACommonTags.Blocks]. */ - private fun getCommonTag(name: String): TagKey - { - return ACommonTags.Blocks[ResourceLocation.fromNamespaceAndPath("c", name)] - ?: throw IllegalStateException(ACommonTags.Blocks::class.java.name + " is missing tag name: " + name) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt b/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt deleted file mode 100644 index 8b492d8bd..000000000 --- a/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.kernelpanicsoft.archie.data.internal.common.tags - -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags -import net.minecraft.core.HolderLookup -import net.minecraft.data.PackOutput -import net.minecraft.world.entity.EntityType -import java.util.concurrent.CompletableFuture - -/** - * Populates Archie's vanilla-derived common ("c") entity type tags (see [ACommonTags.EntityTypes]) - * with their vanilla entity type members, so downstream mods can depend on the `c` tag convention - * without every mod having to redeclare it. - */ -class AInternalEntityTypeTagsProvider( - output: PackOutput, - registriesFuture: CompletableFuture -) : ATagsProvider.EntityTypeTagsProvider(output, Archie.MOD, registriesFuture, false) -{ - override fun generate(registries: HolderLookup.Provider) - { - ACommonTags.EntityTypes.BOSSES += listOf( - EntityType.ENDER_DRAGON, - EntityType.WITHER - ) - ACommonTags.EntityTypes.MINECARTS += listOf( - EntityType.MINECART, - EntityType.CHEST_MINECART, - EntityType.FURNACE_MINECART, - EntityType.HOPPER_MINECART, - EntityType.SPAWNER_MINECART, - EntityType.TNT_MINECART, - EntityType.COMMAND_BLOCK_MINECART - ) - ACommonTags.EntityTypes.BOATS += listOf( - EntityType.BOAT, - EntityType.CHEST_BOAT - ) - ACommonTags.EntityTypes.CAPTURING_NOT_SUPPORTED() - ACommonTags.EntityTypes.TELEPORTING_NOT_SUPPORTED() - } -} \ No newline at end of file diff --git a/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt b/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt deleted file mode 100644 index 7dd19cfef..000000000 --- a/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt +++ /dev/null @@ -1,46 +0,0 @@ -package net.kernelpanicsoft.archie.data.internal.common.tags - -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags -import net.minecraft.core.HolderLookup -import net.minecraft.data.PackOutput -import net.minecraft.world.level.material.Fluids -import java.util.concurrent.CompletableFuture - -/** - * Populates Archie's vanilla-derived common ("c") fluid tags (see [ACommonTags.Fluids]) with - * their vanilla fluid members, so downstream mods can depend on the `c` tag convention without - * every mod having to redeclare it. - */ -class AInternalFluidTagsProvider(output: PackOutput, registriesFuture: CompletableFuture) : - ATagsProvider.FluidTagsProvider( - output, Archie.MOD, - registriesFuture, - false - ) -{ - override fun generate(registries: HolderLookup.Provider) - { - ACommonTags.Fluids.WATER += listOf( - Fluids.WATER, - Fluids.FLOWING_WATER - ) - ACommonTags.Fluids.LAVA += listOf( - Fluids.LAVA, - Fluids.FLOWING_LAVA - ) - ACommonTags.Fluids.MILK *= listOf( - mcLoc("milk"), - mcLoc("flowing_milk") - ) - ACommonTags.Fluids.GASEOUS() - ACommonTags.Fluids.HONEY() - ACommonTags.Fluids.POTION() - ACommonTags.Fluids.SUSPICIOUS_STEW() - ACommonTags.Fluids.MUSHROOM_STEW() - ACommonTags.Fluids.RABBIT_STEW() - ACommonTags.Fluids.BEETROOT_SOUP() - ACommonTags.Fluids.HIDDEN_FROM_RECIPE_VIEWERS() - } -} \ No newline at end of file diff --git a/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt b/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt deleted file mode 100644 index 00ed7dfb7..000000000 --- a/Archie/common/src/main/datagen/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt +++ /dev/null @@ -1,654 +0,0 @@ -package net.kernelpanicsoft.archie.data.internal.common.tags - -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags -import dev.architectury.platform.Platform -import net.minecraft.core.HolderLookup -import net.minecraft.core.registries.BuiltInRegistries -import net.minecraft.data.PackOutput -import net.minecraft.resources.ResourceLocation -import net.minecraft.tags.ItemTags -import net.minecraft.tags.TagKey -import net.minecraft.world.item.DyeColor -import net.minecraft.world.item.Item -import net.minecraft.world.item.Items -import java.util.concurrent.CompletableFuture -import java.util.function.Consumer - -/** - * Populates Archie's vanilla-derived common ("c") item tags (see [ACommonTags.Items]) with - * their vanilla item members, so downstream mods can depend on the `c` tag convention without - * every mod having to redeclare it. - */ -class AInternalItemTagsProvider( - output: PackOutput, - lookupProvider: CompletableFuture, - blockTagsProvider: BlockTagsProvider -) : ATagsProvider.ItemTagsProvider(output, Archie.MOD, lookupProvider, blockTagsProvider, false) -{ - override fun generate(registries: HolderLookup.Provider) - { - if (Platform.isNeoForge()) - { - ACommonTags.Items.ENCHANTING_FUELS += ACommonTags.Items.GEMS_LAPIS - } - copy(ACommonTags.Blocks.BARRELS, ACommonTags.Items.BARRELS) - copy( - ACommonTags.Blocks.BARRELS_WOODEN, - ACommonTags.Items.BARRELS_WOODEN - ) - ACommonTags.Items.BONES += Items.BONE - copy( - ACommonTags.Blocks.BOOKSHELVES, - ACommonTags.Items.BOOKSHELVES - ) - ACommonTags.Items.BRICKS += listOf( - ACommonTags.Items.BRICKS_NORMAL, - ACommonTags.Items.BRICKS_NETHER - ) - ACommonTags.Items.BRICKS_NORMAL += Items.BRICK - ACommonTags.Items.BRICKS_NETHER += Items.NETHER_BRICK - ACommonTags.Items.BUCKETS_EMPTY += Items.BUCKET - ACommonTags.Items.BUCKETS_WATER += Items.WATER_BUCKET - ACommonTags.Items.BUCKETS_LAVA += Items.LAVA_BUCKET - ACommonTags.Items.BUCKETS_MILK += Items.MILK_BUCKET - ACommonTags.Items.BUCKETS_POWDER_SNOW += Items.POWDER_SNOW_BUCKET - ACommonTags.Items.BUCKETS_ENTITY_WATER += listOf( - Items.AXOLOTL_BUCKET, - Items.COD_BUCKET, - Items.PUFFERFISH_BUCKET, - Items.TADPOLE_BUCKET, - Items.TROPICAL_FISH_BUCKET, - Items.SALMON_BUCKET - ) - ACommonTags.Items.BUCKETS += listOf( - ACommonTags.Items.BUCKETS_EMPTY, - ACommonTags.Items.BUCKETS_WATER, - ACommonTags.Items.BUCKETS_LAVA, - ACommonTags.Items.BUCKETS_MILK, - ACommonTags.Items.BUCKETS_POWDER_SNOW, - ACommonTags.Items.BUCKETS_ENTITY_WATER - ) - copy( - ACommonTags.Blocks.BUDDING_BLOCKS, - ACommonTags.Items.BUDDING_BLOCKS - ) - copy(ACommonTags.Blocks.BUDS, ACommonTags.Items.BUDS) - copy(ACommonTags.Blocks.CHAINS, ACommonTags.Items.CHAINS) - copy(ACommonTags.Blocks.CHESTS, ACommonTags.Items.CHESTS) - copy( - ACommonTags.Blocks.CHESTS_ENDER, - ACommonTags.Items.CHESTS_ENDER - ) - copy( - ACommonTags.Blocks.CHESTS_TRAPPED, - ACommonTags.Items.CHESTS_TRAPPED - ) - copy( - ACommonTags.Blocks.CHESTS_WOODEN, - ACommonTags.Items.CHESTS_WOODEN - ) - copy(ACommonTags.Blocks.CLUSTERS, ACommonTags.Items.CLUSTERS) - copy( - ACommonTags.Blocks.COBBLESTONES, - ACommonTags.Items.COBBLESTONES - ) - copy( - ACommonTags.Blocks.COBBLESTONES_NORMAL, - ACommonTags.Items.COBBLESTONES_NORMAL - ) - copy( - ACommonTags.Blocks.COBBLESTONES_INFESTED, - ACommonTags.Items.COBBLESTONES_INFESTED - ) - copy( - ACommonTags.Blocks.COBBLESTONES_MOSSY, - ACommonTags.Items.COBBLESTONES_MOSSY - ) - copy( - ACommonTags.Blocks.COBBLESTONES_DEEPSLATE, - ACommonTags.Items.COBBLESTONES_DEEPSLATE - ) - ACommonTags.Items.CROPS += listOf( - ACommonTags.Items.CROPS_BEETROOT, - ACommonTags.Items.CROPS_CARROT, - ACommonTags.Items.CROPS_NETHER_WART, - ACommonTags.Items.CROPS_POTATO, - ACommonTags.Items.CROPS_WHEAT - ) - ACommonTags.Items.CROPS_BEETROOT += Items.BEETROOT - ACommonTags.Items.CROPS_CARROT += Items.CARROT - ACommonTags.Items.CROPS_NETHER_WART += Items.NETHER_WART - ACommonTags.Items.CROPS_POTATO += Items.POTATO - ACommonTags.Items.CROPS_WHEAT += Items.WHEAT - addColored(ACommonTags.Items.DYED, "{color}_banner") - addColored(ACommonTags.Items.DYED, "{color}_bed") - addColored(ACommonTags.Items.DYED, "{color}_candle") - addColored(ACommonTags.Items.DYED, "{color}_carpet") - addColored(ACommonTags.Items.DYED, "{color}_concrete") - addColored(ACommonTags.Items.DYED, "{color}_concrete_powder") - addColored(ACommonTags.Items.DYED, "{color}_glazed_terracotta") - addColored(ACommonTags.Items.DYED, "{color}_shulker_box") - addColored(ACommonTags.Items.DYED, "{color}_stained_glass") - addColored(ACommonTags.Items.DYED, "{color}_stained_glass_pane") - addColored(ACommonTags.Items.DYED, "{color}_terracotta") - addColored(ACommonTags.Items.DYED, "{color}_wool") - addColoredTags(ACommonTags.Items.DYED) { values: TagKey -> - ACommonTags.Items.DYED += values - } - ACommonTags.Items.DUSTS += listOf( - ACommonTags.Items.DUSTS_GLOWSTONE, - ACommonTags.Items.DUSTS_REDSTONE, - ACommonTags.Items.DUSTS_PRISMARINE - ) - ACommonTags.Items.DUSTS_GLOWSTONE += Items.GLOWSTONE_DUST - ACommonTags.Items.DUSTS_REDSTONE += Items.REDSTONE - ACommonTags.Items.DUSTS_PRISMARINE += Items.PRISMARINE_SHARD - addColored(ACommonTags.Items.DYES, "{color}_dye") - addColoredTags(ACommonTags.Items.DYES) { values: TagKey -> - ACommonTags.Items.DYES += listOf(values) - } - ACommonTags.Items.EGGS += Items.EGG - copy(ACommonTags.Blocks.END_STONES, ACommonTags.Items.END_STONES) - ACommonTags.Items.ENDER_PEARLS += Items.ENDER_PEARL - ACommonTags.Items.FEATHERS += Items.FEATHER - copy( - ACommonTags.Blocks.FENCE_GATES, - ACommonTags.Items.FENCE_GATES - ) - copy( - ACommonTags.Blocks.FENCE_GATES_WOODEN, - ACommonTags.Items.FENCE_GATES_WOODEN - ) - copy(ACommonTags.Blocks.FENCES, ACommonTags.Items.FENCES) - copy( - ACommonTags.Blocks.FENCES_NETHER_BRICK, - ACommonTags.Items.FENCES_NETHER_BRICK - ) - copy( - ACommonTags.Blocks.FENCES_WOODEN, - ACommonTags.Items.FENCES_WOODEN - ) - ACommonTags.Items.FOODS_FRUITS += listOf( - Items.APPLE, - Items.GOLDEN_APPLE, - Items.ENCHANTED_GOLDEN_APPLE - ) - ACommonTags.Items.FOODS_VEGETABLES += listOf( - Items.CARROT, - Items.GOLDEN_CARROT, - Items.POTATO, - Items.MELON_SLICE, - Items.BEETROOT - ) - ACommonTags.Items.FOODS_BERRIES += listOf( - Items.SWEET_BERRIES, - Items.GLOW_BERRIES - ) - ACommonTags.Items.FOODS_BREADS += Items.BREAD - ACommonTags.Items.FOODS_COOKIES += Items.COOKIE - ACommonTags.Items.FOODS_RAW_MEATS += listOf( - Items.BEEF, - Items.PORKCHOP, - Items.CHICKEN, - Items.RABBIT, - Items.MUTTON - ) - ACommonTags.Items.FOODS_RAW_FISHES += listOf( - Items.COD, - Items.SALMON, - Items.TROPICAL_FISH, - Items.PUFFERFISH - ) - ACommonTags.Items.FOODS_COOKED_MEATS += listOf( - Items.COOKED_BEEF, - Items.COOKED_PORKCHOP, - Items.COOKED_CHICKEN, - Items.COOKED_RABBIT, - Items.COOKED_MUTTON - ) - ACommonTags.Items.FOODS_COOKED_FISHES += listOf( - Items.COOKED_COD, - Items.COOKED_SALMON - ) - ACommonTags.Items.FOODS_SOUPS += listOf( - Items.BEETROOT_SOUP, - Items.MUSHROOM_STEW, - Items.RABBIT_STEW, - Items.SUSPICIOUS_STEW - ) - ACommonTags.Items.FOODS_CANDIES() - ACommonTags.Items.FOODS_EDIBLE_WHEN_PLACED += Items.CAKE - ACommonTags.Items.FOODS_FOOD_POISONING += listOf( - Items.POISONOUS_POTATO, - Items.PUFFERFISH, - Items.SPIDER_EYE, - Items.CHICKEN, - Items.ROTTEN_FLESH - ) - ACommonTags.Items.FOODS { - add( - Items.BAKED_POTATO, - Items.PUMPKIN_PIE, - Items.HONEY_BOTTLE, - Items.OMINOUS_BOTTLE, - Items.DRIED_KELP - ) - addTags( - ACommonTags.Items.FOODS_FRUITS, - ACommonTags.Items.FOODS_VEGETABLES, - ACommonTags.Items.FOODS_BERRIES, - ACommonTags.Items.FOODS_BREADS, - ACommonTags.Items.FOODS_COOKIES, - ACommonTags.Items.FOODS_RAW_MEATS, - ACommonTags.Items.FOODS_RAW_FISHES, - ACommonTags.Items.FOODS_COOKED_MEATS, - ACommonTags.Items.FOODS_COOKED_FISHES, - ACommonTags.Items.FOODS_SOUPS, - ACommonTags.Items.FOODS_CANDIES, - ACommonTags.Items.FOODS_EDIBLE_WHEN_PLACED, - ACommonTags.Items.FOODS_FOOD_POISONING - ) - } - ACommonTags.Items.GEMS += listOf( - ACommonTags.Items.GEMS_AMETHYST, - ACommonTags.Items.GEMS_DIAMOND, - ACommonTags.Items.GEMS_EMERALD, - ACommonTags.Items.GEMS_LAPIS, - ACommonTags.Items.GEMS_PRISMARINE, - ACommonTags.Items.GEMS_QUARTZ - ) - ACommonTags.Items.GEMS_AMETHYST += Items.AMETHYST_SHARD - ACommonTags.Items.GEMS_DIAMOND += Items.DIAMOND - ACommonTags.Items.GEMS_EMERALD += Items.EMERALD - ACommonTags.Items.GEMS_LAPIS += Items.LAPIS_LAZULI - ACommonTags.Items.GEMS_PRISMARINE += Items.PRISMARINE_CRYSTALS - ACommonTags.Items.GEMS_QUARTZ += Items.QUARTZ - copy( - ACommonTags.Blocks.GLASS_BLOCKS, - ACommonTags.Items.GLASS_BLOCKS - ) - copy( - ACommonTags.Blocks.GLASS_BLOCKS_COLORLESS, - ACommonTags.Items.GLASS_BLOCKS_COLORLESS - ) - copy( - ACommonTags.Blocks.GLASS_BLOCKS_TINTED, - ACommonTags.Items.GLASS_BLOCKS_TINTED - ) - copy( - ACommonTags.Blocks.GLASS_BLOCKS_CHEAP, - ACommonTags.Items.GLASS_BLOCKS_CHEAP - ) - copy( - ACommonTags.Blocks.GLASS_BLOCKS_STAINED, - ACommonTags.Items.GLASS_BLOCKS_STAINED - ) - copy( - ACommonTags.Blocks.GLASS_PANES, - ACommonTags.Items.GLASS_PANES - ) - copy( - ACommonTags.Blocks.GLASS_PANES_COLORLESS, - ACommonTags.Items.GLASS_PANES_COLORLESS - ) - copy( - ACommonTags.Blocks.GLASS_PANES_STAINED, - ACommonTags.Items.GLASS_PANES_STAINED - ) - copy(ACommonTags.Blocks.GRAVELS, ACommonTags.Items.GRAVELS) - ACommonTags.Items.GUNPOWDERS += Items.GUNPOWDER - ACommonTags.Items.HIDDEN_FROM_RECIPE_VIEWERS() - ACommonTags.Items.INGOTS += listOf( - ACommonTags.Items.INGOTS_COPPER, - ACommonTags.Items.INGOTS_GOLD, - ACommonTags.Items.INGOTS_IRON, - ACommonTags.Items.INGOTS_NETHERITE - ) - ACommonTags.Items.INGOTS_COPPER += Items.COPPER_INGOT - ACommonTags.Items.INGOTS_GOLD += Items.GOLD_INGOT - ACommonTags.Items.INGOTS_IRON += Items.IRON_INGOT - ACommonTags.Items.INGOTS_NETHERITE += Items.NETHERITE_INGOT - ACommonTags.Items.LEATHERS += Items.LEATHER - ACommonTags.Items.MUSHROOMS += listOf( - Items.BROWN_MUSHROOM, - Items.RED_MUSHROOM - ) - ACommonTags.Items.NETHER_STARS += Items.NETHER_STAR - copy( - ACommonTags.Blocks.NETHERRACKS, - ACommonTags.Items.NETHERRACKS - ) - ACommonTags.Items.NUGGETS += listOf( - ACommonTags.Items.NUGGETS_GOLD, - ACommonTags.Items.NUGGETS_IRON - ) - ACommonTags.Items.NUGGETS_IRON += Items.IRON_NUGGET - ACommonTags.Items.NUGGETS_GOLD += Items.GOLD_NUGGET - copy(ACommonTags.Blocks.OBSIDIANS, ACommonTags.Items.OBSIDIANS) - copy( - ACommonTags.Blocks.ORE_BEARING_GROUND_DEEPSLATE, - ACommonTags.Items.ORE_BEARING_GROUND_DEEPSLATE - ) - copy( - ACommonTags.Blocks.ORE_BEARING_GROUND_NETHERRACK, - ACommonTags.Items.ORE_BEARING_GROUND_NETHERRACK - ) - copy( - ACommonTags.Blocks.ORE_BEARING_GROUND_STONE, - ACommonTags.Items.ORE_BEARING_GROUND_STONE - ) - copy( - ACommonTags.Blocks.ORE_RATES_DENSE, - ACommonTags.Items.ORE_RATES_DENSE - ) - copy( - ACommonTags.Blocks.ORE_RATES_SINGULAR, - ACommonTags.Items.ORE_RATES_SINGULAR - ) - copy( - ACommonTags.Blocks.ORE_RATES_SPARSE, - ACommonTags.Items.ORE_RATES_SPARSE - ) - copy(ACommonTags.Blocks.ORES, ACommonTags.Items.ORES) - copy(ACommonTags.Blocks.ORES_COAL, ACommonTags.Items.ORES_COAL) - copy( - ACommonTags.Blocks.ORES_COPPER, - ACommonTags.Items.ORES_COPPER - ) - copy( - ACommonTags.Blocks.ORES_DIAMOND, - ACommonTags.Items.ORES_DIAMOND - ) - copy( - ACommonTags.Blocks.ORES_EMERALD, - ACommonTags.Items.ORES_EMERALD - ) - copy(ACommonTags.Blocks.ORES_GOLD, ACommonTags.Items.ORES_GOLD) - copy(ACommonTags.Blocks.ORES_IRON, ACommonTags.Items.ORES_IRON) - copy(ACommonTags.Blocks.ORES_LAPIS, ACommonTags.Items.ORES_LAPIS) - copy( - ACommonTags.Blocks.ORES_QUARTZ, - ACommonTags.Items.ORES_QUARTZ - ) - copy( - ACommonTags.Blocks.ORES_REDSTONE, - ACommonTags.Items.ORES_REDSTONE - ) - copy( - ACommonTags.Blocks.ORES_NETHERITE_SCRAP, - ACommonTags.Items.ORES_NETHERITE_SCRAP - ) - copy( - ACommonTags.Blocks.ORES_IN_GROUND_DEEPSLATE, - ACommonTags.Items.ORES_IN_GROUND_DEEPSLATE - ) - copy( - ACommonTags.Blocks.ORES_IN_GROUND_NETHERRACK, - ACommonTags.Items.ORES_IN_GROUND_NETHERRACK - ) - copy( - ACommonTags.Blocks.ORES_IN_GROUND_STONE, - ACommonTags.Items.ORES_IN_GROUND_STONE - ) - copy( - ACommonTags.Blocks.PLAYER_WORKSTATIONS_CRAFTING_TABLES, - ACommonTags.Items.PLAYER_WORKSTATIONS_CRAFTING_TABLES - ) - copy( - ACommonTags.Blocks.PLAYER_WORKSTATIONS_FURNACES, - ACommonTags.Items.PLAYER_WORKSTATIONS_FURNACES - ) - ACommonTags.Items.RAW_BLOCKS += listOf( - ACommonTags.Items.RAW_BLOCKS_COPPER, - ACommonTags.Items.RAW_BLOCKS_GOLD, - ACommonTags.Items.RAW_BLOCKS_IRON - ) - ACommonTags.Items.RAW_BLOCKS_COPPER += Items.RAW_COPPER_BLOCK - ACommonTags.Items.RAW_BLOCKS_GOLD += Items.RAW_GOLD_BLOCK - ACommonTags.Items.RAW_BLOCKS_IRON += Items.RAW_IRON_BLOCK - ACommonTags.Items.RAW_MATERIALS += listOf( - ACommonTags.Items.RAW_MATERIALS_COPPER, - ACommonTags.Items.RAW_MATERIALS_GOLD, - ACommonTags.Items.RAW_MATERIALS_IRON - ) - ACommonTags.Items.RAW_MATERIALS_COPPER += Items.RAW_COPPER - ACommonTags.Items.RAW_MATERIALS_GOLD += Items.RAW_GOLD - ACommonTags.Items.RAW_MATERIALS_IRON += Items.RAW_IRON - ACommonTags.Items.RODS += listOf( - ACommonTags.Items.RODS_WOODEN, - ACommonTags.Items.RODS_BLAZE, - ACommonTags.Items.RODS_BREEZE - ) - ACommonTags.Items.RODS_BLAZE += Items.BLAZE_ROD - ACommonTags.Items.RODS_BREEZE += Items.BREEZE_ROD - ACommonTags.Items.RODS_WOODEN += Items.STICK - copy(ACommonTags.Blocks.ROPES, ACommonTags.Items.ROPES) - copy(ACommonTags.Blocks.SANDS, ACommonTags.Items.SANDS) - copy( - ACommonTags.Blocks.SANDS_COLORLESS, - ACommonTags.Items.SANDS_COLORLESS - ) - copy(ACommonTags.Blocks.SANDS_RED, ACommonTags.Items.SANDS_RED) - copy( - ACommonTags.Blocks.SANDSTONE_BLOCKS, - ACommonTags.Items.SANDSTONE_BLOCKS - ) - copy( - ACommonTags.Blocks.SANDSTONE_SLABS, - ACommonTags.Items.SANDSTONE_SLABS - ) - copy( - ACommonTags.Blocks.SANDSTONE_STAIRS, - ACommonTags.Items.SANDSTONE_STAIRS - ) - copy( - ACommonTags.Blocks.SANDSTONE_RED_BLOCKS, - ACommonTags.Items.SANDSTONE_RED_BLOCKS - ) - copy( - ACommonTags.Blocks.SANDSTONE_RED_SLABS, - ACommonTags.Items.SANDSTONE_RED_SLABS - ) - copy( - ACommonTags.Blocks.SANDSTONE_RED_STAIRS, - ACommonTags.Items.SANDSTONE_RED_STAIRS - ) - copy( - ACommonTags.Blocks.SANDSTONE_UNCOLORED_BLOCKS, - ACommonTags.Items.SANDSTONE_UNCOLORED_BLOCKS - ) - copy( - ACommonTags.Blocks.SANDSTONE_UNCOLORED_SLABS, - ACommonTags.Items.SANDSTONE_UNCOLORED_SLABS - ) - copy( - ACommonTags.Blocks.SANDSTONE_UNCOLORED_STAIRS, - ACommonTags.Items.SANDSTONE_UNCOLORED_STAIRS - ) - ACommonTags.Items.SEEDS += listOf( - ACommonTags.Items.SEEDS_BEETROOT, - ACommonTags.Items.SEEDS_MELON, - ACommonTags.Items.SEEDS_PUMPKIN, - ACommonTags.Items.SEEDS_WHEAT - ) - ACommonTags.Items.SEEDS_BEETROOT += Items.BEETROOT_SEEDS - ACommonTags.Items.SEEDS_MELON += Items.MELON_SEEDS - ACommonTags.Items.SEEDS_PUMPKIN += Items.PUMPKIN_SEEDS - ACommonTags.Items.SEEDS_WHEAT += Items.WHEAT_SEEDS - copy( - ACommonTags.Blocks.SHULKER_BOXES, - ACommonTags.Items.SHULKER_BOXES - ) - ACommonTags.Items.SLIMEBALLS += Items.SLIME_BALL - copy(ACommonTags.Blocks.STONES, ACommonTags.Items.STONES) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS, - ACommonTags.Items.STORAGE_BLOCKS - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_AMETHYST, - ACommonTags.Items.STORAGE_BLOCKS_AMETHYST - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_BONE_MEAL, - ACommonTags.Items.STORAGE_BLOCKS_BONE_MEAL - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_COAL, - ACommonTags.Items.STORAGE_BLOCKS_COAL - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_COPPER, - ACommonTags.Items.STORAGE_BLOCKS_COPPER - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_DIAMOND, - ACommonTags.Items.STORAGE_BLOCKS_DIAMOND - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_DRIED_KELP, - ACommonTags.Items.STORAGE_BLOCKS_DRIED_KELP - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_EMERALD, - ACommonTags.Items.STORAGE_BLOCKS_EMERALD - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_GOLD, - ACommonTags.Items.STORAGE_BLOCKS_GOLD - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_IRON, - ACommonTags.Items.STORAGE_BLOCKS_IRON - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_LAPIS, - ACommonTags.Items.STORAGE_BLOCKS_LAPIS - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_NETHERITE, - ACommonTags.Items.STORAGE_BLOCKS_NETHERITE - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_QUARTZ, - ACommonTags.Items.STORAGE_BLOCKS_QUARTZ - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_RAW_COPPER, - ACommonTags.Items.STORAGE_BLOCKS_RAW_COPPER - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_RAW_GOLD, - ACommonTags.Items.STORAGE_BLOCKS_RAW_GOLD - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_RAW_IRON, - ACommonTags.Items.STORAGE_BLOCKS_RAW_IRON - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_REDSTONE, - ACommonTags.Items.STORAGE_BLOCKS_REDSTONE - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_SLIME, - ACommonTags.Items.STORAGE_BLOCKS_SLIME - ) - copy( - ACommonTags.Blocks.STORAGE_BLOCKS_WHEAT, - ACommonTags.Items.STORAGE_BLOCKS_WHEAT - ) - ACommonTags.Items.STRINGS += Items.STRING - ACommonTags.Items.VILLAGER_JOB_SITES += listOf( - Items.BARREL, Items.BLAST_FURNACE, Items.BREWING_STAND, Items.CARTOGRAPHY_TABLE, - Items.CAULDRON, Items.COMPOSTER, Items.FLETCHING_TABLE, Items.GRINDSTONE, - Items.LECTERN, Items.LOOM, Items.SMITHING_TABLE, Items.SMOKER, Items.STONECUTTER - ) - - - // Tools and Armors - ACommonTags.Items.TOOLS_SHIELDS += Items.SHIELD - ACommonTags.Items.TOOLS_BOWS += Items.BOW - ACommonTags.Items.TOOLS_BRUSHES += Items.BRUSH - ACommonTags.Items.TOOLS_CROSSBOWS += Items.CROSSBOW - ACommonTags.Items.TOOLS_FISHING_RODS += Items.FISHING_ROD - ACommonTags.Items.TOOLS_SHEARS += Items.SHEARS - ACommonTags.Items.TOOLS_SPEARS += Items.TRIDENT - ACommonTags.Items.TOOLS += listOf( - ACommonTags.Items.TOOLS_AXES, - ACommonTags.Items.TOOLS_HOES, - ACommonTags.Items.TOOLS_PICKAXES, - ACommonTags.Items.TOOLS_SHOVELS, - ACommonTags.Items.TOOLS_SWORDS, - - ACommonTags.Items.TOOLS_BOWS, - ACommonTags.Items.TOOLS_BRUSHES, - ACommonTags.Items.TOOLS_CROSSBOWS, - ACommonTags.Items.TOOLS_FISHING_RODS, - ACommonTags.Items.TOOLS_SHEARS, - ACommonTags.Items.TOOLS_SHIELDS, - ACommonTags.Items.TOOLS_SPEARS - ) - ACommonTags.Items.ARMORS += listOf( - ACommonTags.Items.ARMORS_HELMETS, - ACommonTags.Items.ARMORS_CHESTPLATES, - ACommonTags.Items.ARMORS_LEGGINGS, - ACommonTags.Items.ARMORS_BOOTS - ) - ACommonTags.Items.ENCHANTABLES += listOf( - ItemTags.ARMOR_ENCHANTABLE, - ItemTags.EQUIPPABLE_ENCHANTABLE, - ItemTags.WEAPON_ENCHANTABLE, - ItemTags.SWORD_ENCHANTABLE, - ItemTags.MINING_ENCHANTABLE, - ItemTags.MINING_LOOT_ENCHANTABLE, - ItemTags.FISHING_ENCHANTABLE, - ItemTags.TRIDENT_ENCHANTABLE, - ItemTags.BOW_ENCHANTABLE, - ItemTags.CROSSBOW_ENCHANTABLE, - ItemTags.FIRE_ASPECT_ENCHANTABLE, - ItemTags.DURABILITY_ENCHANTABLE - ) - - ACommonTags.Items.ENCHANTABLES *= ItemTags.MACE_ENCHANTABLE - - - } - - /** - * For each [DyeColor], resolves the vanilla item named by substituting `{color}` into - * `pattern` and adds it to the per-color common tag `c:{group path}/{color}` (via [getCommonItemTag]). - */ - private fun addColored(group: TagKey, pattern: String) - { - val prefix = group.location().path.lowercase() + '/' - for (color in DyeColor.entries) - { - val key = ResourceLocation.fromNamespaceAndPath("minecraft", pattern.replace("{color}", color.getName())) - val tag = getCommonItemTag(prefix + color.getName()) - val item = BuiltInRegistries.ITEM[key] - check(item !== Items.AIR) { "Unknown vanilla item: $key" } - tag += item - } - } - - /** Passes each of [group]'s per-color common tags (`c:{group path}/{color}`) to [consumer]. */ - private fun addColoredTags(group: TagKey, consumer: Consumer>) - { - val prefix = group.location().path.lowercase() + '/' - for (color in DyeColor.entries) - { - val tag = getCommonItemTag(prefix + color.getName()) - consumer.accept(tag) - } - } - - /** Looks up a common ("c") item tag by [name], throwing if it isn't declared in [ACommonTags.Items]. */ - private fun getCommonItemTag(name: String): TagKey - { - return ACommonTags.Items[ResourceLocation.fromNamespaceAndPath("c", name)] - ?: throw IllegalStateException(ACommonTags.Items::class.java.name + " is missing tag name: " + name) - } - -} \ No newline at end of file diff --git a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt deleted file mode 100644 index 27b1be2ab..000000000 --- a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt +++ /dev/null @@ -1,49 +0,0 @@ -package net.kernelpanicsoft.archie.gametest.internal - -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.events.AEvents -import net.kernelpanicsoft.archie.gametest.AGameTestEventObject -import net.kernelpanicsoft.archie.gametest.internal.tests.ArchieItemHandlerTests -import net.kernelpanicsoft.archie.gametest.internal.tests.BlockEntityNBTHolderTests -import net.kernelpanicsoft.archie.gametest.internal.tests.BlockEntityStateManagerTests -import net.kernelpanicsoft.archie.gametest.internal.tests.ComposeRenderingTests -import net.kernelpanicsoft.archie.gametest.internal.tests.InputComponentsGameTest -import net.kernelpanicsoft.archie.gametest.internal.tests.LayoutComponentsGameTest -import net.kernelpanicsoft.archie.gametest.internal.tests.ModalComponentsGameTest - -/** - * ID of the empty structure template used by every GameTest in this suite; GameTests that don't - * need a specific structure should reference this via `@GameTest(template = EMPTY)`. - */ -const val EMPTY = "archie:gametest/empty" - -/** - * Registers Archie's own internal GameTest suite (the tests under [net.kernelpanicsoft.archie.gametest.internal.tests]) - * against the given builder, scoped by the environment each suite needs to run in. - */ -internal fun AEvents.ArchieGameTestBuilder.archieGameTests() -{ - common { - - } - client { - register() - register() - register() - register() - } - server { - register() - register() - register() - } -} - -/** - * Registration entry point for Archie's internal GameTest suite, hooked into [AEvents]'s - * GameTest registration handler for [Archie.MOD]. - */ -internal object ArchieGameTest : AGameTestEventObject(Archie.MOD) -{ - override fun AEvents.ArchieGameTestBuilder.handler() = archieGameTests() -} \ No newline at end of file diff --git a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt deleted file mode 100644 index 8bf0ca92a..000000000 --- a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt +++ /dev/null @@ -1,77 +0,0 @@ -package net.kernelpanicsoft.archie.gametest.internal.tests - -import earth.terrarium.common_storage_lib.resources.item.ItemResource -import net.kernelpanicsoft.archie.gametest.assertEquals -import net.kernelpanicsoft.archie.gametest.assertTrue -import net.kernelpanicsoft.archie.gametest.internal.EMPTY -import net.kernelpanicsoft.archie.transfer.ArchieItemStorage -import net.minecraft.gametest.framework.GameTest -import net.minecraft.gametest.framework.GameTestHelper -import net.minecraft.world.item.ItemStack -import net.minecraft.world.item.Items - -/** - * GameTest coverage for [ArchieItemStorage]: max-stack-size clamping on insert, resource - * clearing on full extraction, and that simulated insert/extract calls never mutate storage. - */ -@Suppress("unused") -class ArchieItemHandlerTests -{ - @GameTest(template = EMPTY) - fun GameTestHelper.testInsertRespectsMaxStackSize() - { - val storage = ArchieItemStorage(1) - val stone = ItemResource.of(ItemStack(Items.STONE, 1)) - - val inserted = storage.insert(stone, 80, false) - - assertEquals(64L, inserted) - assertEquals(64, storage.get(0).getItem().count) - succeed() - } - - @GameTest(template = EMPTY) - fun GameTestHelper.testExtractToZeroClearsResource() - { - val storage = ArchieItemStorage(1) - val stone = ItemResource.of(ItemStack(Items.STONE, 1)) - - storage.insert(stone, 10, false) - val extracted = storage.extract(stone, 10, false) - - assertEquals(10L, extracted) - assertTrue(storage.get(0).getItem().isEmpty) { - "Expected slot to be empty after full extraction" - } - succeed() - } - - @GameTest(template = EMPTY) - fun GameTestHelper.testSimulatedInsertDoesNotMutateStorage() - { - val storage = ArchieItemStorage(1) - val diamond = ItemResource.of(ItemStack(Items.DIAMOND, 1)) - - val inserted = storage.insert(diamond, 16, true) - - assertEquals(16L, inserted) - assertTrue(storage.get(0).getItem().isEmpty) { - "Simulated insert should not mutate slot contents" - } - succeed() - } - - @GameTest(template = EMPTY) - fun GameTestHelper.testSimulatedExtractDoesNotMutateStorage() - { - val storage = ArchieItemStorage(1) - val iron = ItemResource.of(ItemStack(Items.IRON_INGOT, 1)) - storage.insert(iron, 7, false) - - val extracted = storage.extract(iron, 4, true) - - assertEquals(4L, extracted) - assertEquals(7, storage.get(0).getItem().count) - succeed() - } -} diff --git a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt deleted file mode 100644 index 20252e262..000000000 --- a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt +++ /dev/null @@ -1,127 +0,0 @@ -package net.kernelpanicsoft.archie.gametest.internal.tests - -import dev.architectury.fluid.FluidStack -import net.kernelpanicsoft.archie.gametest.assertEquals -import net.kernelpanicsoft.archie.gametest.assertTrue -import net.kernelpanicsoft.archie.gametest.internal.EMPTY -import net.kernelpanicsoft.archie.serialization.NBTHolder -import net.kernelpanicsoft.archie.serialization.Sync -import net.kernelpanicsoft.archie.serialization.listField -import net.kernelpanicsoft.archie.serialization.mapField -import net.minecraft.gametest.framework.GameTest -import net.minecraft.gametest.framework.GameTestHelper -import net.minecraft.nbt.CompoundTag -import net.minecraft.world.item.ItemStack -import net.minecraft.world.item.Items -import net.minecraft.world.level.material.Fluids - -/** - * GameTest coverage for [NBTHolder]: default values, save/load round-tripping for scalar, - * list, map, item, fluid, and energy delegated fields, and that only [Sync]-annotated fields - * appear in the sync tag. - */ -@Suppress("unused") -class BlockEntityNBTHolderTests -{ - /** Minimal [NBTHolder] with one of each supported field kind, used as a fixture across tests. */ - private class HolderFixture : NBTHolder by NBTHolder.create() - { - var counter by intField { 1 } - var label by stringField { "default" } - val values by listField { listOf(1, 2) } - val weights by mapField { mapOf("a" to 1) } - - @Sync - var syncedCounter by intField { 7 } - } - - /** [NBTHolder] with one of each resource-storage field kind, used only by the test below. */ - private class ResourceFixture : NBTHolder by NBTHolder.create() - { - val items by itemField(1) - val tank by fluidField(FluidStack.bucketAmount() * 2) - val energy by energyField(1_000) - } - - @GameTest(template = EMPTY) - fun GameTestHelper.testFieldDefaultsAndPersistenceRoundTrip() - { - val holder = HolderFixture() - assertEquals(1, holder.counter) - assertEquals("default", holder.label) - - holder.counter = 12 - holder.label = "changed" - - val tag = CompoundTag() - holder.saveToTag(tag) - - val loaded = HolderFixture() - loaded.loadFromTag(tag) - - assertEquals(12, loaded.counter) - assertEquals("changed", loaded.label) - succeed() - } - - @GameTest(template = EMPTY) - fun GameTestHelper.testListAndMapDelegatesPersistMutations() - { - val holder = HolderFixture() - holder.values.add(3) - holder.weights["b"] = 2 - - val tag = CompoundTag() - holder.saveToTag(tag) - - val loaded = HolderFixture() - loaded.loadFromTag(tag) - - assertEquals(listOf(1, 2, 3), loaded.values.toList()) - assertEquals(2, loaded.weights["b"]) - succeed() - } - - @GameTest(template = EMPTY) - fun GameTestHelper.testSyncTagContainsOnlySyncAnnotatedFields() - { - val holder = HolderFixture() - holder.counter = 42 - holder.syncedCounter = 9 - - val syncTag = holder.getSyncTag() - assertTrue("synced_counter" in syncTag) { - "Expected sync tag to include synced field" - } - assertTrue("counter" !in syncTag) { - "Expected sync tag to exclude non-synced field" - } - succeed() - } - - @GameTest(template = EMPTY) - fun GameTestHelper.testItemFluidAndEnergyFieldsPersistMutations() - { - val holder = ResourceFixture() - holder.items[0].set(ItemStack(Items.DIAMOND, 5)) - holder.tank[0].set(FluidStack.create(Fluids.WATER, FluidStack.bucketAmount())) - holder.energy.insert(400, false) - - val tag = CompoundTag() - holder.saveToTag(tag) - - val loaded = ResourceFixture() - loaded.loadFromTag(tag) - - assertEquals(ItemStack(Items.DIAMOND, 5).item, loaded.items[0].getItem().item) - assertEquals(5, loaded.items[0].getItem().count) - assertEquals(FluidStack.bucketAmount(), loaded.tank[0].getFluid().amount) - assertTrue(loaded.tank[0].getFluid().fluid == Fluids.WATER) { - "Expected loaded tank to still hold water" - } - assertEquals(400L, loaded.energy.getStoredAmount()) - assertEquals(1_000L, loaded.energy.getCapacity()) - succeed() - } - -} diff --git a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt deleted file mode 100644 index 42ede1ede..000000000 --- a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt +++ /dev/null @@ -1,86 +0,0 @@ -package net.kernelpanicsoft.archie.gametest.internal.tests - -import kotlinx.serialization.builtins.serializer -import net.kernelpanicsoft.archie.gametest.assertEquals -import net.kernelpanicsoft.archie.gametest.assertTrue -import net.kernelpanicsoft.archie.gametest.internal.EMPTY -import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager -import net.minecraft.core.BlockPos -import net.minecraft.gametest.framework.GameTest -import net.minecraft.gametest.framework.GameTestHelper -import net.minecraft.world.level.block.Blocks -import net.minecraft.world.level.block.entity.ChestBlockEntity - -/** - * GameTest coverage for [BlockEntityStateManager]: container identity on repeated registration, - * that dirty containers stay dirty when there are no tracked players to sync to, and that - * unregistering/clearing correctly drops tracked containers. - */ -@Suppress("unused") -class BlockEntityStateManagerTests -{ - @GameTest(template = EMPTY) - fun GameTestHelper.testRegisterReturnsStableContainer() - { - BlockEntityStateManager.clear() - val blockEntity = ChestBlockEntity(BlockPos(1, 2, 3), Blocks.CHEST.defaultBlockState()) - - val first = BlockEntityStateManager.registerBlockEntity(blockEntity) - val second = BlockEntityStateManager.registerBlockEntity(blockEntity) - - assertTrue(first === second) { "Expected the same container instance for repeated registration" } - assertTrue(BlockEntityStateManager.getContainer(blockEntity) != null) { "Expected container to be retrievable" } - BlockEntityStateManager.clear() - succeed() - } - - @GameTest(template = EMPTY) - fun GameTestHelper.testSyncSkipsWithoutTrackedPlayers() - { - BlockEntityStateManager.clear() - val blockEntity = ChestBlockEntity(BlockPos(2, 2, 3), Blocks.CHEST.defaultBlockState()) - val container = BlockEntityStateManager.registerBlockEntity(blockEntity) - container.setPropertySerializer("energy", Int.serializer()) - container.updateProperty("energy", 99) - - var packetsSent = 0 - BlockEntityStateManager.syncDirtyEntities(40L) { _, _ -> packetsSent++ } - - assertEquals(0, packetsSent) - assertTrue(container.isDirty) { "Container should remain dirty until a packet is sent" } - BlockEntityStateManager.clear() - succeed() - } - - @GameTest(template = EMPTY) - fun GameTestHelper.testUnregisterRemovesContainer() - { - BlockEntityStateManager.clear() - val blockEntity = ChestBlockEntity(BlockPos(3, 2, 3), Blocks.CHEST.defaultBlockState()) - - val container = BlockEntityStateManager.registerBlockEntity(blockEntity) - container.setPropertySerializer("progress", Int.serializer()) - container.updateProperty("progress", 3) - - BlockEntityStateManager.unregisterBlockEntity(blockEntity) - assertEquals(null, BlockEntityStateManager.getContainer(blockEntity)) - BlockEntityStateManager.clear() - succeed() - } - - @GameTest(template = EMPTY) - fun GameTestHelper.testClearRemovesAllTrackedEntities() - { - BlockEntityStateManager.clear() - val first = ChestBlockEntity(BlockPos(4, 2, 3), Blocks.CHEST.defaultBlockState()) - val second = ChestBlockEntity(BlockPos(5, 2, 3), Blocks.CHEST.defaultBlockState()) - - BlockEntityStateManager.registerBlockEntity(first) - BlockEntityStateManager.registerBlockEntity(second) - BlockEntityStateManager.clear() - - assertEquals(null, BlockEntityStateManager.getContainer(first)) - assertEquals(null, BlockEntityStateManager.getContainer(second)) - succeed() - } -} diff --git a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ComposeRenderingTests.kt b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ComposeRenderingTests.kt deleted file mode 100644 index 760248a79..000000000 --- a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ComposeRenderingTests.kt +++ /dev/null @@ -1,102 +0,0 @@ -package net.kernelpanicsoft.archie.gametest.internal.tests - -import androidx.compose.runtime.LaunchedEffect -import net.kernelpanicsoft.archie.gametest.ClientGameTest -import net.kernelpanicsoft.archie.gametest.ClientGameTestContext -import net.kernelpanicsoft.archie.gametest.LayerSelector -import net.kernelpanicsoft.archie.gametest.waitForScreen -import net.kernelpanicsoft.archie.gui.ComposeScreen -import net.kernelpanicsoft.archie.gui.layer.LocalLayerManager -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.MeasurePolicy -import net.kernelpanicsoft.archie.gui.layout.MeasureResult -import net.kernelpanicsoft.archie.gui.modifiers.Constraints -import net.minecraft.network.chat.Component - -private fun fixedSizeMeasurePolicy(width: Int, height: Int): MeasurePolicy = MeasurePolicy { _, _, _ -> - MeasureResult(width, height) { } -} - -/** - * Client GameTest coverage for [ComposeScreen] rendering: that a [Layout] node is measured - * with its policy's reported size, and that a layer pushed via `LocalLayerManager.modal` is - * measured independently on top of the base layer. - */ -@Suppress("unused") -class ComposeRenderingTests { - @ClientGameTest - fun ClientGameTestContext.testComposeScreenMeasuresRenderableNode() { - setScreen { RenderProbeScreen() } - waitForScreen { - waitForLayer(0) { - assertTrue(hasNode(RENDER_PROBE_NAME)) { "Expected render probe node to exist" } - computeOnClient { - rootNode.measure(Constraints(maxWidth = 320, maxHeight = 240)) - } - node(RENDER_PROBE_NAME) { - assertEquals(120, computeOnClient { node.width }) - assertEquals(64, computeOnClient { node.height }) - } - } - } - } - - @ClientGameTest - fun ClientGameTestContext.testComposeScreenPushesModalLayer() { - setScreen { ModalProbeScreen() } - waitForScreen { - waitForLayer(1) { - assertTrue(hasNode(MODAL_PROBE_NAME, LayerSelector.Top)) { "Expected modal probe node to exist" } - computeOnClient { - rootNode.measure(Constraints(maxWidth = 320, maxHeight = 240)) - } - node(MODAL_PROBE_NAME) { - assertEquals(96, computeOnClient { node.width }) - assertEquals(48, computeOnClient { node.height }) - } - } - } - } - - private class RenderProbeScreen : ComposeScreen(Component.literal("Compose Render Probe")) { - override fun init() { - super.init() - start { - Layout( - name = RENDER_PROBE_NAME, - measurePolicy = fixedSizeMeasurePolicy(120, 64), - ) - } - } - } - - private class ModalProbeScreen : ComposeScreen(Component.literal("Compose Modal Probe")) { - override fun init() { - super.init() - start { - Layout( - name = BASE_PROBE_NAME, - measurePolicy = fixedSizeMeasurePolicy(160, 80), - ) - val layerManager = LocalLayerManager.current - LaunchedEffect(Unit) { - layerManager.modal(dismissOnClickOutside = false) { - Layout( - name = MODAL_PROBE_NAME, - measurePolicy = fixedSizeMeasurePolicy(96, 48), - ) - } - } - } - } - } - - companion object { - private const val BASE_PROBE_NAME = "ComposeBaseProbe" - private const val MODAL_PROBE_NAME = "ComposeModalProbe" - private const val RENDER_PROBE_NAME = "ComposeRenderProbe" - } -} - - - diff --git a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/InputComponentsGameTest.kt b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/InputComponentsGameTest.kt deleted file mode 100644 index 1cdad6e25..000000000 --- a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/InputComponentsGameTest.kt +++ /dev/null @@ -1,303 +0,0 @@ -package net.kernelpanicsoft.archie.gametest.internal.tests - -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import net.kernelpanicsoft.archie.gametest.ClientGameTest -import net.kernelpanicsoft.archie.gametest.ClientGameTestContext -import net.kernelpanicsoft.archie.gametest.waitForScreen -import net.kernelpanicsoft.archie.gui.ComposeScreen -import net.kernelpanicsoft.archie.gui.composables.basic.Text -import net.kernelpanicsoft.archie.gui.composables.input.Button -import net.kernelpanicsoft.archie.gui.composables.input.Checkbox -import net.kernelpanicsoft.archie.gui.composables.input.ColorPicker -import net.kernelpanicsoft.archie.gui.composables.input.RadioGroup -import net.kernelpanicsoft.archie.gui.composables.input.RadioOption -import net.kernelpanicsoft.archie.gui.composables.input.Slider -import net.kernelpanicsoft.archie.gui.composables.input.Switch -import net.kernelpanicsoft.archie.gui.composables.input.textfield.BasicTextField -import net.kernelpanicsoft.archie.gui.composables.containers.Scrollable -import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates -import net.kernelpanicsoft.archie.gui.layout.Arrangement -import net.kernelpanicsoft.archie.gui.layout.Column -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.fillMaxSize -import net.kernelpanicsoft.archie.gui.modifiers.sizeIn -import net.kernelpanicsoft.archie.gui.theme.Theme -import net.kernelpanicsoft.archie.gui.util.HsvColor -import net.kernelpanicsoft.archie.gui.util.KColor -import net.minecraft.network.chat.Component -import org.lwjgl.glfw.GLFW -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference - -/** - * Client GameTest coverage for every standalone input composable in - * `net.kernelpanicsoft.archie.gui.composables.input` - Checkbox, Switch, RadioGroup, Slider, - * BasicTextField, ColorPicker, Button: hierarchy shape, click/hover/keypress/type input - * handling, and (where the component is texture-state driven) which [TextureStates] key it - * resolves for a given interaction - a texture-correctness check with no pixel comparison. - * - * Runs against [InputComponentsProbeScreen], a plain [ComposeScreen] with no world/menu/player, - * since none of these composables need one - unlike slot/menu rendering, which does (see - * `net.kernelpanicsoft.archie.test.gametest.TestScreenGameTest` in Archie-Test for that case). - */ -@Suppress("unused") -class InputComponentsGameTest { - @ClientGameTest - fun ClientGameTestContext.testHierarchyAndSizing() { - setScreen { InputComponentsProbeScreen() } - waitForScreen { - waitForLayer(0) { - node("Column") { - assertHasDescendant("Checkbox") - assertHasDescendant("Switch") - assertHasDescendant("RadioButton") - assertHasDescendant("Slider") - assertHasDescendant("TextFieldCore") - assertHasDescendant("ColorPicker") - assertHasDescendant("Button") - - // Every RadioGroup option's Row wraps exactly one RadioButton (itself inside - // the Box every Clickable-based composable renders its content in) plus its - // label, in order. - node("Row") { - assertChildNames("Box", "Text") - node("Box") { assertHasDescendant("RadioButton") } - } - - assertAllDescendantsSized() - } - } - } - } - - @ClientGameTest - fun ClientGameTestContext.testCheckboxHoverAndClickRenderState() { - setScreen { InputComponentsProbeScreen() } - waitForScreen { - waitForLayer(0) { - node("Checkbox") { - assertRenderState(TextureStates.DEFAULT) - - hover() - waitForComposeIdle() - assertRenderState(TextureStates.HOVERED) - - click() - waitForComposeIdle() - assertRenderState(TextureStates.CLICKED_AND_HOVERED) { "Expected checkbox to be both checked and hovered right after a click at its own center" } - - click() - waitForComposeIdle() - assertRenderState(TextureStates.HOVERED) { "Expected checkbox to be unchecked again after a second click" } - } - } - } - } - - @ClientGameTest - fun ClientGameTestContext.testSwitchHoverAndClickRenderState() { - setScreen { InputComponentsProbeScreen() } - waitForScreen { - waitForLayer(0) { - node("Switch") { - // Probe's initial `switched = true`. - assertRenderState(TextureStates.CLICKED) - - click() - waitForComposeIdle() - assertRenderState(TextureStates.DEFAULT) { "Expected switch to be off after toggling its initial on state" } - - hover() - waitForComposeIdle() - assertRenderState(TextureStates.HOVERED) - } - } - } - } - - @ClientGameTest - fun ClientGameTestContext.testRadioGroupSelectsOptionExclusively() { - val selected = AtomicReference("alpha") - setScreen { InputComponentsProbeScreen(onRadioSelected = { selected.set(it) }) } - waitForScreen { - waitForLayer(0) { - node("Column") { - val options = nodes("RadioButton") - assertTrue(options.size == 3) { "Expected 3 RadioButton options, found ${options.size}" } - - // Probe's initial `radio = "alpha"` (declaration order: alpha, beta, gamma). - options[0] { - assertRenderState(TextureStates.CLICKED) { "Expected the first (initially selected) radio option to render CLICKED" } - } - options[1] { assertRenderState(TextureStates.DEFAULT) } - options[2] { assertRenderState(TextureStates.DEFAULT) } - - // Select "beta" (options[1]) and verify selection moved there exclusively. - options[1] { click() } - waitForComposeIdle() - - assertEquals("beta", selected.get()) { "Expected clicking the second radio option to select 'beta'" } - options[0] { - assertRenderState(TextureStates.DEFAULT) { "Expected the first option to deselect once a different option is chosen" } - } - options[1] { assertRenderState(TextureStates.CLICKED) } - options[2] { assertRenderState(TextureStates.DEFAULT) } - - // Clicking an already-selected option is a documented no-op (see RadioButtonCore). - options[1] { click() } - waitForComposeIdle() - assertEquals("beta", selected.get()) { "Expected re-clicking the selected option to stay a no-op" } - } - } - } - } - - @ClientGameTest - fun ClientGameTestContext.testSliderHoverAndDragRenderState() { - setScreen { InputComponentsProbeScreen() } - waitForScreen { - waitForLayer(0) { - node("Slider") { - assertRenderState(TextureStates.DEFAULT) - - hover() - waitForComposeIdle() - assertRenderState(TextureStates.HOVERED) - - context.getInput().holdMouse(0) - waitForComposeIdle() - assertRenderState(TextureStates.CLICKED) { "Expected slider to report the dragging (CLICKED) state while the mouse button is held" } - - context.getInput().releaseMouse(0) - waitForComposeIdle() - assertRenderState(TextureStates.HOVERED) { "Expected slider to return to hovered after releasing the drag" } - } - } - } - } - - @ClientGameTest - fun ClientGameTestContext.testButtonClickFiresCallbackAndRenderState() { - val clicked = AtomicBoolean(false) - setScreen { InputComponentsProbeScreen(onButtonClick = { clicked.set(true) }) } - waitForScreen { - waitForLayer(0) { - node("Button") { - assertRenderState(TextureStates.DEFAULT) - - hover() - waitForComposeIdle() - assertRenderState(TextureStates.HOVERED) - - click() - waitForComposeIdle() - - assertTrue(clicked.get()) { "Expected Button's onClick callback to have fired" } - } - } - } - } - - @ClientGameTest - fun ClientGameTestContext.testTextFieldTypeAndBackspace() { - val typed = AtomicReference("") - setScreen { InputComponentsProbeScreen(onTextChanged = { typed.set(it) }) } - waitForScreen { - waitForLayer(0) { - node("TextFieldCore") { - click() - waitForComposeIdle() - - type("hello") - waitForComposeIdle() - assertEquals("hello", typed.get()) { "Expected typed characters to reach onValueChange" } - - pressKey(GLFW.GLFW_KEY_BACKSPACE) - waitForComposeIdle() - assertEquals("hell", typed.get()) { "Expected backspace to remove the last typed character" } - - assertAllDescendantsSized() - } - } - } - } - - @ClientGameTest - fun ClientGameTestContext.testColorPickerInteractionUpdatesColor() { - val initial = HsvColor.from(KColor.CYAN) - val changed = AtomicReference(null) - setScreen { InputComponentsProbeScreen(initialColor = initial, onColorChanged = { changed.set(it) }) } - waitForScreen { - waitForLayer(0) { - node("ColorPicker") { - assertChildNames("SaturationValueArea", "AlphaBar", "HueBar") - assertAllDescendantsSized() - - node("HueBar") { click() } - waitForComposeIdle() - - assertTrue(changed.get() != null) { "Expected clicking the hue bar to report a color change" } - } - } - } - } -} - -private class InputComponentsProbeScreen( - private val initialColor: HsvColor = HsvColor.from(KColor.CYAN), - private val onButtonClick: () -> Unit = {}, - private val onTextChanged: (String) -> Unit = {}, - private val onColorChanged: (HsvColor) -> Unit = {}, - private val onRadioSelected: (String) -> Unit = {}, -) : ComposeScreen(Component.literal("Input Components Probe")) { - override fun init() { - super.init() - start { - Theme { - var checked by remember { mutableStateOf(false) } - var switched by remember { mutableStateOf(true) } - var radio by remember { mutableStateOf("alpha") } - var slider by remember { mutableStateOf(0.35f) } - var text by remember { mutableStateOf("") } - var color by remember { mutableStateOf(initialColor) } - - // Scrollable, since this probe's fixed-height components (ColorPicker etc.) can - // together exceed the actual game window's height depending on display/GUI - // scale - without it, a plain Column silently starves later children of their - // Column-inherited remaining-height budget instead of scrolling. - Scrollable(modifier = Modifier.fillMaxSize()) { - Column(verticalArrangement = Arrangement.spacedBy(4)) { - Checkbox(checked = checked, onCheckedChange = { checked = it }) - Switch(checked = switched, onCheckedChange = { switched = it }) - RadioGroup( - options = listOf( - RadioOption("alpha", Component.literal("Alpha")), - RadioOption("beta", Component.literal("Beta")), - RadioOption("gamma", Component.literal("Gamma")), - ), - selected = radio, - onSelected = { radio = it; onRadioSelected(it) }, - ) - Slider(value = slider, onValueChange = { slider = it }, steps = 10) - BasicTextField( - value = text, - onValueChange = { text = it; onTextChanged(it) }, - modifier = Modifier.sizeIn(minWidth = 120, maxWidth = 180), - ) - ColorPicker( - color = color, - onColorChanged = { color = it; onColorChanged(it) }, - modifier = Modifier.sizeIn(minWidth = 150, minHeight = 90, maxWidth = 170, maxHeight = 100), - ) - Button(onClick = { onButtonClick() }) { - Text(Component.literal("Click me"), dropShadow = false) - } - } - } - } - } - } -} diff --git a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/LayoutComponentsGameTest.kt b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/LayoutComponentsGameTest.kt deleted file mode 100644 index 49677cc27..000000000 --- a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/LayoutComponentsGameTest.kt +++ /dev/null @@ -1,211 +0,0 @@ -package net.kernelpanicsoft.archie.gametest.internal.tests - -import androidx.compose.runtime.Composable -import net.kernelpanicsoft.archie.gametest.ClientGameTest -import net.kernelpanicsoft.archie.gametest.ClientGameTestContext -import net.kernelpanicsoft.archie.gametest.TestNodeScope -import net.kernelpanicsoft.archie.gametest.waitForScreen -import net.kernelpanicsoft.archie.gui.ComposeScreen -import net.kernelpanicsoft.archie.gui.composables.basic.HorizontalDivider -import net.kernelpanicsoft.archie.gui.composables.basic.Icon -import net.kernelpanicsoft.archie.gui.composables.basic.Text -import net.kernelpanicsoft.archie.gui.composables.containers.Collapsible -import net.kernelpanicsoft.archie.gui.composables.containers.Panel -import net.kernelpanicsoft.archie.gui.composables.containers.Scrollable -import net.kernelpanicsoft.archie.gui.composables.containers.ScrollableState -import net.kernelpanicsoft.archie.gui.composables.containers.TabPanel -import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates -import net.kernelpanicsoft.archie.gui.layout.Arrangement -import net.kernelpanicsoft.archie.gui.layout.Column -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.MeasureResult -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.height -import net.kernelpanicsoft.archie.gui.modifiers.width -import net.kernelpanicsoft.archie.gui.theme.Theme -import net.minecraft.network.chat.Component -import net.minecraft.resources.ResourceLocation -import java.util.concurrent.atomic.AtomicBoolean - -/** - * Client GameTest coverage for the remaining (non-input) composables: [Panel]/[net.kernelpanicsoft.archie.gui.composables.containers.Surface], - * [HorizontalDivider], [Icon], [Text], [Collapsible], [Scrollable], and [TabPanel]. - * - * Each gets its own small, single-purpose probe screen rather than one shared screen, since - * several of these composables reuse generic container node names ("Row", "Column") - isolating - * them keeps [TestNodeScope.node] lookups unambiguous without needing a unique-name redesign. - */ -@Suppress("unused") -class LayoutComponentsGameTest { - @ClientGameTest - fun ClientGameTestContext.testPanelDividerIconTextHierarchyAndSizing() { - setScreen { PanelDisplayProbeScreen() } - waitForScreen { - waitForLayer(0) { - node("Surface") { - // Panel wraps its content in a padded Box inside the themed Surface. - node("Box") { - assertChildNames("Text", "Spacer", "Texture") - } - assertAllDescendantsSized() - } - } - } - } - - @ClientGameTest - fun ClientGameTestContext.testCollapsibleTogglesContentOnHeaderClick() { - val toggled = AtomicBoolean(false) - setScreen { CollapsibleProbeScreen(initiallyExpanded = false, onToggled = { toggled.set(true) }) } - waitForScreen { - waitForLayer(0) { - node("Column") { - assertTrue(!hasDescendant("CollapsibleContent")) { "Expected content hidden while collapsed" } - - node("Row") { click() } - waitForComposeIdle() - - assertTrue(toggled.get()) { "Expected onToggled to fire on header click" } - assertHasDescendant("CollapsibleContent") - assertAllDescendantsSized() - - node("Row") { click() } - waitForComposeIdle() - assertTrue(!hasDescendant("CollapsibleContent")) { "Expected content hidden again after collapsing" } - } - } - } - } - - @ClientGameTest - fun ClientGameTestContext.testScrollableScrollsContent() { - val scrollState = ScrollableState() - setScreen { ScrollableProbeScreen(scrollState) } - waitForScreen { - waitForLayer(0) { - node("Scrollable") { - assertAllDescendantsSized() - assertEquals(0.0, computeOnClient { scrollState.scrollOffset }) - - scroll(y = -10.0) - waitForComposeIdle() - - val offsetAfterScroll = computeOnClient { scrollState.scrollOffset } - assertTrue(offsetAfterScroll > 0.0) { "Expected scrolling over the Scrollable node to move scrollOffset, got $offsetAfterScroll" } - } - } - } - } - - @ClientGameTest - fun ClientGameTestContext.testTabPanelSwitchesActiveTabOnClick() { - setScreen { TabPanelProbeScreen() } - waitForScreen { - waitForLayer(0) { - node("Column") { - val tabs = nodes("Tab") - assertTrue(tabs.size == 2) { "Expected 2 Tab headers, found ${tabs.size}" } - - assertHasDescendant("FirstTabMarker") - assertTrue(!hasDescendant("SecondTabMarker")) { "Expected only the first tab's content to be composed initially" } - tabs[0] { - assertRenderState(TextureStates.CLICKED) { - "Expected the first (initially active) tab to render CLICKED" - } - } - tabs[1] { click() } - waitForComposeIdle() - - assertHasDescendant("SecondTabMarker") - assertTrue(!hasDescendant("FirstTabMarker")) { "Expected only the second tab's content to be composed after switching" } - tabs[1] { assertRenderState(TextureStates.CLICKED) } - tabs[0] { assertRenderState(TextureStates.DEFAULT) } - } - } - } - } -} - -private class PanelDisplayProbeScreen : ComposeScreen(Component.literal("Panel Display Probe")) { - override fun init() { - super.init() - start { - Theme { - Panel { - Text(Component.literal("Panel content"), dropShadow = false) - HorizontalDivider() - Icon(texture = ResourceLocation.withDefaultNamespace("textures/item/porkchop.png"), size = 16) - } - } - } - } -} - -private class CollapsibleProbeScreen( - private val initiallyExpanded: Boolean, - private val onToggled: (Boolean) -> Unit, -) : ComposeScreen(Component.literal("Collapsible Probe")) { - override fun init() { - super.init() - start { - Theme { - Column { - Collapsible( - title = Component.literal("Section"), - initiallyExpanded = initiallyExpanded, - onToggled = onToggled, - ) { - Text(Component.literal("Collapsible body"), dropShadow = false) - } - } - } - } - } -} - -private class ScrollableProbeScreen( - private val scrollState: ScrollableState, -) : ComposeScreen(Component.literal("Scrollable Probe")) { - override fun init() { - super.init() - start { - Theme { - Scrollable(state = scrollState, modifier = Modifier.height(80).width(120)) { - Column(verticalArrangement = Arrangement.spacedBy(2)) { - repeat(60) { index -> - Text(Component.literal("Row ${index + 1}"), dropShadow = false) - } - } - } - } - } - } -} - -/** A zero-size, invisible node whose mere presence in the tree marks which branch was composed. */ -@Composable -private fun Marker(name: String) { - Layout(name = name, measurePolicy = { _, _, _ -> MeasureResult(0, 0) {} }) -} - -private class TabPanelProbeScreen : ComposeScreen(Component.literal("Tab Panel Probe")) { - override fun init() { - super.init() - start { - Theme { - Column { - TabPanel { - tab("first", Component.literal("First")) { - Marker("FirstTabMarker") - Text(Component.literal("First tab content"), dropShadow = false) - } - tab("second", Component.literal("Second")) { - Marker("SecondTabMarker") - Text(Component.literal("Second tab content"), dropShadow = false) - } - } - } - } - } - } -} diff --git a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ModalComponentsGameTest.kt b/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ModalComponentsGameTest.kt deleted file mode 100644 index b78e4b820..000000000 --- a/Archie/common/src/main/gametest/net/kernelpanicsoft/archie/gametest/internal/tests/ModalComponentsGameTest.kt +++ /dev/null @@ -1,210 +0,0 @@ -package net.kernelpanicsoft.archie.gametest.internal.tests - -import net.kernelpanicsoft.archie.gametest.ClientGameTest -import net.kernelpanicsoft.archie.gametest.ClientGameTestContext -import net.kernelpanicsoft.archie.gametest.LayerSelector -import net.kernelpanicsoft.archie.gametest.waitForScreen -import net.kernelpanicsoft.archie.gui.ComposeScreen -import net.kernelpanicsoft.archie.gui.composables.basic.Text -import net.kernelpanicsoft.archie.gui.composables.input.Button -import net.kernelpanicsoft.archie.gui.composables.modal.ModalChoice -import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates -import net.kernelpanicsoft.archie.gui.layer.LocalLayerManager -import net.kernelpanicsoft.archie.gui.layout.Column -import net.minecraft.network.chat.Component -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.atomic.AtomicReference - -/** - * Client GameTest coverage for the built-in modal dialogs (`net.kernelpanicsoft.archie.gui.composables.modal`): - * [net.kernelpanicsoft.archie.gui.composables.modal.AlertDialog], - * [net.kernelpanicsoft.archie.gui.composables.modal.PromptDialog], - * [net.kernelpanicsoft.archie.gui.composables.modal.ChoiceDialog], and - * [net.kernelpanicsoft.archie.gui.composables.modal.ConfirmDialog] - modal-layer hierarchy, - * typing into a prompt, disabled-button texture state, and confirm/cancel/dismiss flows. - * - * Runs against [ModalComponentsProbeScreen], a plain [ComposeScreen] (a [net.kernelpanicsoft.archie.gui.LayerManagerProvider] - * on its own, same as [net.kernelpanicsoft.archie.gui.ComposeContainerScreen]), so no world, - * menu, or player is needed. `net.kernelpanicsoft.archie.test.gametest.TestScreenGameTest` in - * Archie-Test already covers a [net.kernelpanicsoft.archie.gui.composables.modal.ConfirmDialog] - * end to end against the real block-entity-backed screen; this file exercises it (plus the other - * three dialog kinds) at the lightweight-probe level instead of duplicating that coverage. - */ -@Suppress("unused") -class ModalComponentsGameTest { - @ClientGameTest - fun ClientGameTestContext.testAlertDialogConfirmDismisses() { - val confirmed = AtomicBoolean(false) - setScreen { ModalComponentsProbeScreen(onAlertConfirm = { confirmed.set(true) }) } - waitForScreen { - assertEquals(1, layerCount) - val triggers = baseLayer.rootNode { nodes("Button") } - triggers[0] { click() } // "Open Alert" - waitFor { _ -> layerCount == 2 } - - node("Surface", layer = LayerSelector.Top) { - val buttons = nodes("Button") - assertTrue(buttons.size == 1) { "Expected AlertDialog to have exactly 1 action button, found ${buttons.size}" } - buttons[0] { click() } - } - - waitFor { _ -> layerCount == 1 } - assertTrue(confirmed.get()) { "Expected AlertDialog's onConfirm to fire" } - } - } - - @ClientGameTest - fun ClientGameTestContext.testPromptDialogValidatorDisablesConfirmUntilTyped() { - val confirmedValue = AtomicReference(null) - setScreen { ModalComponentsProbeScreen(onPromptConfirm = { confirmedValue.set(it) }) } - waitForScreen { - val triggers = baseLayer.rootNode { nodes("Button") } - triggers[1] { click() } // "Open Prompt" - waitFor { _ -> layerCount == 2 } - - node("Surface", layer = LayerSelector.Top) { - assertHasDescendant("TextFieldCore") - val buttons = nodes("Button") - assertTrue(buttons.size == 2) { "Expected PromptDialog to have 2 action buttons (cancel, confirm), found ${buttons.size}" } - val confirmButton = buttons[1] - - // Empty initial value fails the `it.isNotBlank()` validator - confirm starts disabled. - confirmButton { - assertRenderState(TextureStates.DISABLED) { "Expected the confirm button to render DISABLED while the field is blank" } - } - - node("TextFieldCore") { click(); type("hello") } - waitForComposeIdle() - - confirmButton { - assertTrue(renderState != TextureStates.DISABLED) { "Expected the confirm button to no longer be disabled once the field has text" } - } - - confirmButton { click() } - } - - waitFor { _ -> layerCount == 1 } - assertEquals("hello", confirmedValue.get()) { "Expected PromptDialog's onConfirm to report the typed value" } - } - } - - @ClientGameTest - fun ClientGameTestContext.testChoiceDialogSelectsEnabledOptionAndSkipsDisabled() { - val selected = AtomicReference(null) - setScreen { ModalComponentsProbeScreen(onChoiceSelected = { selected.set(it) }) } - waitForScreen { - val triggers = baseLayer.rootNode { nodes("Button") } - triggers[2] { click() } // "Open Choice" - waitFor { _ -> layerCount == 2 } - waitForComposeIdle() - - node("Surface", layer = LayerSelector.Top) { - val buttons = nodes("Button") - // 3 choices ("alpha", "beta", disabled "locked") + 1 cancel button, in that order. - assertTrue(buttons.size == 4) { "Expected ChoiceDialog to have 4 buttons (3 choices + cancel), found ${buttons.size}" } - - buttons[2] { - assertRenderState(TextureStates.DISABLED) { "Expected the disabled 'locked' choice to render DISABLED" } - } - - buttons[1] { click() } // "beta" - } - - waitFor { _ -> layerCount == 1 } - assertEquals("beta", selected.get()) { "Expected ChoiceDialog's onSelected to report the clicked choice" } - } - } - - @ClientGameTest - fun ClientGameTestContext.testConfirmDialogConfirmAndCancelFlows() { - val confirmed = AtomicBoolean(false) - val cancelled = AtomicBoolean(false) - setScreen { - ModalComponentsProbeScreen( - onConfirmDialogConfirm = { confirmed.set(true) }, - onConfirmDialogCancel = { cancelled.set(true) }, - ) - } - waitForScreen { - val triggers = baseLayer.rootNode { nodes("Button") } - - // Confirm path. - triggers[3] { click() } // "Open Confirm" - waitFor { _ -> layerCount == 2 } - node("Surface", layer = LayerSelector.Top) { - val buttons = nodes("Button") - assertTrue(buttons.size == 2) { "Expected ConfirmDialog to have 2 action buttons (confirm, cancel), found ${buttons.size}" } - buttons[0] { click() } // confirm is first, see ConfirmDialog.kt - } - // ConfirmDialog's dismiss is deferred behind a close animation (see DIALOG_ANIMATION_MS), - // so unlike the other three dialogs this can't rely on waitForComposeIdle alone. - waitFor { _ -> layerCount == 1 } - assertTrue(confirmed.get()) { "Expected ConfirmDialog's onConfirm to fire" } - - // Cancel path. - triggers[3] { click() } // "Open Confirm" again - waitFor { _ -> layerCount == 2 } - node("Surface", layer = LayerSelector.Top) { - val buttons = nodes("Button") - buttons[1] { click() } // cancel is second - } - waitFor { _ -> layerCount == 1 } - assertTrue(cancelled.get()) { "Expected ConfirmDialog's onCancel to fire" } - } - } -} - -private class ModalComponentsProbeScreen( - private val onAlertConfirm: () -> Unit = {}, - private val onPromptConfirm: (String) -> Unit = {}, - private val onChoiceSelected: (String) -> Unit = {}, - private val onConfirmDialogConfirm: () -> Unit = {}, - private val onConfirmDialogCancel: () -> Unit = {}, -) : ComposeScreen(Component.literal("Modal Components Probe")) { - override fun init() { - super.init() - start { - val layers = LocalLayerManager.current - Column { - Button(onClick = { - layers.alertDialog( - title = Component.literal("Alert"), - message = Component.literal("Something happened."), - onConfirm = onAlertConfirm, - ) - }) { Text(Component.literal("Open Alert"), dropShadow = false) } - - Button(onClick = { - layers.promptDialog( - title = Component.literal("Prompt"), - initialValue = "", - validator = { it.isNotBlank() }, - onConfirm = onPromptConfirm, - ) - }) { Text(Component.literal("Open Prompt"), dropShadow = false) } - - Button(onClick = { - layers.choiceDialog( - title = Component.literal("Choice"), - choices = listOf( - ModalChoice("alpha", Component.literal("Alpha")), - ModalChoice("beta", Component.literal("Beta")), - ModalChoice("locked", Component.literal("Locked"), enabled = false), - ), - onSelected = onChoiceSelected, - ) - }) { Text(Component.literal("Open Choice"), dropShadow = false) } - - Button(onClick = { - layers.confirmDialog( - title = Component.literal("Confirm"), - onConfirm = onConfirmDialogConfirm, - onCancel = onConfirmDialogCancel, - ) { - Text(Component.literal("Are you sure?"), dropShadow = false) - } - }) { Text(Component.literal("Open Confirm"), dropShadow = false) } - } - } - } -} diff --git a/Archie/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java b/Archie/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java deleted file mode 100644 index 30e433a7a..000000000 --- a/Archie/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java +++ /dev/null @@ -1,52 +0,0 @@ -package net.kernelpanicsoft.archie.gui.access; - -import java.util.ArrayDeque; -import java.util.Deque; -import net.kernelpanicsoft.archie.Archie; - -/** - * Tracks when slot rendering overrides the default GUI depth so downstream - * render calls (like GuiGraphics#renderItem) can adjust their transforms. - */ -public final class SlotLayerDepthContext -{ - private static final ThreadLocal> DEPTHS = ThreadLocal.withInitial(ArrayDeque::new); - - private SlotLayerDepthContext() - { - } - - public static void push(float depth) - { - Deque depths = DEPTHS.get(); - depths.push(depth); - Archie.LOGGER.debug("Slot depth push -> {} (stack size={})", depth, depths.size()); - } - - public static void pop() - { - Deque depths = DEPTHS.get(); - if (depths.isEmpty()) - { - DEPTHS.remove(); - return; - } - Float removed = depths.pop(); - Archie.LOGGER.debug("Slot depth pop -> {} (remaining={})", removed, depths.size()); - if (depths.isEmpty()) - { - DEPTHS.remove(); - } - } - - public static boolean isActive() - { - Deque depths = DEPTHS.get(); - return !depths.isEmpty(); - } - - public static Float currentDepth() - { - return DEPTHS.get().peek(); - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.common.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.common.kt deleted file mode 100644 index 24e218198..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.common.kt +++ /dev/null @@ -1,8 +0,0 @@ -package net.kernelpanicsoft.archie - -/** Cross-loader platform identification, backed by an `actual` per mod loader. */ -expect object APlatform -{ - /** The current mod loader's short id: `"fabric"` on Fabric, `"neoforge"` on NeoForge. */ - val platform: String -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt deleted file mode 100644 index f52e8685e..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt +++ /dev/null @@ -1,353 +0,0 @@ -package net.kernelpanicsoft.archie - -import com.mojang.logging.LogUtils -import dev.architectury.event.events.client.ClientTickEvent -import dev.architectury.platform.Mod -import dev.architectury.platform.Platform -import dev.architectury.registry.ReloadListenerRegistry -import kotlinx.serialization.Serializable -import net.kernelpanicsoft.archie.config.CategorySpec -import net.kernelpanicsoft.archie.config.ConfigContainer -import net.kernelpanicsoft.archie.config.ConfigSpec -import net.kernelpanicsoft.archie.config.DataSpec -import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform -import net.kernelpanicsoft.archie.data.common.conditions.ABuiltinConditions -import net.kernelpanicsoft.archie.data.common.crafting.ingredients.ABuiltinIngredients -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags -import net.kernelpanicsoft.archie.data.internal.ArchieDatagen -import net.kernelpanicsoft.archie.events.AEvents -import net.kernelpanicsoft.archie.gametest.AGameTestPlatform -import net.kernelpanicsoft.archie.gametest.AGameTestSide -import net.kernelpanicsoft.archie.gametest.ThreadingImpl -import net.kernelpanicsoft.archie.gametest.internal.ArchieGameTest -import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager -import net.kernelpanicsoft.archie.gui.item.ItemStateManager -import net.kernelpanicsoft.archie.gui.theme.ThemeManifestResourceListener -import net.kernelpanicsoft.archie.gui.theme.ThemeResourceListener -import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel -import net.kernelpanicsoft.archie.util.buildArray -import net.kernelpanicsoft.archie.util.onClient -import net.kernelpanicsoft.archie.util.rem -import net.minecraft.core.registries.BuiltInRegistries -import net.minecraft.network.chat.Component -import net.minecraft.server.packs.PackType -import net.minecraft.world.item.BlockItem -import net.minecraft.world.item.Items -import net.minecraft.world.level.block.entity.BlockEntityType -import org.slf4j.Logger - -/** - * Archie's mod object and library entrypoint. - */ -object Archie -{ - /** Archie's own mod id, used as the namespace for its resources and network channel. */ - const val MOD_ID = "archie" - - - /** The Architectury [Mod] descriptor for Archie itself. */ - @JvmField - val MOD: Mod = Platform.getMod(MOD_ID) - - /** Shared SLF4J logger for Archie's own internal logging. */ - @JvmField - val LOGGER: Logger = LogUtils.getLogger() - - /** - * Initializes Archie's shared (loader-independent) systems. - * - * Registers Archie with [AEvents], wires up networking (skipped only for a server-only - * gametest run, since Architectury's networking registration touches client-only classes), - * initializes block entity state syncing, built-in data providers, and Archie's own config, - * and activates the datagen/gametest code paths when running under those tasks. - * - * @throws IllegalStateException if running on LexForge, which is not supported. - */ - @JvmStatic - fun init() - { - - if (Platform.isMinecraftForge()) - error("LexForge is not supported. Switch to NeoForge, or don't use my mods.") - AEvents += MOD - if (!AGameTestPlatform.isGameTest || AGameTestPlatform.side == AGameTestSide.CLIENT) - { - ArchieNetworkChannel.init() - } - BlockEntityStateManager.init() - ItemStateManager.init() - - ABuiltinIngredients.init() - ABuiltinConditions.init() - ACommonTags.init() - Config.init() - - - // Datagen and GameTest code paths are only activated in dedicated run configs. - if (AGameTestPlatform.isGameTest) - ArchieGameTest.init() - if (ADataGeneratorPlatform.isDataGen) - ArchieDatagen.init() - onClient { - ReloadListenerRegistry.register(PackType.CLIENT_RESOURCES, ThemeManifestResourceListener(), Archie % "theme_manifest") - ReloadListenerRegistry.register(PackType.CLIENT_RESOURCES, ThemeResourceListener(), Archie % "theme") - } - } - - /** - * Reserved for client-only initialization that must run after [init], from a client - * entrypoint. Currently a no-op: Archie's own config screen(s) already register synchronously - * inside [init] via `Config.init()` (see [ConfigContainer.initClient]), so there's nothing left - * to do here. - */ - @JvmStatic - fun initClient() - { - ClientTickEvent.CLIENT_POST.register { - ThreadingImpl.onClientTick() - } - } - - /** - * Reserved for common-side initialization that must run after both [init] and platform - * bootstrap. Currently a no-op. - */ - @JvmStatic - fun initCommon() - { - } - - /** - * Archie's own config, registered under the "Config" title. `General` holds Archie's real - * settings; `Test` is a self-test fixture exercising every [DataSpec] value type - * supported by the config system and is not meant to be user-facing. - */ - object Config : ConfigContainer(MOD) - { - object Common : ConfigSpec.Common(MOD) - { - object General : CategorySpec(Component.literal("General"), "general") - { - val tests by boolean( - title = Component.literal("Tests"), - default = false - ) - } - - @Suppress("unused") - object Test : CategorySpec(Component.literal("Test Category"), "test") - { - override val isEnabled: Boolean - get() = General.tests - - var testBoolean by boolean( - title = Component.literal("Test Boolean"), - comment = Component.literal("Test Comment") - ) - - val testInt by int( - title = Component.literal("Test Int"), - ) - - val testLong by long( - title = Component.literal("Test Long"), - ) - - val testIntSlider by intSlider( - title = Component.literal("Test Int Slider"), - min = Int.MIN_VALUE / 2 + 1, - max = Int.MAX_VALUE / 2 - ) - - val testLongSlider by longSlider( - title = Component.literal("Test Long Slider"), - min = Long.MIN_VALUE / 2 + 1, - max = Long.MAX_VALUE / 2, - ) - - val testFloat by float( - title = Component.literal("Test Float"), - ) - - val testDouble by double( - title = Component.literal("Test Double"), - ) - - val testString by string( - title = Component.literal("Test String"), - ) - - val testSpec by spec( - title = Component.literal("Test Spec"), - default = TestSpec(), - factory = ::TestSpec - ) - - val testRegistry: BlockItem by registry( - title = Component.literal("Test Registry"), - default = Items.COBBLESTONE, - subclass = BlockItem::class, - registry = BuiltInRegistries.ITEM - ) - - val testKeycode by keycode( - title = Component.literal("Test Keycode"), - ) - - val testColor by color( - title = Component.literal("Test Color"), - alpha = true - ) - - val testEnumSelector by enumSelector( - title = Component.literal("Test Enum Selector"), - kclass = TestEnum::class, - default = TestEnum.Foo - ) - - val testSelector by selector( - title = Component.literal("Test Selector"), - kclass = String::class, - default = "foo", - entries = buildArray { - add("foo") - add("bar") - } - ) - - val testIntList by intList( - title = Component.literal("Test Int List"), - ) - - val testLongList by longList( - title = Component.literal("Test Long List"), - ) - - val testFloatList by floatList( - title = Component.literal("Test Float List"), - ) - - val testDoubleList by doubleList( - title = Component.literal("Test Double List"), - ) - - val testStringList by stringList( - title = Component.literal("Test String List"), - ) - - val testSpecList by specList( - title = Component.literal("Test Spec List"), - factory = ::TestSpec - ) - - val testRegistryList: List by registryList( - title = Component.literal("Test Registry List"), - factory = Items::COBBLESTONE, - subclass = BlockItem::class, - registry = BuiltInRegistries.ITEM - ) - - val testKeycodeList by keycodeList( - title = Component.literal("Test Keycode List"), - ) - - val testColorList by colorList( - title = Component.literal("Test Color List"), - ) - - val testIntMap by intMap( - title = Component.literal("Test Int Map"), - ) - - val testLongMap by longMap( - title = Component.literal("Test Long Map"), - ) - - val testFloatMap by floatMap( - title = Component.literal("Test Float Map"), - ) - - val testDoubleMap by doubleMap( - title = Component.literal("Test Double Map"), - ) - - val testStringMap by stringMap( - title = Component.literal("Test String Map"), - ) - - val testSpecMap by specMap( - title = Component.literal("Test Spec Map"), - factory = ::TestSpec - ) - - val testRegistryMap: Map by registryMap( - title = Component.literal("Test Registry Map"), - factory = Items::COBBLESTONE, - subclass = BlockItem::class, - registry = BuiltInRegistries.ITEM - ) - - val testKeycodeMap by keycodeMap( - title = Component.literal("Test Keycode Map"), - ) - - val testColorMap by colorMap( - title = Component.literal("Test Color Map") - ) - - val testNestedSpec by spec( - title = Component.literal("Test Nested Spec"), - default = TestNestedSpec(), - factory = ::TestNestedSpec - ) - - @Serializable - enum class TestEnum - { - Foo, - Bar - } - - class TestSpec : DataSpec(Component.literal("Test Spec")) - { - val test by boolean( - title = Component.literal("Test"), - ) - } - - class TestNestedSpec : DataSpec(Component.literal("Test Nested Spec")) - { - val childrenList by specList( - title = Component.literal("Children List"), - factory = ::TestNestedSpec - ) - - val childrenMap by specMap( - title = Component.literal("Children Map"), - factory = ::TestNestedSpec - ) - } - - object TestSub : CategorySpec(Component.literal("Test Subcategory"), "test_sub") - { - val test by boolean( - title = Component.literal("Test"), - ) - - val testRegistry by registry( - title = Component.literal("Test Registry"), - default = BlockEntityType.CHEST, - registry = BuiltInRegistries.BLOCK_ENTITY_TYPE - ) - - object TestSubSub : CategorySpec(Component.literal("Test Sub Subcategory"), "test_sub_sub") - { - val test by boolean( - title = Component.literal("Test"), - ) - } - } - } - } - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/block/entity/NBTBlockEntity.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/block/entity/NBTBlockEntity.kt deleted file mode 100644 index e2cdfddc9..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/block/entity/NBTBlockEntity.kt +++ /dev/null @@ -1,60 +0,0 @@ -package net.kernelpanicsoft.archie.block.entity - -import net.kernelpanicsoft.archie.serialization.NBTHolder -import net.minecraft.core.BlockPos -import net.minecraft.core.HolderLookup -import net.minecraft.nbt.CompoundTag -import net.minecraft.network.protocol.Packet -import net.minecraft.network.protocol.game.ClientGamePacketListener -import net.minecraft.network.protocol.game.ClientboundBlockEntityDataPacket -import net.minecraft.world.level.block.entity.BlockEntity -import net.minecraft.world.level.block.entity.BlockEntityType -import net.minecraft.world.level.block.state.BlockState - -/** - * A [BlockEntity] base class that automatically persists fields declared with [NBTHolder] - * delegates to and from the block entity's [CompoundTag]. - * - * Subclass this and declare fields using the [NBTHolder] delegation API: - * ```kotlin - * class MyBlockEntity(pos: BlockPos, state: BlockState) - * : NBTBlockEntity(MY_TYPE, pos, state) { - * - * var energy by nbt.intField() - * var label by nbt.stringField { "default" } - * val items by nbt.itemField(9) - * } - * ``` - * - * Saving and loading are handled automatically via [saveAdditional] and [loadAdditional]. - * Call [BlockEntity.setChanged] to push the block entity state to tracking clients. - */ -abstract class NBTBlockEntity(type: BlockEntityType<*>, pos: BlockPos, blockState: BlockState) : BlockEntity( - type, pos, - blockState -), NBTHolder by NBTHolder.create() -{ - override fun loadAdditional(compoundTag: CompoundTag, provider: HolderLookup.Provider) - { - super.loadAdditional(compoundTag, provider) - loadFromTag(compoundTag) - } - - override fun saveAdditional(compoundTag: CompoundTag, provider: HolderLookup.Provider) - { - super.saveAdditional(compoundTag, provider) - saveToTag(compoundTag) - } - - /** Returns the [NBTHolder] sync tag sent to tracking clients; see [NBTHolder.getSyncTag]. */ - override fun getUpdateTag(provider: HolderLookup.Provider): CompoundTag - { - return getSyncTag() - } - - /** Builds the block entity update packet carrying [getUpdateTag]'s data. */ - override fun getUpdatePacket(): Packet? - { - return ClientboundBlockEntityDataPacket.create(this) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CategorySpec.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CategorySpec.kt deleted file mode 100644 index c71c62fbd..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CategorySpec.kt +++ /dev/null @@ -1,31 +0,0 @@ -package net.kernelpanicsoft.archie.config - -import net.minecraft.network.chat.Component -import kotlin.reflect.KClass -import kotlin.reflect.full.isSubclassOf - -/** - * A top-level section of a [ConfigSpec], or a nested subsection of another [CategorySpec]. - * Declared as a nested singleton `object` inside a [ConfigSpec] or a parent [CategorySpec] - - * [ConfigSpec.categories] and [subcategories] both discover their members by reflecting over - * nested objects, so there's nothing to override or register manually. - */ -abstract class CategorySpec(title: Component, id: String = title.string.toSnakeCase()) : DataSpec(title, id) -{ - /** Nested [CategorySpec] objects declared inside this one, for grouping in the UI. */ - val subcategories: List - get() = this::class.nestedClasses - .filterIsInstance>() - .filter { it.isSubclassOf(CategorySpec::class) } - .mapNotNull { klass -> klass.objectInstance } - - override fun init() - { - super.init() - subcategories.forEach { cat -> - types[cat.id] = FieldType.Category(cat) - if (cat.subcategories.isNotEmpty()) - cat.init() - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigContainer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigContainer.kt deleted file mode 100644 index 19eb9b825..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigContainer.kt +++ /dev/null @@ -1,48 +0,0 @@ -package net.kernelpanicsoft.archie.config - -import me.shedaniel.clothconfig2.api.ConfigBuilder -import net.kernelpanicsoft.archie.config.builder.startConfigField -import net.minecraft.client.Minecraft -import net.minecraft.client.gui.components.Button -import net.minecraft.client.gui.screens.Screen -import net.minecraft.network.chat.Component - -/** - * Client-side mirror of a [ConfigContainer]. If the container holds exactly one [ConfigSpec], - * [buildConfigContainer] opens that spec's own screen directly; with more than one, it builds a - * screen listing an "Edit" entry per spec (via `ConfigFieldBuilder`/`ConfigSpecEntry`) that drills - * into that spec's screen. A [ConfigSpec.Type.SERVER] entry is hidden while not in a world. - */ -class ClientConfigContainer(internal var container: ConfigContainer) -{ - fun buildConfigContainer(parent: Screen): Screen - { - val isWorld = Minecraft.getInstance().level != null - val configs = container.configs - if (configs.size == 1) - { - if (!isWorld && configs.first().type == ConfigSpec.Type.SERVER) - return parent - return configs.first().client.buildConfig(parent) - } - return ConfigBuilder.create().apply { - title = container.title - val category = getOrCreateCategory(container.title) - configs.filter { it.type != ConfigSpec.Type.SERVER || isWorld }.forEach { config -> - val entryBuilder = entryBuilder() - category.addEntry( - entryBuilder.startConfigField(config.title, config) - .build() - ) - } - setFallbackCategory(category) - parentScreen = parent - setAfterInitConsumer { configScreen -> - configScreen.removeWidget(configScreen.children().first { it is Button && it.message == Component.empty() }) - } - }.build() - } - - /** Registers this spec's config screen with the platform's mod-list UI, client-side only. */ - fun initClient() = container.mod.registerConfigurationScreen(::buildConfigContainer) -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigSpec.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigSpec.kt deleted file mode 100644 index 1fa7eccb4..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigSpec.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.kernelpanicsoft.archie.config - -import me.shedaniel.clothconfig2.api.ConfigBuilder -import net.minecraft.client.Minecraft -import net.minecraft.client.gui.screens.Screen - -/** Client-side mirror of a [ConfigSpec], built lazily as [ConfigSpec.client]; builds the Cloth Config UI screen. */ -@Suppress("unused") -class ClientConfigSpec(internal var spec: ConfigSpec) -{ - /** - * Builds a fresh Cloth Config [ConfigBuilder] for [spec]: one category per enabled entry of - * [ConfigSpec.categoriesMap]. Saving writes locally via [ConfigSpec.save] for - * [ConfigSpec.Type.COMMON]/[ConfigSpec.Type.CLIENT]/[ConfigSpec.Type.STARTUP] specs, or sends - * the edited config to the server over [ConfigSpec.channel] for [ConfigSpec.Type.SERVER] specs. - */ - fun buildConfig(parent: Screen): Screen - { - return ConfigBuilder.create().apply { - title = spec.title - savingRunnable = Runnable { - when (spec.type) - { - ConfigSpec.Type.COMMON, - ConfigSpec.Type.CLIENT, - ConfigSpec.Type.STARTUP -> spec.save() - ConfigSpec.Type.SERVER -> spec.channel.toServer(spec) - } - } - spec.categoriesMap.values.forEach { value -> - if (value.isEnabled) - { - val entryBuilder = entryBuilder() - val category = getOrCreateCategory(value.title) - - value.client.buildRoot(category, entryBuilder) - } - } - parentScreen = parent - }.build() - } - -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientDataSpec.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientDataSpec.kt deleted file mode 100644 index 2185c3069..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientDataSpec.kt +++ /dev/null @@ -1,1265 +0,0 @@ -package net.kernelpanicsoft.archie.config - -import me.shedaniel.clothconfig2.api.AbstractConfigListEntry -import me.shedaniel.clothconfig2.api.ConfigCategory -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.SubCategoryListEntry -import me.shedaniel.math.Color -import net.kernelpanicsoft.archie.config.builder.* -import net.kernelpanicsoft.archie.util.toMutableEntry -import net.minecraft.core.Registry -import net.minecraft.network.chat.Component -import java.util.function.Consumer -import java.util.function.Supplier -import kotlin.reflect.KClass - -/** - * Client-side mirror of a [DataSpec], built lazily as [DataSpec.client]. Every `boolean`/ - * `int`/... method here is called by its [DataSpec] counterpart (via [DataSpec.onClient]) - * with matching parameters, and queues a [ConfigEntryBuilder]-based entry that reads from and - * writes back into the same backing maps on [spec]. [buildRoot]/[buildSub] then turn the queued - * entries into an actual Cloth Config [ConfigCategory]/[SubCategoryListEntry]. None of this is - * called directly by mod authors - see [DataSpec] for the public DSL. - */ -@Suppress("unused") -class ClientDataSpec(internal val spec: DataSpec) -{ - /** Queued entry builders, appended to in declaration order by each field-registering method below. */ - internal val builders: MutableList AbstractConfigListEntry<*>> = mutableListOf() - - /** Builds this category's entries plus its subcategories (as nested [SubCategoryListEntry]s) directly into the top-level [category]. */ - internal fun buildRoot(category: ConfigCategory, entryBuilder: ConfigEntryBuilder) - { - builders.forEach { builder -> - category.addEntry(entryBuilder.builder()) - } - if (spec is CategorySpec) - { - spec.subcategories.forEach { subcategory -> - category.addEntry(subcategory.client.buildSub(entryBuilder)) - } - } - } - - /** Builds this category (and its subcategories, recursively) as a single [SubCategoryListEntry]. */ - internal fun buildSub(entryBuilder: ConfigEntryBuilder): SubCategoryListEntry - { - val category = entryBuilder.startSubCategory(spec.title) - - builders.forEach { builder -> - category.add(entryBuilder.builder()) - } - - if (spec is CategorySpec) - { - spec.subcategories.forEach { subcategory -> - category.add(subcategory.client.buildSub(entryBuilder)) - } - } - - return category.build() - } - - /** Queues a read-only description entry showing [text], used to render a field's `comment` above it. */ - internal fun comment(text: Component) - { - builders.add { startTextDescription(text).build() } - } - - internal fun boolean( - id: String, - title: Component, - comment: Component? = null, - default: Boolean = false, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.booleans.getOrPut(id) { default } - val ret = startBooleanToggle(title, spec.booleans.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { - spec.booleans[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - - internal fun int( - id: String, - title: Component, - comment: Component? = null, - default: Int = 0, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.ints.getOrPut(id) { default } - val ret = startIntField(title, spec.ints.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { - spec.ints[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - - - } - - internal fun long( - id: String, - title: Component, - comment: Component? = null, - default: Long = 0, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.longs.getOrPut(id) { default } - val ret = startLongField(title, spec.longs.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { - spec.longs[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - - - } - - internal fun intSlider( - id: String, - title: Component, - comment: Component? = null, - min: Int, - max: Int, - default: Int = min + (max - min) / 2, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.ints.getOrPut(id) { default } - val ret = startIntSlider(title, spec.ints.getOrPut(id) { default }, min, max) - .apply { - saveConsumer = Consumer { - spec.ints[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - - - } - - internal fun longSlider( - id: String, - title: Component, - comment: Component? = null, - min: Long, - max: Long, - default: Long = min + (max - min) / 2, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.longs.getOrPut(id) { default } - val ret = startLongSlider(title, spec.longs.getOrPut(id) { default }, min, max) - .apply { - saveConsumer = Consumer { - spec.longs[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - - - } - - internal fun float( - id: String, - title: Component, - comment: Component? = null, - default: Float = 0.0f, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.floats.getOrPut(id) { default } - val ret = startFloatField(title, spec.floats.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { - spec.floats[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - - - } - - internal fun double( - id: String, - title: Component, - comment: Component? = null, - default: Double = 0.0, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.doubles.getOrPut(id) { default } - val ret = startDoubleField(title, spec.doubles.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { - spec.doubles[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - - - } - - internal fun string( - id: String, - title: Component, - comment: Component? = null, - default: String = "", - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.strings.getOrPut(id) { default } - val ret = startStrField(title, spec.strings.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { - spec.strings[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - - - } - - @Suppress("UNCHECKED_CAST") - internal fun spec( - id: String, - title: Component, - comment: Component? = null, - default: T, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - (spec.specs as MutableMap).getOrPut(id) { default } - val ret = startSpecField( - title, - spec.specs.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { - spec.specs[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - - - } - - internal fun registry( - id: String, - title: Component, - comment: Component? = null, - default: T, - registry: Registry, - resetKey: Component? = null, - subclass: KClass? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - val ret = startRegistryField( - title, - registry.get(spec.registries.getOrPut(id) { - registry.getKey( - default - )!! - }) ?: default, - subclass, - registry - ) - .apply { - setSaveConsumer { - spec.registries[id] = registry.getKey(it)!! - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun keycode( - id: String, - title: Component, - comment: Component? = null, - default: CommonKeyCode = CommonKeyCode.unknown, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - val ret = startModifierKeyCodeField( - title, - spec.keycodes.getOrPut(id) { default }.toClient() - ) - .apply { - setModifierSaveConsumer { - spec.keycodes[id] = it.toCommon() - } - setModifierDefaultValue { - default.toClient() - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun color( - id: String, - title: Component, - comment: Component? = null, - alpha: Boolean = false, - default: Color = if (alpha) Color.ofTransparent(-1) else Color.ofOpaque(-1), - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - val ret = startColorField( - title, - spec.colors.getOrPut(id) { default }.color - ) - .apply { - setSaveConsumer2 { - spec.colors[id] = it - } - setDefaultValue2 { - default - } - alphaMode = alpha - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - @Suppress("UNCHECKED_CAST") - internal fun > enumSelector( - id: String, - title: Component, - comment: Component? = null, - kclass: KClass, - default: T, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.enums.getOrPut(id) { default } - val ret = startEnumSelector(title, kclass.java, spec.enums.getOrPut(id) { default } as T) - .apply { - saveConsumer = Consumer { - spec.enums[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - @Suppress("UNCHECKED_CAST") - internal fun selector( - id: String, - title: Component, - comment: Component? = null, - kclass: KClass, - default: T, - entries: Array, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.selectors.getOrPut(id) { default } - val ret = startSelector(title, entries, spec.selectors.getOrPut(id) { default } as T) - .apply { - saveConsumer = Consumer { - spec.selectors[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun intList( - id: String, - title: Component, - comment: Component? = null, - default: List = listOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.intLists.getOrPut(id) { default } - val ret = startIntList( - title, - spec.intLists.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { - spec.intLists[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun longList( - id: String, - title: Component, - comment: Component? = null, - default: List = listOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.longLists.getOrPut(id) { default } - val ret = startLongList( - title, - spec.longLists.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { - spec.longLists[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun floatList( - id: String, - title: Component, - comment: Component? = null, - default: List = listOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.floatLists.getOrPut(id) { default } - val ret = startFloatList( - title, - spec.floatLists.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { - spec.floatLists[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun doubleList( - id: String, - title: Component, - comment: Component? = null, - default: List = listOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.doubleLists.getOrPut(id) { default } - val ret = startDoubleList( - title, - spec.doubleLists.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { - spec.doubleLists[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun stringList( - id: String, - title: Component, - comment: Component? = null, - default: List = listOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.stringLists.getOrPut(id) { default } - val ret = startStrList( - title, - spec.stringLists.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { - spec.stringLists[id] = it - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - @Suppress("UNCHECKED_CAST") - internal fun specList( - id: String, - title: Component, - comment: Component? = null, - default: List = listOf(), - factory: () -> T, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - (spec.specLists as MutableMap>).getOrPut(id) { default } - val ret = startSpecList( - title, - spec.specLists.getOrPut( - id - ) { default }, - factory - ) - .apply { - saveConsumer = Consumer { - spec.specLists[id] = it as List - } - defaultValue = Supplier { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun registryList( - id: String, - title: Component, - comment: Component? = null, - default: List = listOf(), - factory: () -> T, - registry: Registry, - resetKey: Component? = null, - subclass: KClass? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - val ret = startRegistryList( - title, - spec.registryLists.getOrPut( - id - ) { default.map { registry.getKey(it)!! } } - .map { registry.get(it) ?: factory() }, - factory, - subclass, - registry - ) - .apply { - setSaveConsumer { value -> - spec.registryLists[id] = value.map { registry.getKey(it)!! } - } - - setDefaultValue { - default - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun keycodeList( - id: String, - title: Component, - comment: Component? = null, - default: List = listOf(), - factory: () -> CommonKeyCode = CommonKeyCode.Companion::unknown, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.keycodeLists.getOrPut(id) { default } - val ret = startKeycodeList( - title, - spec.keycodeLists.getOrPut( - id - ) { default }.map { it.toClient() } - ) { factory().toClient() } - .apply { - saveConsumer = Consumer { value -> - spec.keycodeLists[id] = value.map { it.toCommon() } - } - defaultValue = Supplier { - default.map { it.toClient() } - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun colorList( - id: String, - title: Component, - comment: Component? = null, - alpha: Boolean = false, - default: List = listOf(), - factory: () -> Color = { if (alpha) Color.ofTransparent(-1) else Color.ofOpaque(-1) }, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - val ret = startColorList( - title, - spec.colorLists.getOrPut(id) { default }, - factory - ) - .apply { - setSaveConsumer { - spec.colorLists[id] = if (alpha) - it.map(Color::ofTransparent) - else - it.map(Color::ofOpaque) - } - setDefaultValue { - default.map { it.color } - } - alphaMode = alpha - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun intMap( - id: String, - title: Component, - comment: Component? = null, - default: Map = mapOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.intMaps.getOrPut(id) { default } - val ret = startIntMap( - title, - spec.intMaps.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { value -> - spec.intMaps[id] = value.associate { it.toPair() } - } - defaultValue = Supplier { - default.entries.toList().map { it.toMutableEntry() } - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun longMap( - id: String, - title: Component, - comment: Component? = null, - default: Map = mapOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.longMaps.getOrPut(id) { default } - val ret = startLongMap( - title, - spec.longMaps.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { value -> - spec.longMaps[id] = value.associate { it.toPair() } - } - defaultValue = Supplier { - default.entries.toList().map { it.toMutableEntry() } - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun floatMap( - id: String, - title: Component, - comment: Component? = null, - default: Map = mapOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.floatMaps.getOrPut(id) { default } - val ret = startFloatMap( - title, - spec.floatMaps.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { value -> - spec.floatMaps[id] = value.associate { it.toPair() } - } - defaultValue = Supplier { - default.entries.toList().map { it.toMutableEntry() } - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun doubleMap( - id: String, - title: Component, - comment: Component? = null, - default: Map = mapOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.doubleMaps.getOrPut(id) { default } - val ret = startDoubleMap( - title, - spec.doubleMaps.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { value -> - spec.doubleMaps[id] = value.associate { it.toPair() } - } - defaultValue = Supplier { - default.entries.toList().map { it.toMutableEntry() } - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun stringMap( - id: String, - title: Component, - comment: Component? = null, - default: Map = mapOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.stringMaps.getOrPut(id) { default } - val ret = startStrMap( - title, - spec.stringMaps.getOrPut(id) { default }) - .apply { - saveConsumer = Consumer { value -> - spec.stringMaps[id] = value.associate { it.toPair() } - } - defaultValue = Supplier { - default.entries.toList().map { it.toMutableEntry() } - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - @Suppress("UNCHECKED_CAST") - internal fun specMap( - id: String, - title: Component, - comment: Component? = null, - default: Map = mapOf(), - factory: () -> T, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - (spec.specMaps as MutableMap>).getOrPut(id) { default } - val ret = startSpecMap( - title, - spec.specMaps.getOrPut( - id - ) { default }, - factory - ) - .apply { - saveConsumer = Consumer { value -> - spec.specMaps[id] = value.associate { it.toPair() } - } - defaultValue = Supplier { - default.entries.toList().map { it.toMutableEntry() } - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun registryMap( - id: String, - title: Component, - comment: Component? = null, - default: Map = mapOf(), - factory: () -> T, - registry: Registry, - resetKey: Component? = null, - subclass: KClass? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - val ret = startRegistryMap( - title, - spec.registryMaps.getOrPut( - id - ) { default.mapValues { registry.getKey(it.value)!! } } - .mapValues { - registry.get(it.value) ?: factory() - }, - factory, - subclass, - registry - ) - .apply { - setSaveConsumer { value -> - spec.registryMaps[id] = - value.associate { it.toPair() }.mapValues { registry.getKey(it.value)!! } - } - - setDefaultValue { - default.toList().map { it.toMutableEntry() } - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun keycodeMap( - id: String, - title: Component, - comment: Component? = null, - default: Map = mapOf(), - factory: () -> CommonKeyCode = CommonKeyCode.Companion::unknown, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - spec.keycodeMaps.getOrPut(id) { default } - val ret = startKeycodeMap( - title, - spec.keycodeMaps.getOrPut(id) { default }.mapValues { it.value.toClient() } - ) { factory().toClient() } - .apply { - saveConsumer = Consumer { value -> - spec.keycodeMaps[id] = - value.associate { it.toPair() }.mapValues { it.value.toCommon() } - } - defaultValue = Supplier { - default.mapValues { it.value.toClient() }.entries.toList().map { it.toMutableEntry() } - } - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } - - internal fun colorMap( - id: String, - title: Component, - comment: Component? = null, - alpha: Boolean = false, - default: Map = mapOf(), - factory: () -> Color = { if (alpha) Color.ofTransparent(-1) else Color.ofOpaque(-1) }, - resetKey: Component? = null, - needsRestart: Boolean = false - ) - { - if (comment != null) - { - comment(comment) - } - builders.add { - val reset = resetButtonKey - resetButtonKey = resetKey ?: resetButtonKey - val ret = startColorMap( - title, - spec.colorMaps.getOrPut( - id - ) { default }, - factory - ) - .apply { - setSaveConsumer { value -> - spec.colorMaps[id] = if (alpha) - value.associate { it.toPair() }.mapValues { Color.ofTransparent(it.value) } - else - value.associate { it.toPair() }.mapValues { Color.ofOpaque(it.value) } - } - setDefaultValue { - default.mapValues { it.value.color }.toList().map { it.toMutableEntry() } - } - alphaMode = alpha - requireRestart(needsRestart) - } - .build() - resetButtonKey = reset - ret - } - } -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CommonKeyCode.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CommonKeyCode.kt deleted file mode 100644 index f4cb133a3..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CommonKeyCode.kt +++ /dev/null @@ -1,84 +0,0 @@ -package net.kernelpanicsoft.archie.config - -import com.mojang.blaze3d.platform.InputConstants -import kotlinx.serialization.Serializable -import me.shedaniel.clothconfig2.api.Modifier -import me.shedaniel.clothconfig2.api.ModifierKeyCode - -/** - * A serializable, client-independent representation of a keybind, used by [DataSpec.keycode] - * fields so config files don't depend on Cloth Config's [ModifierKeyCode]. Convert to/from the - * client type with [toClient]/[toCommon]. - */ -@Serializable -data class CommonKeyCode(val type: Type, val key: Int, val modifiers: Set) -{ - constructor(type: Type, key: Int, vararg modifiers: Modifier) : this(type, key, modifiers.toSet()) - - /** Which [InputConstants] key space [key] is a code in. */ - @Serializable - enum class Type - { - KEYSYM, - SCANCODE, - MOUSE; - } - - /** A modifier key held alongside the base [key]. */ - @Serializable - enum class Modifier - { - ALT, - CONTROL, - SHIFT - } - - companion object - { - /** Sentinel for "no key bound". */ - val unknown: CommonKeyCode = CommonKeyCode(Type.KEYSYM, -1) - } -} - -private val CommonKeyCode.modifier: Modifier - get() - { - var alt = false - var control = false - var shift = false - modifiers.forEach { - when (it) - { - CommonKeyCode.Modifier.ALT -> alt = true - CommonKeyCode.Modifier.CONTROL -> control = true - CommonKeyCode.Modifier.SHIFT -> shift = true - } - } - return Modifier.of(alt, control, shift) - } - -/** Converts this to Cloth Config's client-side [ModifierKeyCode]. */ -fun CommonKeyCode.toClient(): ModifierKeyCode = when (type) -{ - CommonKeyCode.Type.KEYSYM -> ModifierKeyCode.of(InputConstants.Type.KEYSYM.getOrCreate(key), modifier) - CommonKeyCode.Type.SCANCODE -> ModifierKeyCode.of(InputConstants.Type.SCANCODE.getOrCreate(key), modifier) - CommonKeyCode.Type.MOUSE -> ModifierKeyCode.of(InputConstants.Type.MOUSE.getOrCreate(key), modifier) -} - -private val ModifierKeyCode.modifiers: Set - get() = buildSet { - modifier.apply { - if (hasAlt()) add(CommonKeyCode.Modifier.ALT) - if (hasControl()) add(CommonKeyCode.Modifier.CONTROL) - if (hasShift()) add(CommonKeyCode.Modifier.SHIFT) - } - } - -/** Converts a Cloth Config [ModifierKeyCode] to the serializable [CommonKeyCode]. */ -@Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") -fun ModifierKeyCode.toCommon(): CommonKeyCode = when (type) -{ - InputConstants.Type.KEYSYM -> CommonKeyCode(CommonKeyCode.Type.KEYSYM, keyCode.value, modifiers) - InputConstants.Type.SCANCODE -> CommonKeyCode(CommonKeyCode.Type.SCANCODE, keyCode.value, modifiers) - InputConstants.Type.MOUSE -> CommonKeyCode(CommonKeyCode.Type.MOUSE, keyCode.value, modifiers) -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigContainer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigContainer.kt deleted file mode 100644 index 62c620040..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigContainer.kt +++ /dev/null @@ -1,60 +0,0 @@ -package net.kernelpanicsoft.archie.config - -import dev.architectury.platform.Mod -import dev.architectury.platform.Platform -import net.kernelpanicsoft.archie.APlatform -import net.kernelpanicsoft.archie.util.onClient -import net.minecraft.network.chat.Component -import kotlin.reflect.KClass -import kotlin.reflect.full.isSubclassOf - -/** - * The single per-mod root of the config system, declared as a singleton `object` holding one or - * more nested [ConfigSpec] objects: - * ```kotlin - * object Config : ConfigContainer(MyMod.MOD) { - * object Common : ConfigSpec.Common(MyMod.MOD) { ... } - * object Client : ConfigSpec.Client(MyMod.MOD) { ... } - * } - * ``` - * Call [init] once during common mod init, on both physical sides; it initializes every nested - * [ConfigSpec] (loading/creating its file per its [ConfigSpec.predicate] timing) and, on the - * client, builds and registers the merged Cloth Config UI screen via [ClientConfigContainer]. - * - * @param mod The owning mod, used to derive each nested [ConfigSpec]'s default filename. - * @param title Display title used for the container's screen when it holds more than one - * [ConfigSpec]. Defaults to [mod]'s name. - */ -abstract class ConfigContainer(val mod: Mod, val title: Component = Component.literal(mod.name)) -{ - /** Client-side mirror of this container, used to build the merged Cloth Config UI screen. */ - internal val client by lazy { ClientConfigContainer(this) } - - /** Nested [ConfigSpec] objects declared inside this container. */ - val configs: List - get() = this::class.nestedClasses - .filterIsInstance>() - .filter { it.isSubclassOf(ConfigSpec::class) } - .mapNotNull { klass -> klass.objectInstance } - - /** Initializes every nested [ConfigSpec] and, on the client, registers the config UI screen. */ - fun init() - { - configs.forEach(ConfigSpec::init) - onClient { initClient() } - } - - internal fun initClient() - { - // Cloth Config's mod id differs by loader: Fabric allows hyphens ("cloth-config"), while - // NeoForge's mod id charset doesn't, so its variant registers as "cloth_config" instead. - val clothConfigModId = when (val platform = APlatform.platform) - { - "fabric" -> "cloth-config" - "neoforge" -> "cloth_config" - else -> throw UnsupportedOperationException("Unsupported platform: $platform") - } - if (Platform.isModLoaded(clothConfigModId)) - client.initClient() - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigSpec.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigSpec.kt deleted file mode 100644 index 2b4c5916c..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigSpec.kt +++ /dev/null @@ -1,313 +0,0 @@ -package net.kernelpanicsoft.archie.config - -import dev.architectury.event.events.client.ClientLifecycleEvent -import dev.architectury.event.events.client.ClientPlayerEvent -import dev.architectury.event.events.common.LifecycleEvent -import dev.architectury.event.events.common.PlayerEvent -import net.kernelpanicsoft.archie.config.serializer.Json5ConfigSerializer -import net.kernelpanicsoft.archie.config.serializer.TomlConfigSerializer -import net.kernelpanicsoft.archie.APlatform -import net.kernelpanicsoft.archie.util.onClient -import dev.architectury.platform.Mod -import dev.architectury.platform.Platform -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.* -import net.kernelpanicsoft.archie.config.ConfigSpec.Server.Companion.CONFIG_DIR -import net.kernelpanicsoft.archie.networking.NetworkChannel -import net.kernelpanicsoft.archie.serialization.SerializationManager -import net.kernelpanicsoft.archie.util.foldEnv -import net.kernelpanicsoft.archie.util.isClient -import net.kernelpanicsoft.archie.util.rem -import net.minecraft.network.chat.Component -import net.minecraft.world.level.storage.LevelResource -import java.nio.file.Path -import java.util.function.Predicate -import kotlin.reflect.KClass -import kotlin.reflect.full.isSubclassOf - -/** - * The root of a mod's config. [ConfigSpec] is sealed - declare one or more nested singleton - * `object`s subclassing [Common], [Client], [Server], or [Startup] (nested inside a - * [ConfigContainer]) depending on when the config should load and whether it should sync. - * [categories] are discovered automatically from nested [CategorySpec] objects - no need to - * override anything: - * ```kotlin - * object Config : ConfigContainer(MyMod.MOD) { - * object MyConfig : ConfigSpec.Common(MyMod.MOD, Component.literal("My Config")) { - * object General : CategorySpec(Component.literal("General"), "general") { ... } - * object Advanced : CategorySpec(Component.literal("Advanced"), "advanced") { ... } - * } - * } - * ``` - * Call [ConfigContainer.init] once during common mod init (on both physical sides); it loads (or - * creates) the config file(s) and, on the client, also registers the Cloth Config UI screen(s). - * There is no separate client-side init step to call. - * - * @param mod The owning mod, used to derive the default [filename] and locate the config - * directory. - * @param title Display title of the config, shown as the Cloth Config screen title. - * @param id Unique identifier for this config, used to derive the default [filename] and - * register the network channel. Defaults to the snake-cased [title]. - */ -@Suppress("unused") -sealed class ConfigSpec(val type: Type, val mod: Mod, val title: Component, val id: String = title.string.toSnakeCase()) -{ - internal val channel = NetworkChannel(mod % id) - /** Client-side mirror of this spec, used to build the Cloth Config UI screen. */ - internal val client by lazy { ClientConfigSpec(this) } - - open val synchronized = false - - /** Top-level sections of this config. */ - val categories: List - get() = this::class.nestedClasses - .filterIsInstance>() - .filter { it.isSubclassOf(CategorySpec::class) } - .mapNotNull { klass -> klass.objectInstance } - - /** [categories] indexed by [DataSpec.id]. */ - internal val categoriesMap: Map by lazy { - categories.associateBy { it.id } - } - - /** - * Serializer used to read/write the config file. Defaults per-platform: JSON5 on Fabric, - * TOML on NeoForge. Override to force a specific format regardless of platform. - */ - protected open val fileSerializer: IConfigSerializer = when (val platform = APlatform.platform) - { - "fabric" -> Json5ConfigSerializer - "neoforge" -> TomlConfigSerializer - else -> throw UnsupportedOperationException("Unsupported platform: $platform") - } - - /** Path of the config file, relative to the game's config directory, without extension. */ - open val filename: String = "${mod.modId}/${id}" - - /** Whether [load] has run at least once. */ - var isLoaded: Boolean = false - protected set - - var configFolder: Path = Platform.getConfigFolder() - protected set - - private var isEventsRegistered: Boolean = false - - abstract val predicate: () -> Boolean - - /** - * Registers all [categories] (and their subcategories) and, for a [synchronized] spec, - * registers this spec's [NetworkChannel] plus the player-join/quit listeners that push it to - * joining clients and reset [isLoaded] on disconnect. - * - * This does **not** load the config file or touch the client UI - each of [Common], [Client], - * [Server], and [Startup] overrides this to additionally register the lifecycle event - * (documented on that subclass) that calls [load] at the right time. The client-side settings - * screen is built and registered separately, once every nested spec in the container is - * initialized, by [ConfigContainer.initClient]. Call [ConfigContainer.init], not this method - * directly. - */ - open fun init() - { - SerializationManager { - module { - contextual(this@ConfigSpec::class) { - serializer - } - } - } - categoriesMap.values.forEach { cat -> - cat.init() - } - if (synchronized && !isEventsRegistered) - { - channel.configServerbound(this) - channel.configClientbound(this) - channel.register() - foldEnv( - client = { - ClientPlayerEvent.CLIENT_PLAYER_QUIT.register { - this.isLoaded = false - } - }, - server = { - PlayerEvent.PLAYER_JOIN.register { player -> - if (!player.server.isSingleplayer) - channel.toPlayer(player, this) - - } - } - ) - } - isEventsRegistered = true - } - - - /** Reads the config file via [fileSerializer], creating it with defaults if absent, and marks [isLoaded]. */ - fun load() = fileSerializer.load(this, configFolder).also { isLoaded = true } - - /** Writes the current values of every field in [categories] to the config file via [fileSerializer]. */ - fun save() = fileSerializer.save(this, configFolder) - - /** Serializes/deserializes a [ConfigSpec] by delegating each entry of [categoriesMap] to its own [DataSpec.serializer]. */ - internal class ConfigSerializer(val factory: () -> ConfigSpec) : KSerializer - { - override val descriptor: SerialDescriptor by lazy { - with(factory()) - { - buildClassSerialDescriptor(title.string) - { - categoriesMap.forEach { (key, value) -> - element(key, value.serializer.descriptor) - } - } - } - } - - override fun deserialize(decoder: Decoder): ConfigSpec - { - return decoder.decodeStructure(descriptor) - { - val spec = factory() - with(spec) - { - while (true) - { - when (val index = decodeElementIndex(descriptor)) - { - in categoriesMap.entries.indices -> - { - val (_, value) = categoriesMap.entries.toList()[index] - decodeSerializableElement(descriptor, index, value.serializer) - } - - CompositeDecoder.DECODE_DONE -> break - else -> error("Unexpected index: $index") - } - } - } - spec - } - } - - override fun serialize(encoder: Encoder, value: ConfigSpec) - { - encoder.encodeStructure(descriptor) - { - value.categoriesMap.entries.forEachIndexed { index, (_, value) -> - encodeSerializableElement(descriptor, index, value.serializer, value) - } - } - } - } - - enum class Type - { - COMMON, - CLIENT, - SERVER, - STARTUP - } - - abstract class Common(mod: Mod, title: Component = Component.literal("Common"), id: String = title.string.toSnakeCase()) : ConfigSpec( - Type.COMMON, - mod, - title, - id, - ) - { - private var isEventsRegistered: Boolean = false - - override fun init() - { - super.init() - if (!isEventsRegistered) - { - LifecycleEvent.SETUP.register { - load() - } - } - isEventsRegistered = true - } - - override val predicate: () -> Boolean = { isLoaded } - } - - abstract class Client(mod: Mod, title: Component = Component.literal("Client"), id: String = title.string.toSnakeCase()) : ConfigSpec( - Type.CLIENT, - mod, - title, - id, - ) - { - private var isEventsRegistered: Boolean = false - - override fun init() - { - super.init() - if (!isEventsRegistered) - { - onClient { - ClientLifecycleEvent.CLIENT_SETUP.register { - load() - } - } - } - isEventsRegistered = true - } - - override val predicate: () -> Boolean = { isLoaded && isClient } - } - - abstract class Server(mod: Mod, title: Component = Component.literal("Server"), id: String = title.string.toSnakeCase()) : ConfigSpec( - Type.SERVER, - mod, - title, - id, - ) { - private var isEventsRegistered: Boolean = false - - override val synchronized: Boolean = true - - override fun init() - { - super.init() - if (!isEventsRegistered) - { - LifecycleEvent.SERVER_BEFORE_START.register { server -> - configFolder = server.getWorldPath(CONFIG_DIR) - load() - } - } - isEventsRegistered = true - } - - override val predicate: () -> Boolean = { isLoaded } - - companion object - { - private val CONFIG_DIR = LevelResource("serverconfig") - } - } - - abstract class Startup(mod: Mod, title: Component = Component.literal("Startup"), id: String = title.string.toSnakeCase()) : ConfigSpec( - Type.STARTUP, - mod, - title, - id, - ) - { - override fun init() - { - super.init() - load() - } - - override val predicate: () -> Boolean = { isLoaded } - } - - /** [KSerializer] for this spec, used by [fileSerializer] to read/write the config file. */ - internal val serializer by lazy { ConfigSerializer { this } } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/DataSpec.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/DataSpec.kt deleted file mode 100644 index 522648f89..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/DataSpec.kt +++ /dev/null @@ -1,1673 +0,0 @@ -package net.kernelpanicsoft.archie.config - -import io.github.xn32.json5k.SerialComment -import kotlinx.serialization.KSerializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.* -import me.shedaniel.math.Color -import net.kernelpanicsoft.archie.serialization.SerializationManager -import net.kernelpanicsoft.archie.util.onClient -import net.minecraft.core.Registry -import net.minecraft.network.chat.Component -import net.minecraft.resources.ResourceLocation -import net.peanuuutz.tomlkt.TomlComment -import kotlin.properties.PropertyDelegateProvider -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KClass -import kotlin.reflect.KProperty - -/** - * A group of config fields, declared by subclassing this and adding fields with the - * `by boolean(...)`, `by int(...)`, etc. delegates below. - * - * Each delegate call registers a field under an id derived from the *property* name - * (snake_cased), stores the field's [FieldType] and default value, and - on the client - mirrors - * the field into [ClientDataSpec] so Cloth Config can render it. The delegate itself just - * reads the current value back out of this category's backing maps, so config values are read - * with plain property access (e.g. `MyConfig.General.enableFeature`). - * - * [CategorySpec] is a [DataSpec] subclass used for a [ConfigSpec]'s top-level sections (and - * supports nested [CategorySpec.subcategories] for grouping in the UI). Subclass [DataSpec] - * directly instead for non-category values embedded as fields via `spec`/`specList`/`specMap`. - * - * @param title Display title shown in the Cloth Config UI. - * @param id Stable identifier used as this category's key in its parent and in the serialized - * file. Defaults to the snake_cased [title]. - */ -@Suppress("unused") -abstract class DataSpec(val title: Component, val id: String = title.string.toSnakeCase()) -{ - /** Client-side mirror of this category, used to build the Cloth Config UI. */ - val client by lazy { ClientDataSpec(this) } - - internal val types: MutableMap> = linkedMapOf() - internal val comments: MutableMap = mutableMapOf() - internal val booleans: MutableMap = mutableMapOf() - internal val ints: MutableMap = mutableMapOf() - internal val longs: MutableMap = mutableMapOf() - internal val floats: MutableMap = mutableMapOf() - internal val doubles: MutableMap = mutableMapOf() - internal val strings: MutableMap = mutableMapOf() - internal val specs: MutableMap = mutableMapOf() - internal val registries: MutableMap = mutableMapOf() - internal val keycodes: MutableMap = mutableMapOf() - internal val colors: MutableMap = mutableMapOf() - internal val enums: MutableMap> = mutableMapOf() - internal val selectors: MutableMap = mutableMapOf() - internal val intLists: MutableMap> = mutableMapOf() - internal val longLists: MutableMap> = mutableMapOf() - internal val floatLists: MutableMap> = mutableMapOf() - internal val doubleLists: MutableMap> = mutableMapOf() - internal val stringLists: MutableMap> = mutableMapOf() - internal val specLists: MutableMap> = mutableMapOf() - internal val registryLists: MutableMap> = mutableMapOf() - internal val keycodeLists: MutableMap> = mutableMapOf() - internal val colorLists: MutableMap> = mutableMapOf() - internal val intMaps: MutableMap> = mutableMapOf() - internal val longMaps: MutableMap> = mutableMapOf() - internal val floatMaps: MutableMap> = mutableMapOf() - internal val doubleMaps: MutableMap> = mutableMapOf() - internal val stringMaps: MutableMap> = mutableMapOf() - internal val specMaps: MutableMap> = mutableMapOf() - internal val registryMaps: MutableMap> = mutableMapOf() - internal val keycodeMaps: MutableMap> = mutableMapOf() - internal val colorMaps: MutableMap> = mutableMapOf() - - /** - * Whether this category is currently active. When `false`, Cloth Config hides/disables the - * category's fields in the UI. Override with a `get()` that reads another field (e.g. a - * parent toggle) to make this category conditional. - */ - open val isEnabled: Boolean = true - - /** Registers [subcategories] as fields on this category, recursively. */ - internal open fun init() - { - SerializationManager { - module { - contextual(this@DataSpec::class) { - serializer - } - } - } - } - - internal var accessPredicate: () -> Boolean = { false } - - /** - * Declares a `Boolean` config field, e.g. `val/var enableFeature by boolean(...)`. - * - * The field's id is the delegated property's name, snake_cased. On the client, the field is - * also registered with [ClientDataSpec] so it renders as a toggle in the Cloth Config UI. - * Writing back to this delegate will update the current value, and reading from it will - * return the current value. To persist changes, you must call [ConfigSpec.save] or the value will not be saved to disk. - * - * @param title Display title shown in the Cloth Config UI. - * @param comment Optional comment written next to the field in the serialized file (JSON5/TOML) - * and used as the UI tooltip. - * @param default Value used until a stored/loaded value overrides it. - * @param resetKey Optional label for the UI's "reset to default" control. - * @return A read/write property delegate exposing the field's current value. - */ - protected fun boolean( - title: Component, - comment: Component? = null, - default: Boolean = false, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.boolean(id, title, comment, default, resetKey) - } - types[id] = FieldType.Boolean - booleans.putIfAbsent(id, default) - object : ReadWriteProperty - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Boolean = booleans.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Boolean - ) { booleans[id] = value } - } - } - - /** Declares an `Int` config field. See [boolean] for parameter semantics. */ - protected fun int( - title: Component, - comment: Component? = null, - default: Int = 0, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.int(id, title, comment, default, resetKey) - } - types[id] = FieldType.Int - ints.putIfAbsent(id, default) - object : ReadWriteProperty - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Int = ints.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Int - ) { ints[id] = value } - } - } - - /** Declares a `Long` config field. See [boolean] for parameter semantics. */ - protected fun long( - title: Component, - comment: Component? = null, - default: Long = 0, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.long(id, title, comment, default, resetKey) - } - types[id] = FieldType.Long - longs.putIfAbsent(id, default) - object : ReadWriteProperty - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Long = longs.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Long - ) { longs[id] = value } - } - } - - /** - * Declares an `Int` config field rendered as a slider bounded by [min]/[max]. See [boolean] - * for the remaining parameter semantics. - * - * @param min Minimum value the slider allows. - * @param max Maximum value the slider allows. - * @param default Defaults to the midpoint of [min] and [max] if not given. - */ - protected fun intSlider( - title: Component, - comment: Component? = null, - min: Int, - max: Int, - default: Int = min + (max - min) / 2, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.intSlider(id, title, comment, min, max, default, resetKey) - } - types[id] = FieldType.Int - ints.putIfAbsent(id, default) - object : ReadWriteProperty - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Int = ints.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Int - ) { ints[id] = value } - } - } - - /** Declares a `Long` config field rendered as a slider. See [intSlider] for parameter semantics. */ - protected fun longSlider( - title: Component, - comment: Component? = null, - min: Long, - max: Long, - default: Long = min + (max - min) / 2, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.longSlider(id, title, comment, min, max, default, resetKey) - } - types[id] = FieldType.Long - longs.putIfAbsent(id, default) - object : ReadWriteProperty - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Long = longs.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Long - ) { longs[id] = value } - } - } - - /** Declares a `Float` config field. See [boolean] for parameter semantics. */ - protected fun float( - title: Component, - comment: Component? = null, - default: Float = 0.0f, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.float(id, title, comment, default, resetKey) - } - types[id] = FieldType.Float - floats.putIfAbsent(id, default) - object : ReadWriteProperty - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Float = floats.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Float - ) { floats[id] = value } - } - } - - /** Declares a `Double` config field. See [boolean] for parameter semantics. */ - protected fun double( - title: Component, - comment: Component? = null, - default: Double = 0.0, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.double(id, title, comment, default, resetKey) - } - types[id] = FieldType.Double - doubles.putIfAbsent(id, default) - object : ReadWriteProperty - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Double = doubles.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Double - ) { doubles[id] = value } - } - } - - /** Declares a `String` config field. See [boolean] for parameter semantics. */ - protected fun string( - title: Component, - comment: Component? = null, - default: String = "", - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.string(id, title, comment, default, resetKey) - } - types[id] = FieldType.String - strings.putIfAbsent(id, default) - object : ReadWriteProperty - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): String = strings.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: String - ) { strings[id] = value } - } - } - - /** - * Declares a field that embeds another [DataSpec] as a nested, serializable section. See - * [boolean] for the remaining parameter semantics. - * - * @param default Instance used until a stored/loaded value overrides it. Never mutated in - * place - deserialization always builds a fresh instance via [factory]. - * @param factory Creates a new instance of the nested spec; used by the deserializer so - * loading a saved value never mutates [default]. - */ - @Suppress("UNCHECKED_CAST") - protected fun spec( - title: Component, - comment: Component? = null, - default: T, - factory: () -> T, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.spec(id, title, comment, default, resetKey) - } - types[id] = FieldType.Spec { factory() } as FieldType - (specs as MutableMap).putIfAbsent(id, default) - object : ReadWriteProperty - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): T = (specs as MutableMap).getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: T - ) { specs[id] = value } - } - } - - /** - * Declares a field whose value is an entry of a vanilla [Registry], stored as the entry's - * [ResourceLocation] key. See [boolean] for the remaining parameter semantics. - * - * @param registry Registry the field's value is looked up in. - * @param subclass If given, narrows the entries offered in the UI to this runtime type; the - * stored key is still resolved against the full [registry]. - */ - protected fun registry( - title: Component, - comment: Component? = null, - default: T, - registry: Registry, - resetKey: Component? = null, - subclass: KClass? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.registry(id, title, comment, default, registry, resetKey, subclass) - } - types[id] = FieldType.Registry - registries.putIfAbsent(id, registry.getKey(default)!!) - object : ReadWriteProperty - { - @Suppress("UNCHECKED_CAST") - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): R = (registry.get(registries.getOrPut(id) { - registry.getKey(default)!! - }) ?: default) as R - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: R - ) { registries[id] = registry.getKey(value)!! } - } - } - - /** - * Declares a [CommonKeyCode] config field, rendered as a keybind picker. See [boolean] for - * the remaining parameter semantics. - */ - protected fun keycode( - title: Component, - comment: Component? = null, - default: CommonKeyCode = CommonKeyCode.unknown, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.keycode(id, title, comment, default, resetKey) - } - types[id] = FieldType.KeyCode - keycodes.putIfAbsent(id, default) - object : ReadWriteProperty - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): CommonKeyCode = keycodes.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: CommonKeyCode - ) { keycodes[id] = value } - } - } - - /** - * Declares a [Color] (ARGB) config field, rendered as a color picker. See [boolean] for the - * remaining parameter semantics. - * - * @param alpha Whether the picker allows editing the alpha channel. - */ - protected fun color( - title: Component, - comment: Component? = null, - alpha: Boolean = false, - default: Color = if (alpha) Color.ofTransparent(-1) else Color.ofOpaque(-1), - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.color(id, title, comment, alpha, default, resetKey) - } - types[id] = FieldType.Color - colors.putIfAbsent(id, default) - object : ReadWriteProperty - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Color = colors.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Color - ) { colors[id] = value } - } - } - - /** - * Declares a field whose value is one entry of the enum [kclass], rendered as a cycling - * selector over all of the enum's entries. See [boolean] for the remaining parameter - * semantics. - * - * @param kclass The enum type to select from. - */ - @Suppress("UNCHECKED_CAST") - protected fun > enumSelector( - title: Component, - comment: Component? = null, - kclass: KClass, - default: T, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.enumSelector(id, title, comment, kclass, default, resetKey) - } - types[id] = FieldType.EnumSelector(kclass) - enums.putIfAbsent(id, default) - object : ReadWriteProperty - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): T = enums.getOrPut(id) { default } as T - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: T - ) { enums[id] = value } - } - } - - /** - * Declares a field whose value is one of an arbitrary fixed set of [entries], rendered as a - * cycling selector. Unlike [enumSelector], the value type isn't required to be an `enum - * class`. See [boolean] for the remaining parameter semantics. - * - * @param kclass Runtime type of the selectable values. - * @param entries The fixed set of values the selector cycles through. - */ - @Suppress("UNCHECKED_CAST") - protected fun selector( - title: Component, - comment: Component? = null, - kclass: KClass, - default: T, - entries: Array, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.selector(id, title, comment, kclass, default, entries, resetKey) - } - types[id] = FieldType.Selector(kclass) - selectors.putIfAbsent(id, default) - object : ReadWriteProperty - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): T = selectors.getOrPut(id) { default } as T - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: T - ) { selectors[id] = value } - } - } - - /** Declares a `List` config field. See [boolean] for parameter semantics. */ - protected fun intList( - title: Component, - comment: Component? = null, - default: List = listOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.intList(id, title, comment, default, resetKey) - } - types[id] = FieldType.IntList - intLists.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): List = intLists.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: List - ) { intLists[id] = value } - } - } - - /** Declares a `List` config field. See [boolean] for parameter semantics. */ - protected fun longList( - title: Component, - comment: Component? = null, - default: List = listOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.longList(id, title, comment, default, resetKey) - } - types[id] = FieldType.LongList - longLists.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): List = longLists.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: List - ) { longLists[id] = value } - } - } - - /** Declares a `List` config field. See [boolean] for parameter semantics. */ - protected fun floatList( - title: Component, - comment: Component? = null, - default: List = listOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.floatList(id, title, comment, default, resetKey) - } - types[id] = FieldType.FloatList - floatLists.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): List = floatLists.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: List - ) { floatLists[id] = value } - } - } - - /** Declares a `List` config field. See [boolean] for parameter semantics. */ - protected fun doubleList( - title: Component, - comment: Component? = null, - default: List = listOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.doubleList(id, title, comment, default, resetKey) - } - types[id] = FieldType.DoubleList - doubleLists.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): List = doubleLists.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: List - ) { doubleLists[id] = value } - } - } - - /** Declares a `List` config field. See [boolean] for parameter semantics. */ - protected fun stringList( - title: Component, - comment: Component? = null, - default: List = listOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.stringList(id, title, comment, default, resetKey) - } - types[id] = FieldType.StringList - stringLists.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): List = stringLists.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: List - ) { stringLists[id] = value } - } - } - - /** Declares a `List` of nested [DataSpec] entries. See [spec] for parameter semantics. */ - @Suppress("UNCHECKED_CAST") - protected fun specList( - title: Component, - comment: Component? = null, - default: List = listOf(), - factory: () -> T, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.specList(id, title, comment, default, factory, resetKey) - } - types[id] = FieldType.SpecList(factory) as FieldType> - specLists.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): List = specLists.getOrPut(id) { default } as List - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: List - ) { specLists[id] = value } - } - } - - /** - * Declares a `List` of [Registry] entries, stored as a list of [ResourceLocation] keys. See - * [registry] for parameter semantics. - * - * @param factory Used to produce a fallback value if a stored key no longer resolves in - * [registry] (e.g. the entry was removed by a datapack/mod update). - */ - protected fun registryList( - title: Component, - comment: Component? = null, - default: List = listOf(), - factory: () -> T, - registry: Registry, - resetKey: Component? = null, - subclass: KClass? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.registryList(id, title, comment, default, factory, registry, resetKey, subclass) - } - types[id] = FieldType.RegistryList - registryLists.putIfAbsent(id, default.map { registry.getKey(it)!! }) - object : ReadWriteProperty> - { - @Suppress("UNCHECKED_CAST") - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): List = registryLists.getOrPut(id) { - default.map { registry.getKey(it)!! } - }.map { (registry.get(it) ?: factory()) as R } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: List - ) { registryLists[id] = value.map { registry.getKey(it)!! } } - } - } - - /** Declares a `List` config field. See [keycode] for parameter semantics. */ - protected fun keycodeList( - title: Component, - comment: Component? = null, - default: List = listOf(), - factory: () -> CommonKeyCode = CommonKeyCode.Companion::unknown, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.keycodeList(id, title, comment, default, factory, resetKey) - } - types[id] = FieldType.KeyCodeList - keycodeLists.putIfAbsent(id, default ) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): List = keycodeLists.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: List - ) { keycodeLists[id] = value } - } - } - - /** Declares a `List` config field. See [color] for parameter semantics. */ - protected fun colorList( - title: Component, - comment: Component? = null, - alpha: Boolean = false, - default: List = listOf(), - factory: () -> Color = { if (alpha) Color.ofTransparent(-1) else Color.ofOpaque(-1) }, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.colorList(id, title, comment, alpha, default, factory, resetKey) - } - types[id] = FieldType.ColorList - colorLists.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): List = colorLists.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: List - ) { colorLists[id] = value } - } - } - - /** Declares a `Map` config field. See [boolean] for parameter semantics. */ - protected fun intMap( - title: Component, - comment: Component? = null, - default: Map = mapOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.intMap(id, title, comment, default, resetKey) - } - types[id] = FieldType.IntMap - intMaps.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Map = intMaps.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Map - ) { intMaps[id] = value } - } - } - - /** Declares a `Map` config field. See [boolean] for parameter semantics. */ - protected fun longMap( - title: Component, - comment: Component? = null, - default: Map = mapOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.longMap(id, title, comment, default, resetKey) - } - types[id] = FieldType.LongMap - longMaps.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Map = longMaps.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Map - ) { longMaps[id] = value } - } - } - - /** Declares a `Map` config field. See [boolean] for parameter semantics. */ - protected fun floatMap( - title: Component, - comment: Component? = null, - default: Map = mapOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.floatMap(id, title, comment, default, resetKey) - } - types[id] = FieldType.FloatMap - floatMaps.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Map = floatMaps.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Map - ) { floatMaps[id] = value } - } - } - - /** Declares a `Map` config field. See [boolean] for parameter semantics. */ - protected fun doubleMap( - title: Component, - comment: Component? = null, - default: Map = mapOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.doubleMap(id, title, comment, default, resetKey) - } - types[id] = FieldType.DoubleMap - doubleMaps.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Map = doubleMaps.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Map - ) { doubleMaps[id] = value } - } - } - - /** Declares a `Map` config field. See [boolean] for parameter semantics. */ - protected fun stringMap( - title: Component, - comment: Component? = null, - default: Map = mapOf(), - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.stringMap(id, title, comment, default, resetKey) - } - types[id] = FieldType.StringMap - stringMaps.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Map = stringMaps.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Map - ) { stringMaps[id] = value } - } - } - - /** Declares a `Map` of nested [DataSpec] entries. See [spec] for parameter semantics. */ - @Suppress("UNCHECKED_CAST") - protected fun specMap( - title: Component, - comment: Component? = null, - default: Map = mapOf(), - factory: () -> T, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.specMap(id, title, comment, default, factory, resetKey) - } - types[id] = FieldType.SpecMap(factory) as FieldType> - (specMaps as MutableMap>).putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Map = specMaps.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Map - ) { specMaps[id] = value } - } - } - - /** - * Declares a `Map` of [Registry] entries, stored as a map of [ResourceLocation] keys. See - * [registryList] for parameter semantics. - */ - protected fun registryMap( - title: Component, - comment: Component? = null, - default: Map = mapOf(), - factory: () -> T, - registry: Registry, - resetKey: Component? = null, - subclass: KClass? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.registryMap(id, title, comment, default, factory, registry, resetKey, subclass) - } - types[id] = FieldType.RegistryMap - registryMaps.putIfAbsent(id, default.mapValues { registry.getKey(it.value)!! }) - object : ReadWriteProperty> - { - @Suppress("UNCHECKED_CAST") - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Map = registryMaps.getOrPut(id) { - default.mapValues { registry.getKey(it.value)!! } - }.mapValues { registry.get(it.value) ?: factory() } as Map - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Map - ) { registryMaps[id] = value.mapValues { registry.getKey(it.value)!! } } - } - } - - /** Declares a `Map` config field. See [keycode] for parameter semantics. */ - protected fun keycodeMap( - title: Component, - comment: Component? = null, - default: Map = mapOf(), - factory: () -> CommonKeyCode = CommonKeyCode.Companion::unknown, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.keycodeMap(id, title, comment, default, factory, resetKey) - } - types[id] = FieldType.KeyCodeMap - keycodeMaps.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Map = keycodeMaps.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Map - ) { keycodeMaps[id] = value } - } - } - - /** Declares a `Map` config field. See [color] for parameter semantics. */ - protected fun colorMap( - title: Component, - comment: Component? = null, - alpha: Boolean = false, - default: Map = mapOf(), - factory: () -> Color = { if (alpha) Color.ofTransparent(-1) else Color.ofOpaque(-1) }, - resetKey: Component? = null, - needsRestart: Boolean = false - ): PropertyDelegateProvider>> = - PropertyDelegateProvider { _, property -> - val id = property.name.toSnakeCase() - if (comment != null) - { - comments[id] = comment.string - } - onClient { - client.colorMap(id, title, comment, alpha, default, factory, resetKey) - } - types[id] = FieldType.ColorMap - colorMaps.putIfAbsent(id, default) - object : ReadWriteProperty> - { - override fun getValue( - thisRef: DataSpec, - property: KProperty<*> - ): Map = colorMaps.getOrPut(id) { default } - - override fun setValue( - thisRef: DataSpec, - property: KProperty<*>, - value: Map - ) { colorMaps[id] = value } - } - } - - /** - * Serializes/deserializes a [DataSpec] by walking its registered [types] and reading from - * or writing into the corresponding backing map (e.g. [booleans], [ints]). - */ - internal class ConfigCategorySerializer(val factory: () -> DataSpec) : - KSerializer - { - override val descriptor: SerialDescriptor by lazy { - with(factory().also { it.init() }) - { - buildClassSerialDescriptor(title.string) - { - types.forEach { (id, type) -> - element( - elementName = id, - descriptor = type.serializer.descriptor, - annotations = buildList { - if (id in comments) - { - add(TomlComment(comments[id]!!)) - add(SerialComment(comments[id]!!)) - } - } - ) - } - } - } - } - - override fun deserialize(decoder: Decoder): DataSpec - { - return decoder.decodeStructure(descriptor) - { - val spec = factory().also { it.init() } - with(spec) - { - while (true) - { - when (val index = decodeElementIndex(descriptor)) - { - in types.entries.indices -> - { - val (key, type) = types.entries.toList()[index] - when (type) - { - is FieldType.Category -> - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.Boolean -> booleans[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.Int -> ints[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.Long -> longs[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.Float -> floats[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.Double -> doubles[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.String -> strings[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.Spec -> specs[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.Registry -> registries[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.KeyCode -> keycodes[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.Color -> colors[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.EnumSelector -> enums[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.Selector -> selectors[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.IntList -> intLists[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.LongList -> longLists[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.FloatList -> floatLists[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.DoubleList -> doubleLists[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.StringList -> stringLists[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.SpecList -> specLists[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.RegistryList -> registryLists[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.KeyCodeList -> keycodeLists[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.ColorList -> colorLists[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.IntMap -> intMaps[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.LongMap -> longMaps[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.FloatMap -> floatMaps[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.DoubleMap -> doubleMaps[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.StringMap -> stringMaps[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.SpecMap -> specMaps[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.RegistryMap -> registryMaps[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.KeyCodeMap -> keycodeMaps[key] = - decodeSerializableElement(descriptor, index, type.serializer) - - is FieldType.ColorMap -> colorMaps[key] = - decodeSerializableElement(descriptor, index, type.serializer) - } - } - - CompositeDecoder.DECODE_DONE -> break - else -> error("Unexpected index: $index") - } - } - } - spec - } - } - - override fun serialize(encoder: Encoder, value: DataSpec) - { - value.init() - encoder.encodeStructure(descriptor) - { - value.types.entries.forEachIndexed { index, (key, type) -> - @Suppress("UNCHECKED_CAST") - when (type) - { - is FieldType.Category -> encodeSerializableElement( - descriptor, - index, - type.serializer, - type.category - ) - - is FieldType.Boolean -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.booleans[key]!! - ) - - is FieldType.Int -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.ints[key]!! - ) - - is FieldType.Long -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.longs[key]!! - ) - - is FieldType.Float -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.floats[key]!! - ) - - is FieldType.Double -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.doubles[key]!! - ) - - is FieldType.String -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.strings[key]!! - ) - - is FieldType.Spec -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.specs[key]!! - ) - - is FieldType.Registry -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.registries[key]!! - ) - - is FieldType.KeyCode -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.keycodes[key]!! - ) - - is FieldType.Color -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.colors[key]!! - ) - - is FieldType.EnumSelector -> encodeSerializableElement( - descriptor, - index, - type.serializer as KSerializer>, - value.enums[key]!! - ) - - is FieldType.Selector -> encodeSerializableElement( - descriptor, - index, - type.serializer as KSerializer, - value.selectors[key]!! - ) - - is FieldType.IntList -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.intLists[key]!! - ) - - is FieldType.LongList -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.longLists[key]!! - ) - - is FieldType.FloatList -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.floatLists[key]!! - ) - - is FieldType.DoubleList -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.doubleLists[key]!! - ) - - is FieldType.StringList -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.stringLists[key]!! - ) - - is FieldType.SpecList -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.specLists[key]!! - ) - - is FieldType.RegistryList -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.registryLists[key]!! - ) - - is FieldType.KeyCodeList -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.keycodeLists[key]!! - ) - - is FieldType.ColorList -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.colorLists[key]!! - ) - - is FieldType.IntMap -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.intMaps[key]!! - ) - - is FieldType.LongMap -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.longMaps[key]!! - ) - - is FieldType.FloatMap -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.floatMaps[key]!! - ) - - is FieldType.DoubleMap -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.doubleMaps[key]!! - ) - - is FieldType.StringMap -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.stringMaps[key]!! - ) - - is FieldType.SpecMap -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.specMaps[key]!! - ) - - is FieldType.RegistryMap -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.registryMaps[key]!! - ) - - is FieldType.KeyCodeMap -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.keycodeMaps[key]!! - ) - - is FieldType.ColorMap -> encodeSerializableElement( - descriptor, - index, - type.serializer, - value.colorMaps[key]!! - ) - } - } - } - } - } - - internal val serializer by lazy { ConfigCategorySerializer { this } } -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/FieldType.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/FieldType.kt deleted file mode 100644 index 4075d0014..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/FieldType.kt +++ /dev/null @@ -1,188 +0,0 @@ -package net.kernelpanicsoft.archie.config - -import kotlinx.serialization.InternalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.MapSerializer -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.serializer -import net.kernelpanicsoft.archie.serialization.DeferredListSerializer -import net.kernelpanicsoft.archie.serialization.DeferredMapSerializer -import net.kernelpanicsoft.archie.serialization.serializers.ColorSerializer -import net.kernelpanicsoft.archie.serialization.serializers.ResourceLocationSerializer -import net.minecraft.resources.ResourceLocation -import kotlin.reflect.KClass - -/** - * Tags a [DataSpec] field with its runtime type and the [KSerializer] used to read/write it, - * so [DataSpec.ConfigCategorySerializer] can (de)serialize each field generically without a - * `when` over the raw value type. One subtype per builder function in [DataSpec] (e.g. - * [Boolean] for `boolean()`, [IntList] for `intList()`). - */ -internal sealed class FieldType -{ - abstract val serializer: KSerializer - - data class Category(val category: DataSpec) : FieldType() - { - override val serializer: KSerializer = category.serializer - } - - data object Boolean : FieldType() - { - override val serializer: KSerializer = kotlin.Boolean.serializer() - } - - data object Int : FieldType() - { - override val serializer: KSerializer = kotlin.Int.serializer() - } - - data object Long : FieldType() - { - override val serializer: KSerializer = kotlin.Long.serializer() - } - - data object Float : FieldType() - { - override val serializer: KSerializer = kotlin.Float.serializer() - } - - data object Double : FieldType() - { - override val serializer: KSerializer = kotlin.Double.serializer() - } - - data object String : FieldType() - { - override val serializer: KSerializer = kotlin.String.serializer() - } - - data class Spec(val factory: () -> DataSpec) : FieldType() - { - override val serializer: KSerializer = - DataSpec.ConfigCategorySerializer(factory) - } - - data object Registry : FieldType() - { - override val serializer: KSerializer = ResourceLocationSerializer - } - - data object KeyCode : FieldType() - { - override val serializer: KSerializer = CommonKeyCode.serializer() - } - - data object Color : FieldType() - { - override val serializer: KSerializer = ColorSerializer - } - - data class EnumSelector>(val kClass: KClass) : FieldType() - { - @OptIn(InternalSerializationApi::class) - override val serializer: KSerializer = kClass.serializer() - } - - data class Selector(val kClass: KClass) : FieldType() - { - @OptIn(InternalSerializationApi::class) - override val serializer: KSerializer = kClass.serializer() - } - - data object IntList : FieldType>() - { - override val serializer: KSerializer> = ListSerializer(kotlin.Int.serializer()) - } - - data object LongList : FieldType>() - { - override val serializer: KSerializer> = ListSerializer(kotlin.Long.serializer()) - } - - data object FloatList : FieldType>() - { - override val serializer: KSerializer> = ListSerializer(kotlin.Float.serializer()) - } - - data object DoubleList : FieldType>() - { - override val serializer: KSerializer> = ListSerializer(kotlin.Double.serializer()) - } - - data object StringList : FieldType>() - { - override val serializer: KSerializer> = ListSerializer(kotlin.String.serializer()) - } - - data class SpecList(val factory: () -> DataSpec) : FieldType>() - { - override val serializer: KSerializer> = DeferredListSerializer( - DataSpec.ConfigCategorySerializer(factory) - ) - } - - data object RegistryList : FieldType>() - { - override val serializer: KSerializer> = ListSerializer(ResourceLocationSerializer) - } - - data object KeyCodeList : FieldType>() - { - override val serializer: KSerializer> = ListSerializer( - CommonKeyCode.serializer()) - } - - data object ColorList : FieldType>() - { - override val serializer: KSerializer> = ListSerializer(ColorSerializer) - } - - data object IntMap : FieldType>() - { - override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), kotlin.Int.serializer()) - } - - data object LongMap : FieldType>() - { - override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), kotlin.Long.serializer()) - } - - data object FloatMap : FieldType>() - { - override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), kotlin.Float.serializer()) - } - - data object DoubleMap : FieldType>() - { - override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), kotlin.Double.serializer()) - } - - data object StringMap : FieldType>() - { - override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), kotlin.String.serializer()) - } - - data class SpecMap(val factory: () -> DataSpec) : FieldType>() - { - override val serializer: KSerializer> = DeferredMapSerializer(kotlin.String.serializer(), - DataSpec.ConfigCategorySerializer(factory) - ) - } - - data object RegistryMap : FieldType>() - { - override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), ResourceLocationSerializer) - } - - data object KeyCodeMap : FieldType>() - { - override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), CommonKeyCode.serializer()) - } - - data object ColorMap : FieldType>() - { - override val serializer: KSerializer> = MapSerializer(kotlin.String.serializer(), ColorSerializer) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/IConfigSerializer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/IConfigSerializer.kt deleted file mode 100644 index 9edf90a8f..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/IConfigSerializer.kt +++ /dev/null @@ -1,77 +0,0 @@ -package net.kernelpanicsoft.archie.config - -import dev.architectury.platform.Platform -import net.kernelpanicsoft.archie.Archie -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.StandardCopyOption - -/** - * Reads and writes a [ConfigSpec] to/from a specific file format (JSON, JSON5, TOML, ...). Built-in - * implementations live in `net.kernelpanicsoft.archie.config.serializer`; [ConfigSpec.fileSerializer] - * picks one per-platform by default. - * - * [configPath], [load], and [save] all take a [configFolder] that defaults to the platform's shared - * config folder ([Platform.getConfigFolder]); [ConfigSpec] passes its own [ConfigSpec.configFolder] - * instead, which a [ConfigSpec.Server] repoints at the current world's per-save `serverconfig/` folder. - */ -interface IConfigSerializer -{ - /** File the given [config] is read from and written to, resolved under [configFolder]. */ - fun configPath(config: ConfigSpec, configFolder: Path = Platform.getConfigFolder()): Path - /** - * Reads [config]'s file if present via [loadString], then always [save]s it back out - this - * both formats a freshly-created file with defaults and rewrites an existing one with any - * newly-added fields. If the existing file fails to parse, it's logged and renamed to - * `.corrupted` rather than deleted, and loading falls through to writing fresh defaults - * so startup isn't blocked. - */ - fun load(config: ConfigSpec, configFolder: Path = Platform.getConfigFolder()) - { - val path = configPath(config, configFolder) - if (Files.exists(path)) - { - try - { - val string = Files.readString(path) - loadString(config, string) - } - catch (e: Throwable) - { - // A malformed/corrupt file must not permanently block startup. Back the bad - // file up rather than deleting it, log it, and fall through to save(config) - // below so a fresh default file gets written and the mod still loads. - Archie.LOGGER.error("Failed to load config at $path, resetting to defaults. The invalid file was backed up.", e) - runCatching { - Files.move(path, path.resolveSibling("${path.fileName}.corrupted"), StandardCopyOption.REPLACE_EXISTING) - } - } - } - - save(config, configFolder) - } - - /** Parses [string] and populates [config]'s fields from it. Implemented per-format. */ - fun loadString(config: ConfigSpec, string: String) - - /** Writes [config]'s current field values to [configPath], creating parent directories as needed. */ - fun save(config: ConfigSpec, configFolder: Path = Platform.getConfigFolder()) - { - val path = configPath(config, configFolder) - try - { - Files.createDirectories(path.parent) - Files.writeString(path, saveString(config)) - } - catch (e: Throwable) - { - throw SerializationException(e) - } - } - - /** Renders [config]'s current field values as a file-format string. Implemented per-format. */ - fun saveString(config: ConfigSpec): String - - /** Thrown when writing a config file fails (e.g. an I/O error). */ - class SerializationException(cause: Throwable) : Exception(cause) -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorListBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorListBuilder.kt deleted file mode 100644 index 1ddd916e2..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorListBuilder.kt +++ /dev/null @@ -1,38 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.ColorEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.impl.builders.FieldBuilder -import me.shedaniel.math.Color -import net.minecraft.network.chat.Component - -/** [ListFieldBuilder] for [Color] values, using Cloth Config's `startColorField` per row. Set [alphaMode] to allow editing alpha. */ -@Suppress("MemberVisibilityCanBePrivate", "unused") -class ColorListBuilder( - resetButtonKey: Component, - fieldNameKey: Component, - value: List, - private val factory: () -> Color -) : - ListFieldBuilder( - resetButtonKey, - fieldNameKey, - value.map { it.color } - ) -{ - var alphaMode: Boolean = false - - override fun factory(): Int = factory.invoke().color - - override fun ConfigEntryBuilder.builder( - title: Component, - value: Int, - list: NestedListListEntry - ): FieldBuilder - { - return startColorField(title, value) - .setAlphaMode(alphaMode) - } - -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorMapBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorMapBuilder.kt deleted file mode 100644 index 62a03900d..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorMapBuilder.kt +++ /dev/null @@ -1,40 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import net.kernelpanicsoft.archie.util.MutableEntry -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.ColorEntry -import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.impl.builders.FieldBuilder -import me.shedaniel.math.Color -import net.minecraft.network.chat.Component - -/** [MapFieldBuilder] for [Color] values, using Cloth Config's `startColorField` per row. Set [alphaMode] to allow editing alpha. */ -@Suppress("MemberVisibilityCanBePrivate", "unused") -class ColorMapBuilder( - resetButtonKey: Component, - fieldNameKey: Component, - value: Map, - private val factory: () -> Color -) : - MapFieldBuilder( - resetButtonKey, - fieldNameKey, - value.mapValues { it.value.color } - ) -{ - var alphaMode: Boolean = false - - override fun valueFactory(): Int = factory.invoke().color - - override fun ConfigEntryBuilder.valueBuilder( - title: Component, - value: Int, - list: NestedListListEntry, MultiElementListEntry>> - ): FieldBuilder - { - return startColorField(title, value) - .setAlphaMode(alphaMode) - } - -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ConfigFieldBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ConfigFieldBuilder.kt deleted file mode 100644 index 85d8d13d9..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ConfigFieldBuilder.kt +++ /dev/null @@ -1,40 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder -import net.kernelpanicsoft.archie.config.ConfigSpec -import net.kernelpanicsoft.archie.config.entry.ConfigSpecEntry -import net.minecraft.network.chat.Component -import java.util.* -import kotlin.jvm.optionals.getOrNull - -/** - * Cloth Config field builder producing a [ConfigSpecEntry] - a single "Edit" button field that - * navigates into [value]'s own config screen. Built via `ConfigEntryBuilder.startConfigField`, - * used by [net.kernelpanicsoft.archie.config.ClientConfigContainer] to let a container screen with - * multiple [ConfigSpec]s drill down into each one. - */ -class ConfigFieldBuilder( - resetButtonKey: Component, - fieldNameKey: Component, - private val value: T -) : AbstractFieldBuilder, ConfigFieldBuilder>( - resetButtonKey, fieldNameKey -) -{ - var buttonText: Component = Component.literal("Edit") - var requiresRestart: Boolean = false - - override fun build(): ConfigSpecEntry - { - val entry = ConfigSpecEntry( - fieldNameKey, - buttonText, - value, - requiresRestart - ) - entry.setErrorSupplier { - Optional.ofNullable(errorSupplier?.apply(entry.value)?.getOrNull()) - } - return finishBuilding(entry) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DoubleMapBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DoubleMapBuilder.kt deleted file mode 100644 index 5a923c57d..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DoubleMapBuilder.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import net.kernelpanicsoft.archie.util.MutableEntry -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.DoubleListEntry -import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder -import net.minecraft.network.chat.Component - -/** [MapFieldBuilder] for `Double` values, using Cloth Config's `startDoubleField` per row. */ -class DoubleMapBuilder(resetButtonKey: Component, fieldNameKey: Component, value: Map) : MapFieldBuilder( - resetButtonKey, fieldNameKey, value -) -{ - override fun valueFactory(): Double = 0.0 - - override fun ConfigEntryBuilder.valueBuilder( - title: Component, - value: Double, - list: NestedListListEntry, MultiElementListEntry>> - ): AbstractFieldBuilder - { - return startDoubleField(title, value) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DropdownFieldBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DropdownFieldBuilder.kt deleted file mode 100644 index 9f2614d5a..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DropdownFieldBuilder.kt +++ /dev/null @@ -1,46 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry -import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder -import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder -import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder.CellCreatorBuilder -import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder.TopCellElementBuilder -import net.minecraft.network.chat.Component - -/** - * Cloth Config builder for a dropdown/autocomplete field over [selections]. Set [toObjectFunction] - * to parse free-typed text back into a `T` (required when [suggestionMode] is enabled); override - * [toTextFunction] to customize how values are displayed. - */ -open class DropdownFieldBuilder( - resetButtonKey: Component, - fieldNameKey: Component, - private val value: T, - open var selections: Iterable = emptyList() -) : AbstractFieldBuilder, DropdownFieldBuilder>( - resetButtonKey, fieldNameKey -) -{ - open lateinit var toObjectFunction: (String) -> T - open var toTextFunction: (T) -> Component = { Component.literal(it.toString()) } - open var suggestionMode: Boolean = true - - override fun build(): DropdownBoxEntry - { - val entry = DropdownMenuBuilder( - resetButtonKey, - fieldNameKey, - TopCellElementBuilder.of(value, toObjectFunction, toTextFunction), - CellCreatorBuilder.of(toTextFunction) - ) - entry.setSuggestionMode(suggestionMode) - entry.setSelections(selections) - - entry.setSaveConsumer(saveConsumer) - entry.setErrorSupplier(errorSupplier) - entry.setTooltipSupplier(tooltipSupplier) - entry.setDefaultValue(defaultValue) - entry.requireRestart(requireRestart) - return finishBuilding(entry.build()) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/FloatMapBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/FloatMapBuilder.kt deleted file mode 100644 index c7e217dda..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/FloatMapBuilder.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import net.kernelpanicsoft.archie.util.MutableEntry -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.FloatListEntry -import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder -import net.minecraft.network.chat.Component - -/** [MapFieldBuilder] for `Float` values, using Cloth Config's `startFloatField` per row. */ -class FloatMapBuilder(resetButtonKey: Component, fieldNameKey: Component, value: Map) : MapFieldBuilder( - resetButtonKey, fieldNameKey, value -) -{ - override fun valueFactory(): Float = 0.0f - - override fun ConfigEntryBuilder.valueBuilder( - title: Component, - value: Float, - list: NestedListListEntry, MultiElementListEntry>> - ): AbstractFieldBuilder - { - return startFloatField(title, value) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/IntegerMapBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/IntegerMapBuilder.kt deleted file mode 100644 index c27eed956..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/IntegerMapBuilder.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import net.kernelpanicsoft.archie.util.MutableEntry -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.IntegerListEntry -import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder -import net.minecraft.network.chat.Component - -/** [MapFieldBuilder] for `Int` values, using Cloth Config's `startIntField` per row. */ -class IntegerMapBuilder(resetButtonKey: Component, fieldNameKey: Component, value: Map) : MapFieldBuilder( - resetButtonKey, fieldNameKey, value -) -{ - override fun valueFactory(): Int = 0 - - override fun ConfigEntryBuilder.valueBuilder( - title: Component, - value: Int, - list: NestedListListEntry, MultiElementListEntry>> - ): AbstractFieldBuilder - { - return startIntField(title, value) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeListBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeListBuilder.kt deleted file mode 100644 index e1c8657f1..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeListBuilder.kt +++ /dev/null @@ -1,61 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.api.Modifier -import me.shedaniel.clothconfig2.api.ModifierKeyCode -import me.shedaniel.clothconfig2.gui.entries.KeyCodeEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.impl.builders.FieldBuilder -import me.shedaniel.clothconfig2.impl.builders.KeyCodeBuilder -import net.minecraft.network.chat.Component - -/** - * [ListFieldBuilder] for [ModifierKeyCode] values, using Cloth Config's `startModifierKeyCodeField` - * per row. [allowKey] and [allowMouse] can't both be `false` - at least one input source must - * remain selectable. [allowModifiers] toggles whether Ctrl/Shift/Alt can be bound alongside the key. - */ -class KeycodeListBuilder( - resetButtonKey: Component, - fieldNameKey: Component, - value: List, - private val factory: () -> ModifierKeyCode -) : - ListFieldBuilder( - resetButtonKey, - fieldNameKey, - value - ) -{ - var allowModifiers: Boolean = true - private var _allowKey: Boolean = true - var allowKey: Boolean - get() = _allowKey - set(allowKey) - { - require(!(!this.allowMouse && !allowKey)) - _allowKey = allowKey - } - private var _allowMouse: Boolean = true - var allowMouse: Boolean - get() = _allowMouse - set(allowMouse) - { - require(!(!this.allowKey && !allowMouse)) - _allowMouse = allowMouse - } - - override fun factory(): ModifierKeyCode = factory.invoke() - - override fun ConfigEntryBuilder.builder( - title: Component, - value: ModifierKeyCode, - list: NestedListListEntry - ): FieldBuilder - { - return startModifierKeyCodeField(title, value) - .setAllowModifiers(allowModifiers) - .setAllowKey(allowKey) - .setAllowMouse(allowMouse) - } - -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeMapBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeMapBuilder.kt deleted file mode 100644 index 4f9c97ddc..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeMapBuilder.kt +++ /dev/null @@ -1,56 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import net.kernelpanicsoft.archie.util.MutableEntry -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.api.ModifierKeyCode -import me.shedaniel.clothconfig2.gui.entries.KeyCodeEntry -import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.impl.builders.FieldBuilder -import net.minecraft.network.chat.Component - -/** - * [MapFieldBuilder] for [ModifierKeyCode] values, using Cloth Config's `startModifierKeyCodeField` - * per row. See [KeycodeListBuilder] for the `allowKey`/`allowMouse`/`allowModifiers` constraints. - */ -class KeycodeMapBuilder( - resetButtonKey: Component, - fieldNameKey: Component, - value: Map, - private val factory: () -> ModifierKeyCode -) : MapFieldBuilder( - resetButtonKey, fieldNameKey, value -) -{ - private var allowModifiers: Boolean = true - private var _allowKey: Boolean = true - private var allowKey: Boolean - get() = _allowKey - set(allowKey) - { - require(!(!this.allowMouse && !allowKey)) - _allowKey = allowKey - } - private var _allowMouse: Boolean = true - private var allowMouse: Boolean - get() = _allowMouse - set(allowMouse) - { - require(!(!this.allowKey && !allowMouse)) - _allowMouse = allowMouse - } - - override fun valueFactory(): ModifierKeyCode = factory.invoke() - - override fun ConfigEntryBuilder.valueBuilder( - title: Component, - value: ModifierKeyCode, - list: NestedListListEntry, MultiElementListEntry>> - ): FieldBuilder - { - return startModifierKeyCodeField(title, value) - .setAllowModifiers(allowModifiers) - .setAllowKey(allowKey) - .setAllowMouse(allowMouse) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ListFieldBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ListFieldBuilder.kt deleted file mode 100644 index ae58cae49..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ListFieldBuilder.kt +++ /dev/null @@ -1,105 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import me.shedaniel.clothconfig2.api.AbstractConfigListEntry -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.api.ModifierKeyCode -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder -import me.shedaniel.clothconfig2.impl.builders.AbstractListBuilder -import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder -import me.shedaniel.clothconfig2.impl.builders.FieldBuilder -import me.shedaniel.clothconfig2.impl.builders.KeyCodeBuilder -import net.minecraft.network.chat.Component -import java.util.* -import java.util.function.Supplier -import kotlin.jvm.optionals.getOrNull - -/** - * Base Cloth Config builder for a list field, rendered as an editable list of rows sharing one - * element type. Concrete subclasses (e.g. [KeycodeListBuilder]) only need to implement [factory] - * (the default for a newly-inserted row) and [builder] (the field builder for each row); this - * class handles add/remove wiring and error/tooltip propagation. - */ -abstract class ListFieldBuilder, SELF : ListFieldBuilder>( - resetButtonKey: Component, - fieldNameKey: Component, - value: List -) : AbstractListBuilder, SELF>( - resetButtonKey, fieldNameKey -) -{ - init - { - this.value = value - } - - /** Value assigned to a row inserted via the UI's "add" button. */ - abstract fun factory(): T - - /** Builds the Cloth Config field for a single row. */ - abstract fun ConfigEntryBuilder.builder(title: Component, value: T, list: NestedListListEntry): FieldBuilder - - override fun build(): NestedListListEntry - { - val entryBuilder: ConfigEntryBuilder = ConfigEntryBuilder.create() - @Suppress("UnstableApiUsage") - val entry = NestedListListEntry( - fieldNameKey, - value, - isExpanded, - null, - saveConsumer, - defaultValue, - resetButtonKey, - isDeleteButtonEnabled, - isInsertInFront - ) { entryNullable: T?, list: NestedListListEntry -> - val entry = entryNullable ?: factory() - entryBuilder.builder(Component.literal("Entry"), entry, list).apply { - when (this) - { - is AbstractFieldBuilder -> - { - setErrorSupplier { cellValue -> - Optional.ofNullable(cellErrorSupplier?.apply(cellValue)?.getOrNull()) - } - setDefaultValue { - factory() - } - } - - is DropdownMenuBuilder -> - { - setErrorSupplier { cellValue -> - Optional.ofNullable(cellErrorSupplier?.apply(cellValue)?.getOrNull()) - } - setDefaultValue { - factory() - } - } - - is KeyCodeBuilder -> - { - setModifierErrorSupplier { cellValue -> - @Suppress("UNCHECKED_CAST") - Optional.ofNullable(cellErrorSupplier?.apply(cellValue as T)?.getOrNull()) - } - setModifierDefaultValue { - factory() as ModifierKeyCode - } - } - } - requireRestart(this@ListFieldBuilder.isRequireRestart) - setRequirement(this@ListFieldBuilder.enableRequirement) - setDisplayRequirement(this@ListFieldBuilder.displayRequirement) - }.build() - } - entry.setTooltipSupplier { - tooltipSupplier.apply(entry.value) - } - entry.setErrorSupplier { - Optional.ofNullable(errorSupplier?.apply(entry.value)?.getOrNull()) - } - return entry - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/LongMapBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/LongMapBuilder.kt deleted file mode 100644 index 738007a3a..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/LongMapBuilder.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import net.kernelpanicsoft.archie.util.MutableEntry -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.LongListEntry -import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder -import net.minecraft.network.chat.Component - -/** [MapFieldBuilder] for `Long` values, using Cloth Config's `startLongField` per row. */ -class LongMapBuilder(resetButtonKey: Component, fieldNameKey: Component, value: Map) : MapFieldBuilder( - resetButtonKey, fieldNameKey, value -) -{ - override fun valueFactory(): Long = 0 - - override fun ConfigEntryBuilder.valueBuilder( - title: Component, - value: Long, - list: NestedListListEntry, MultiElementListEntry>> - ): AbstractFieldBuilder - { - return startLongField(title, value) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/MapFieldBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/MapFieldBuilder.kt deleted file mode 100644 index bc9f9428c..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/MapFieldBuilder.kt +++ /dev/null @@ -1,181 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import net.kernelpanicsoft.archie.util.MutableEntry -import net.kernelpanicsoft.archie.util.toMutableEntry -import me.shedaniel.clothconfig2.api.AbstractConfigListEntry -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.api.ModifierKeyCode -import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.gui.entries.StringListEntry -import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder -import me.shedaniel.clothconfig2.impl.builders.AbstractListBuilder -import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder -import me.shedaniel.clothconfig2.impl.builders.FieldBuilder -import me.shedaniel.clothconfig2.impl.builders.KeyCodeBuilder -import net.minecraft.network.chat.Component -import java.util.* -import java.util.function.Consumer -import java.util.function.Function -import java.util.function.Supplier -import kotlin.jvm.optionals.getOrNull - -/** - * Base Cloth Config builder for a `String`-keyed map field, rendered as a nested list of - * editable key/value rows. Concrete subclasses (e.g. [IntegerMapBuilder]) only need to implement - * [valueFactory] (the default for a newly-inserted row) and [valueBuilder] (the field builder for - * the value column); this class handles the key column, duplicate-key validation, and row - * add/remove wiring. - */ -abstract class MapFieldBuilder, SELF : MapFieldBuilder>( - resetButtonKey: Component, - fieldNameKey: Component, - value: Map -) : - AbstractListBuilder, NestedListListEntry, MultiElementListEntry>>, SELF>( - resetButtonKey, - fieldNameKey - ) -{ - - open var keyErrorSupplier: ((String) -> Optional)? = null - open var valueErrorSupplier: ((T) -> Optional)? = null - open var valueTooltipSupplier: ((T) -> Optional>)? = null - - init - { - this.value = value.entries.toList().map(Map.Entry::toMutableEntry) - } - - /** Value assigned to a row inserted via the UI's "add" button. */ - abstract fun valueFactory(): T - - /** Builds the Cloth Config field for a row's value column. */ - abstract fun ConfigEntryBuilder.valueBuilder(title: Component, value: T, list: NestedListListEntry, MultiElementListEntry>>): FieldBuilder - - override fun build(): NestedListListEntry, MultiElementListEntry>> - { - val entryBuilder: ConfigEntryBuilder = ConfigEntryBuilder.create() - - val fields: MutableList = mutableListOf() - - @Suppress("UnstableApiUsage") - val entry = NestedListListEntry( - fieldNameKey, - value, - isExpanded, - null, - saveConsumer, - defaultValue, - resetButtonKey, - isDeleteButtonEnabled, - isInsertInFront - ) { entryNullable: MutableEntry?, list: NestedListListEntry, MultiElementListEntry>> -> - val entry: MutableEntry = entryNullable ?: ("" to valueFactory()).toMutableEntry() - val cell = MultiElementListEntry( - Component.literal("Entry"), - entry, - buildList { - add(entryBuilder.startStrField(Component.literal("Key"), entry.key).apply { - saveConsumer = Consumer { key -> - entryNullable?.key = key - entry.key = key - } - setErrorSupplier { entryKey -> - Optional.ofNullable(keyErrorSupplier?.invoke(entryKey)?.getOrNull()).or { - if (fields.count { - it.value == entryKey - } > 1) - Optional.of(Component.literal("Duplicate Key: $entryKey")) - else - Optional.empty() - } - } - requireRestart(this@MapFieldBuilder.requireRestart) - setRequirement(this@MapFieldBuilder.enableRequirement) - setDisplayRequirement(this@MapFieldBuilder.displayRequirement) - }.build().also { - fields.add(it) - }) - - add(entryBuilder.valueBuilder(Component.literal("Value"), entry.value, list).apply { - when (this) - { - is AbstractFieldBuilder -> - { - setSaveConsumer { value -> - entryNullable?.value = value - entry.value = value - } - setErrorSupplier { entryValue -> - Optional.ofNullable(valueErrorSupplier?.invoke(entryValue)?.getOrNull()) - } - setTooltipSupplier { entryValue -> - Optional.ofNullable(valueTooltipSupplier?.invoke(entryValue)?.getOrNull()) - } - setDefaultValue { - valueFactory() - } - } - - is DropdownMenuBuilder -> - { - setSaveConsumer { value -> - entryNullable?.value = value - entry.value = value - } - setErrorSupplier { entryValue -> - Optional.ofNullable(valueErrorSupplier?.invoke(entryValue)?.getOrNull()) - } - setTooltipSupplier { entryValue -> - Optional.ofNullable(valueTooltipSupplier?.invoke(entryValue)?.getOrNull()) - } - setDefaultValue { - valueFactory() - } - } - - is KeyCodeBuilder -> - { - setModifierSaveConsumer { value -> - @Suppress("UNCHECKED_CAST") - entryNullable?.value = value as T - @Suppress("UNCHECKED_CAST") - entry.value = value as T - } - setModifierErrorSupplier { entryValue -> - @Suppress("UNCHECKED_CAST") - Optional.ofNullable(valueErrorSupplier?.invoke(entryValue as T)?.getOrNull()) - } - setModifierTooltipSupplier { entryValue -> - @Suppress("UNCHECKED_CAST") - Optional.ofNullable(valueTooltipSupplier?.invoke(entryValue as T)?.getOrNull()) - } - setModifierDefaultValue { - valueFactory() as ModifierKeyCode - } - } - } - requireRestart(this@MapFieldBuilder.requireRestart) - setRequirement(this@MapFieldBuilder.enableRequirement) - setDisplayRequirement(this@MapFieldBuilder.displayRequirement) - }.build()) - }, - list.isExpanded - ) - cell.setErrorSupplier { - Optional.ofNullable(cellErrorSupplier?.apply(cell.value)?.getOrNull()) - } - cell - } - entry.setTooltipSupplier { - tooltipSupplier.apply(entry.value) - } - entry.setErrorSupplier { - Optional.ofNullable(errorSupplier?.apply(entry.value)?.getOrNull()) - } - return finishBuilding(entry) - } - - -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryFieldBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryFieldBuilder.kt deleted file mode 100644 index d19b088dc..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryFieldBuilder.kt +++ /dev/null @@ -1,56 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry -import me.shedaniel.clothconfig2.impl.builders.DropdownMenuBuilder -import net.minecraft.client.Minecraft -import net.minecraft.client.gui.GuiGraphics -import net.minecraft.core.Registry -import net.minecraft.network.chat.Component -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.item.Item -import net.minecraft.world.item.ItemStack -import net.minecraft.world.level.block.Block -import net.minecraft.world.level.block.entity.BlockEntityType -import net.kernelpanicsoft.archie.config.builder.ofBlockEntityTypeObject -import java.lang.reflect.Field -import kotlin.reflect.KClass - -/** - * Cloth Config builder for a single [registry] entry, rendered as a dropdown over every entry - * (optionally filtered to instances of [subclass]) sorted by registry name. Recognized entry - * types (`Item`, `Block`, `BlockEntityType`) get an icon in their dropdown cell; anything else - * falls back to a plain text cell. - */ -@Suppress("UNCHECKED_CAST") -class RegistryFieldBuilder( - resetButtonKey: Component, - fieldNameKey: Component, - subclass: KClass? = null, - registry: Registry, - value: T -) : DropdownMenuBuilder(resetButtonKey, fieldNameKey, TopCellElementBuilder.of(value, { - registry.getOptional( - ResourceLocation.parse(it) - ).orElse(null) -}, { - Component.literal(registry.getKey(it).toString()) -}), when (value) -{ - is Item -> CellCreatorBuilder.ofItemObject() as DropdownBoxEntry.SelectionCellCreator - is Block -> CellCreatorBuilder.ofBlockObject() as DropdownBoxEntry.SelectionCellCreator - is BlockEntityType<*> -> ofBlockEntityTypeObject() as DropdownBoxEntry.SelectionCellCreator - else -> CellCreatorBuilder.of(20, 146, 7) { - Component.literal(registry.getKey(it).toString()) - } -}) -{ - init - { - selections = ( - if (subclass != null) registry.filterIsInstance(subclass.java) - else registry - ).sortedBy { - registry.getKey(it).toString() - }.toSet() - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryListBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryListBuilder.kt deleted file mode 100644 index 79ffadbbd..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryListBuilder.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.impl.builders.FieldBuilder -import net.minecraft.core.Registry -import net.minecraft.network.chat.Component -import kotlin.reflect.KClass - -/** [ListFieldBuilder] for [registry] entries, using [RegistryFieldBuilder] per row. */ -class RegistryListBuilder( - resetButtonKey: Component, - fieldNameKey: Component, - value: List, - private val factory: () -> T, - private val subclass: KClass? = null, - private val registry: Registry -) : - ListFieldBuilder, RegistryListBuilder>( - resetButtonKey, fieldNameKey, value - ) -{ - override fun factory(): T = this.factory.invoke() - - override fun ConfigEntryBuilder.builder( - title: Component, - value: T, - list: NestedListListEntry> - ): FieldBuilder, *> - { - return startRegistryField(title, value, subclass, registry) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryMapBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryMapBuilder.kt deleted file mode 100644 index 9644a2331..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryMapBuilder.kt +++ /dev/null @@ -1,36 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import net.kernelpanicsoft.archie.util.MutableEntry -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry -import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.impl.builders.FieldBuilder -import net.minecraft.core.Registry -import net.minecraft.network.chat.Component -import kotlin.reflect.KClass - -/** [MapFieldBuilder] for [registry] entries, using [RegistryFieldBuilder] per row. */ -class RegistryMapBuilder( - resetButtonKey: Component, - fieldNameKey: Component, - value: Map, - private val factory: () -> T, - private val subclass: KClass? = null, - private val registry: Registry -) : - MapFieldBuilder, RegistryMapBuilder>( - resetButtonKey, fieldNameKey, value - ) -{ - override fun valueFactory(): T = this.factory.invoke() - - override fun ConfigEntryBuilder.valueBuilder( - title: Component, - value: T, - list: NestedListListEntry, MultiElementListEntry>> - ): FieldBuilder, *> - { - return startRegistryField(title, value, subclass, registry) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecFieldBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecFieldBuilder.kt deleted file mode 100644 index be961f31f..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecFieldBuilder.kt +++ /dev/null @@ -1,56 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import net.kernelpanicsoft.archie.config.DataSpec -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry -import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder -import net.minecraft.network.chat.Component -import java.util.* -import java.util.function.Supplier -import kotlin.jvm.optionals.getOrNull - -/** - * Cloth Config builder for a nested [DataSpec] field, rendered as a collapsible group - * containing [value]'s own fields and subcategories (via [DataSpec.client]). - */ -class SpecFieldBuilder( - resetButtonKey: Component, - fieldNameKey: Component, - value: T -) : AbstractFieldBuilder, SpecFieldBuilder>( - resetButtonKey, fieldNameKey -) -{ - /** Whether the group starts expanded in the UI. */ - var isExpanded: Boolean = false - init - { - this.value = value - } - @Suppress("UnstableApiUsage") - override fun build(): MultiElementListEntry - { - val entryBuilder: ConfigEntryBuilder = ConfigEntryBuilder.create() - val entry = MultiElementListEntry( - fieldNameKey, - value, - buildList { - value.client.builders.forEach { builder -> - add(entryBuilder.builder()) - } - -// value.subcategories.forEach { cat -> -// add(cat.client.buildSub(entryBuilder)) -// } - }, - isExpanded - ) - entry.tooltipSupplier = Supplier { - tooltipSupplier.apply(entry.value) - } - entry.setErrorSupplier { - Optional.ofNullable(errorSupplier?.apply(entry.value)?.getOrNull()) - } - return finishBuilding(entry) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecListBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecListBuilder.kt deleted file mode 100644 index 92ff2865f..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecListBuilder.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import net.kernelpanicsoft.archie.config.DataSpec -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder -import net.minecraft.network.chat.Component - -/** [ListFieldBuilder] for nested [DataSpec] entries, using [SpecFieldBuilder] per row. */ -class SpecListBuilder( - resetButtonKey: Component, - fieldNameKey: Component, - value: List, - private val factory: () -> T -) : ListFieldBuilder, SpecListBuilder>( - resetButtonKey, fieldNameKey, value -) -{ - override fun factory(): T = this.factory.invoke() - - override fun ConfigEntryBuilder.builder( - title: Component, - value: T, - list: NestedListListEntry> - ): AbstractFieldBuilder, *> - { - return startSpecField(title, value).also { it.isExpanded = list.isExpanded } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecMapBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecMapBuilder.kt deleted file mode 100644 index f9e5e0b8c..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecMapBuilder.kt +++ /dev/null @@ -1,31 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import net.kernelpanicsoft.archie.config.DataSpec -import net.kernelpanicsoft.archie.util.MutableEntry -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder -import net.minecraft.network.chat.Component - -/** [MapFieldBuilder] for nested [DataSpec] entries, using [SpecFieldBuilder] per row. */ -class SpecMapBuilder( - resetButtonKey: Component, - fieldNameKey: Component, - value: Map, - private val valueFactory: () -> T -) : MapFieldBuilder, SpecMapBuilder>( - resetButtonKey, fieldNameKey, value -) -{ - override fun valueFactory(): T = this.valueFactory.invoke() - - override fun ConfigEntryBuilder.valueBuilder( - title: Component, - value: T, - list: NestedListListEntry, MultiElementListEntry>> - ): AbstractFieldBuilder, *> - { - return startSpecField(title, value).also { it.isExpanded = list.isExpanded } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/StringMapBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/StringMapBuilder.kt deleted file mode 100644 index ebe9faf9c..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/StringMapBuilder.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import net.kernelpanicsoft.archie.util.MutableEntry -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.MultiElementListEntry -import me.shedaniel.clothconfig2.gui.entries.NestedListListEntry -import me.shedaniel.clothconfig2.gui.entries.StringListEntry -import me.shedaniel.clothconfig2.impl.builders.AbstractFieldBuilder -import net.minecraft.network.chat.Component - -/** [MapFieldBuilder] for `String` values, using Cloth Config's `startStrField` per row. */ -class StringMapBuilder(resetButtonKey: Component, fieldNameKey: Component, value: Map) : MapFieldBuilder( - resetButtonKey, fieldNameKey, value -) -{ - override fun valueFactory(): String = "" - - override fun ConfigEntryBuilder.valueBuilder( - title: Component, - value: String, - list: NestedListListEntry, MultiElementListEntry>> - ): AbstractFieldBuilder - { - return startStrField(title, value) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/extensions.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/extensions.kt deleted file mode 100644 index 3143d6df6..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/extensions.kt +++ /dev/null @@ -1,259 +0,0 @@ -package net.kernelpanicsoft.archie.config.builder - -import net.kernelpanicsoft.archie.config.DataSpec -import net.kernelpanicsoft.archie.util.getReflection -import net.kernelpanicsoft.archie.util.setReflection -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.api.ModifierKeyCode -import me.shedaniel.clothconfig2.gui.entries.DropdownBoxEntry -import me.shedaniel.clothconfig2.impl.builders.ColorFieldBuilder -import me.shedaniel.math.Color -import net.kernelpanicsoft.archie.config.ConfigSpec -import net.minecraft.client.Minecraft -import net.minecraft.client.gui.GuiGraphics -import net.minecraft.core.Registry -import net.minecraft.core.registries.BuiltInRegistries -import net.minecraft.network.chat.Component -import net.minecraft.world.item.ItemStack -import net.minecraft.world.level.block.Block -import net.minecraft.world.level.block.entity.BlockEntityType -import java.lang.reflect.Field -import kotlin.reflect.KClass - -/** - * A [DropdownBoxEntry] cell creator that renders each [BlockEntityType] option as its owning - * block's item icon plus registry name, for use with a dropdown field over block entity types. - */ -fun ofBlockEntityTypeObject(): DropdownBoxEntry.SelectionCellCreator> -{ - return object : DropdownBoxEntry.DefaultSelectionCellCreator>({ - Component.literal(BuiltInRegistries.BLOCK_ENTITY_TYPE.getKey(it).toString()) - }) - { - @Suppress("UNCHECKED_CAST") - override fun create(selection: BlockEntityType<*>): DropdownBoxEntry.SelectionCellElement> - { - val blocksField: Field = BlockEntityType::class.java.getDeclaredField("validBlocks") - blocksField.isAccessible = true - val blocks = blocksField.get(selection) as Set - val block = blocks.first() - val stack = ItemStack(block) - return object : DropdownBoxEntry.DefaultSelectionCellElement>(selection, toTextFunction) - { - override fun render( - graphics: GuiGraphics?, - mouseX: Int, - mouseY: Int, - x: Int, - y: Int, - width: Int, - height: Int, - delta: Float - ) - { - this.rendering = true - this.x = x - this.y = y - this.width = width - this.height = height - val b = mouseX >= x && mouseX <= x + width && mouseY >= y && mouseY <= y + height - if (b) - { - graphics!!.fill(x + 1, y + 1, x + width - 1, y + height - 1, -15132391) - } - - graphics!!.drawString( - Minecraft.getInstance().font, - (this.toTextFunction.apply( - r - ) as Component).visualOrderText, x + 6 + 18, y + 6, if (b) 16777215 else 8947848 - ) - graphics.renderItem(stack, x + 4, y + 2) - } - } - } - - override fun getCellHeight(): Int - { - return 20 - } - - override fun getCellWidth(): Int - { - return 146 - } - - override fun getDropBoxMaxHeight(): Int - { - return cellHeight * 7 - } - } -} - -/** Reflectively exposes Cloth Config's private `alpha` flag on [ColorFieldBuilder], since it has no public getter/setter. */ -var ColorFieldBuilder.alphaMode: Boolean - get() = getReflection("alpha") - set(value) = setReflection("alpha", value) - -/** - * The `start*Field`/`start*List`/`start*Map` functions below extend [ConfigEntryBuilder] the same - * way Cloth Config's own built-ins do (`startBooleanToggle`, `startIntField`, ...), so the field - * types Archie adds - nested specs, registry entries, keybind/color lists and maps - are used the - * same way. Each one just forwards to the matching builder class's constructor. - */ -fun ConfigEntryBuilder.startSpecField(fieldNameKey: Component, value: T): SpecFieldBuilder -{ - return SpecFieldBuilder(resetButtonKey, fieldNameKey, value) -} - -fun ConfigEntryBuilder.startConfigField(fieldNameKey: Component, value: T): ConfigFieldBuilder -{ - return ConfigFieldBuilder(resetButtonKey, fieldNameKey, value) -} - -/** See [startSpecField]. Builds a single registry-entry field, resolved against [registry] and optionally narrowed to [subclass]. */ -fun ConfigEntryBuilder.startRegistryField( - fieldNameKey: Component, - value: T, - subclass: KClass? = null, - registry: Registry -): RegistryFieldBuilder -{ - return RegistryFieldBuilder(resetButtonKey, fieldNameKey, subclass, registry, value) -} - -/** See [startSpecField]. Builds a dropdown field over arbitrary [selections], accepting free-text input for a `String` value. */ -fun ConfigEntryBuilder.startStringDropdownField( - fieldNameKey: Component, - value: String, - selections: Iterable = emptyList() -): DropdownFieldBuilder -{ - return DropdownFieldBuilder(resetButtonKey, fieldNameKey, value, selections).apply { - toObjectFunction = { it } - } -} - -/** See [startSpecField]. Builds a dropdown field over arbitrary [selections] of any type [T]. */ -fun ConfigEntryBuilder.startDropdownField( - fieldNameKey: Component, - value: T, - selections: Iterable = emptyList() -): DropdownFieldBuilder -{ - return DropdownFieldBuilder(resetButtonKey, fieldNameKey, value, selections) -} - -/** See [startSpecField]. Builds a list of nested [DataSpec] entries. */ -fun ConfigEntryBuilder.startSpecList( - fieldNameKey: Component, - value: List, - factory: () -> T -): SpecListBuilder -{ - return SpecListBuilder(resetButtonKey, fieldNameKey, value, factory) -} - -/** See [startSpecField]. Builds a list of [registry] entries; [factory] supplies a value for newly-inserted rows. */ -fun ConfigEntryBuilder.startRegistryList( - fieldNameKey: Component, - value: List, - factory: () -> T, - subclass: KClass? = null, - registry: Registry -): RegistryListBuilder -{ - return RegistryListBuilder(resetButtonKey, fieldNameKey, value, factory, subclass, registry) -} - -/** See [startSpecField]. Builds a list of keybind entries; [factory] supplies a value for newly-inserted rows. */ -fun ConfigEntryBuilder.startKeycodeList( - fieldNameKey: Component, - value: List, - factory: () -> ModifierKeyCode -): KeycodeListBuilder -{ - return KeycodeListBuilder(resetButtonKey, fieldNameKey, value, factory) -} - -/** See [startSpecField]. Builds a list of color entries; [factory] supplies a value for newly-inserted rows. */ -fun ConfigEntryBuilder.startColorList( - fieldNameKey: Component, - value: List, - factory: () -> Color -): ColorListBuilder -{ - return ColorListBuilder(resetButtonKey, fieldNameKey, value, factory) -} - -/** See [startSpecField]. Builds a `String`-keyed map of nested [DataSpec] entries. */ -fun ConfigEntryBuilder.startSpecMap( - fieldNameKey: Component, - value: Map, - factory: () -> T -): SpecMapBuilder -{ - return SpecMapBuilder(resetButtonKey, fieldNameKey, value, factory) -} - -/** See [startSpecField]. Builds a `String`-keyed map of [registry] entries; [factory] supplies a value for newly-inserted rows. */ -fun ConfigEntryBuilder.startRegistryMap( - fieldNameKey: Component, - value: Map, - factory: () -> T, - subclass: KClass? = null, - registry: Registry -): RegistryMapBuilder -{ - return RegistryMapBuilder(resetButtonKey, fieldNameKey, value, factory, subclass, registry) -} - -/** See [startSpecField]. Builds a `String`-keyed map of keybind entries; [factory] supplies a value for newly-inserted rows. */ -fun ConfigEntryBuilder.startKeycodeMap( - fieldNameKey: Component, - value: Map, - factory: () -> ModifierKeyCode -): KeycodeMapBuilder -{ - return KeycodeMapBuilder(resetButtonKey, fieldNameKey, value, factory) -} - -/** See [startSpecField]. Builds a `String`-keyed map of color entries; [factory] supplies a value for newly-inserted rows. */ -fun ConfigEntryBuilder.startColorMap( - fieldNameKey: Component, - value: Map, - factory: () -> Color -): ColorMapBuilder -{ - return ColorMapBuilder(resetButtonKey, fieldNameKey, value, factory) -} - -/** See [startSpecField]. Builds a `String`-keyed map of `Int` entries. */ -fun ConfigEntryBuilder.startIntMap(fieldNameKey: Component, value: Map): IntegerMapBuilder -{ - return IntegerMapBuilder(resetButtonKey, fieldNameKey, value) -} - -/** See [startSpecField]. Builds a `String`-keyed map of `Long` entries. */ -fun ConfigEntryBuilder.startLongMap(fieldNameKey: Component, value: Map): LongMapBuilder -{ - return LongMapBuilder(resetButtonKey, fieldNameKey, value) -} - -/** See [startSpecField]. Builds a `String`-keyed map of `Float` entries. */ -fun ConfigEntryBuilder.startFloatMap(fieldNameKey: Component, value: Map): FloatMapBuilder -{ - return FloatMapBuilder(resetButtonKey, fieldNameKey, value) -} - -/** See [startSpecField]. Builds a `String`-keyed map of `Double` entries. */ -fun ConfigEntryBuilder.startDoubleMap(fieldNameKey: Component, value: Map): DoubleMapBuilder -{ - return DoubleMapBuilder(resetButtonKey, fieldNameKey, value) -} - -/** See [startSpecField]. Builds a `String`-keyed map of `String` entries. */ -fun ConfigEntryBuilder.startStrMap(fieldNameKey: Component, value: Map): StringMapBuilder -{ - return StringMapBuilder(resetButtonKey, fieldNameKey, value) -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/entry/ConfigSpecEntry.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/entry/ConfigSpecEntry.kt deleted file mode 100644 index cdae6e293..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/entry/ConfigSpecEntry.kt +++ /dev/null @@ -1,111 +0,0 @@ -package net.kernelpanicsoft.archie.config.entry - -import com.google.common.collect.Lists -import me.shedaniel.clothconfig2.api.AbstractConfigListEntry -import net.kernelpanicsoft.archie.config.ConfigSpec -import net.kernelpanicsoft.archie.util.minecraftClient -import net.minecraft.client.Minecraft -import net.minecraft.client.gui.GuiGraphics -import net.minecraft.client.gui.components.AbstractWidget -import net.minecraft.client.gui.components.Button -import net.minecraft.client.gui.components.events.GuiEventListener -import net.minecraft.client.gui.narration.NarratableEntry -import net.minecraft.network.chat.Component -import java.util.* - -/** - * Cloth Config list entry rendering a single "Edit" button for [value] that, when clicked, opens - * `value.client.buildConfig(...)` - i.e. navigates from a container's screen into that - * [ConfigSpec]'s own screen. Built by `ConfigFieldBuilder`. - */ -class ConfigSpecEntry( - fieldName: Component, - buttonText: Component, - value: T, - requiresRestart: Boolean -) : AbstractConfigListEntry(fieldName, requiresRestart) -{ - private var value: T - private val buttonWidget: Button - private val widgets: MutableList - - init - { - this.value = value - this.buttonWidget = Button.builder( - buttonText - ) { - configScreen?.let {minecraftClient.setScreen(value.client.buildConfig(it))} - } - .bounds(0, 0, 150, 20).build() - this.widgets = - Lists.newArrayList(*arrayOf(this.buttonWidget)) - } - - override fun getValue(): T - { - return this.value - } - - fun setValue(value: T) - { - this.value = value - } - - override fun getDefaultValue(): Optional = Optional.empty() - - override fun render( - graphics: GuiGraphics, - index: Int, - y: Int, - x: Int, - entryWidth: Int, - entryHeight: Int, - mouseX: Int, - mouseY: Int, - isHovered: Boolean, - delta: Float - ) - { - super.render(graphics, index, y, x, entryWidth, entryHeight, mouseX, mouseY, isHovered, delta) - val window = Minecraft.getInstance().window - this.buttonWidget.active = this.isEditable - this.buttonWidget.y = y - - val displayedFieldName = this.displayedFieldName - if (minecraftClient.font.isBidirectional) - { - graphics.drawString( - minecraftClient.font, - displayedFieldName.visualOrderText, - window.guiScaledWidth - x - Minecraft.getInstance().font.width(displayedFieldName), - y + 6, - 16777215 - ) - this.buttonWidget.x = x - } else - { - graphics.drawString( - minecraftClient.font, - displayedFieldName.visualOrderText, - x, - y + 6, - this.preferredTextColor - ) - this.buttonWidget.x = x + entryWidth - 150 - } - - this.buttonWidget.setWidth(150) - this.buttonWidget.render(graphics, mouseX, mouseY, delta) - } - - override fun children(): MutableList - { - return this.widgets - } - - override fun narratables(): MutableList - { - return this.widgets - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/extensions.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/extensions.kt deleted file mode 100644 index 3af284e05..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/extensions.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.kernelpanicsoft.archie.config - -import me.shedaniel.clothconfig2.api.ConfigEntryBuilder -import me.shedaniel.clothconfig2.gui.entries.TextListEntry -import java.util.* - -/** - * Converts this string to `snake_case`, splitting on both spaces and camelCase humps. Used to - * derive field/category ids from titles and delegated property names (e.g. `"Max Items"` and - * `maxItems` both become `max_items`). - */ -fun String.toSnakeCase() = - split(" ") - .joinToString("") { word -> - word.replaceFirstChar { if (it.isLowerCase()) it.titlecase(Locale.getDefault()) else it.toString() } - } - .replace(humps, "_").lowercase() - -private val humps = "(?<=.)(?=\\p{Upper})".toRegex() - -/** A comment-list entry id paired with a factory for the Cloth Config [TextListEntry] it renders as. */ -typealias Comment = Pair TextListEntry> \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/Json5ConfigSerializer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/Json5ConfigSerializer.kt deleted file mode 100644 index 73d44d8df..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/Json5ConfigSerializer.kt +++ /dev/null @@ -1,31 +0,0 @@ -package net.kernelpanicsoft.archie.config.serializer - -import net.kernelpanicsoft.archie.config.ConfigSpec -import net.kernelpanicsoft.archie.config.IConfigSerializer -import io.github.xn32.json5k.Json5 -import java.nio.file.Path - - -/** [IConfigSerializer] for the JSON5 format (JSON with comments). Archie's default on Fabric. */ -object Json5ConfigSerializer : IConfigSerializer -{ - private val json5 = Json5 { - prettyPrint = true - quoteMemberNames = true - encodeDefaults = true - } - override fun configPath(config: ConfigSpec, configFolder: Path): Path - { - return configFolder.resolve("${config.filename}.json5") - } - - override fun loadString(config: ConfigSpec, string: String) - { - json5.decodeFromString(config.serializer, string) - } - - override fun saveString(config: ConfigSpec): String - { - return json5.encodeToString(config.serializer, config) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/JsonConfigSerializer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/JsonConfigSerializer.kt deleted file mode 100644 index 1c1f54854..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/JsonConfigSerializer.kt +++ /dev/null @@ -1,32 +0,0 @@ -package net.kernelpanicsoft.archie.config.serializer - -import net.kernelpanicsoft.archie.config.ConfigSpec -import net.kernelpanicsoft.archie.config.IConfigSerializer -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.json.Json -import java.nio.file.Path - -/** [IConfigSerializer] for plain JSON (no comments). Not used by default on any platform - opt in explicitly by overriding [ConfigSpec.fileSerializer]. */ -object JsonConfigSerializer : IConfigSerializer -{ - @OptIn(ExperimentalSerializationApi::class) - private val json = Json { - prettyPrint = true - prettyPrintIndent = "\t" - ignoreUnknownKeys = true - } - override fun configPath(config: ConfigSpec, configFolder: Path): Path - { - return configFolder.resolve("${config.filename}.json") - } - - override fun loadString(config: ConfigSpec, string: String) - { - json.decodeFromString(config.serializer, string) - } - - override fun saveString(config: ConfigSpec): String - { - return json.encodeToString(config.serializer, config) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/NullConfigSerializer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/NullConfigSerializer.kt deleted file mode 100644 index 2b01c49b9..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/NullConfigSerializer.kt +++ /dev/null @@ -1,32 +0,0 @@ -package net.kernelpanicsoft.archie.config.serializer - -import net.kernelpanicsoft.archie.config.ConfigSpec -import net.kernelpanicsoft.archie.config.IConfigSerializer -import java.nio.file.Path - -/** - * No-op [IConfigSerializer]: [load] and [save] do nothing, and the string-based methods all throw. - * Useful as a [ConfigSpec.fileSerializer] override for a spec that should never persist to disk - * (e.g. an in-memory-only or test config). - */ -object NullConfigSerializer : IConfigSerializer -{ - override fun configPath(config: ConfigSpec, configFolder: Path): Path - { - throw UnsupportedOperationException() - } - - override fun loadString(config: ConfigSpec, string: String) - { - throw UnsupportedOperationException() - } - - override fun saveString(config: ConfigSpec): String - { - throw UnsupportedOperationException() - } - - override fun load(config: ConfigSpec, configFolder: Path) = Unit - - override fun save(config: ConfigSpec, configFolder: Path) = Unit -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/TomlConfigSerializer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/TomlConfigSerializer.kt deleted file mode 100644 index 212a45551..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/TomlConfigSerializer.kt +++ /dev/null @@ -1,32 +0,0 @@ -package net.kernelpanicsoft.archie.config.serializer - -import net.kernelpanicsoft.archie.config.ConfigSpec -import net.kernelpanicsoft.archie.config.IConfigSerializer -import net.peanuuutz.tomlkt.Toml -import net.peanuuutz.tomlkt.TomlIndentation -import java.nio.file.Path - -/** [IConfigSerializer] for the TOML format. Archie's default on NeoForge. */ -object TomlConfigSerializer : IConfigSerializer -{ - private val toml = Toml { - ignoreUnknownKeys = true - indentation = TomlIndentation.Tab - } - - override fun configPath(config: ConfigSpec, configFolder: Path): Path - { - return configFolder.resolve("${config.filename}.toml") - } - - override fun loadString(config: ConfigSpec, string: String) - { - toml.decodeFromString(config.serializer, string) - } - - override fun saveString(config: ConfigSpec): String - { - return toml.encodeToString(config.serializer, config) - } - -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt deleted file mode 100644 index b8aa1dd8c..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt +++ /dev/null @@ -1,322 +0,0 @@ -package net.kernelpanicsoft.archie.data - -import net.kernelpanicsoft.archie.data.client.ALanguageProvider -import net.kernelpanicsoft.archie.data.client.model.ABlockModelProvider -import net.kernelpanicsoft.archie.data.client.model.ABlockStateProvider -import net.kernelpanicsoft.archie.data.client.model.AItemModelProvider -import net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider -import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider -import dev.architectury.platform.Mod -import net.minecraft.core.HolderLookup -import net.minecraft.data.DataProvider -import net.minecraft.data.PackOutput -import net.minecraft.data.recipes.RecipeOutput -import java.util.concurrent.CompletableFuture - -/** - * Base class for a platform's datagen entrypoint, providing a small DSL for registering - * [DataProvider]s without needing to interact with architectury's `DataGeneratorPlugin` - * directly. - * - * Providers are grouped by [client] and [common] since client-only providers (e.g. models, - * languages) must be skipped on a dedicated server datagen run and vice versa; [isClient] and - * [isServer] gate whether a provider actually runs based on the `archie.datagen.client`/ - * `archie.datagen.server` system properties set by the datagen run configuration. - * - * Loader modules implement [addProvider] on top of their platform's data generator and then - * invoke this generator, typically as `ArchieDatagen(mod) { client { ... }; common { ... } }`. - */ -@Suppress("MemberVisibilityCanBePrivate", "unused") -abstract class ADataGenerator -{ - /** Whether client-only providers should run, from the `archie.datagen.client` system property. */ - val isClient: Boolean - get() = System.getProperty("archie.datagen.client").toBoolean() - - /** Whether server-only providers should run, from the `archie.datagen.server` system property. */ - val isServer: Boolean - get() = System.getProperty("archie.datagen.server").toBoolean() - - abstract val mod: Mod - - /** - * Registers [factory] with the underlying platform data generator, running it only when - * [run] is `true`, and returns the constructed provider so it can be reused (e.g. an item - * tags provider depending on a previously created block tags provider). - */ - abstract fun addProvider( - run: Boolean = true, - factory: ARegistryAwareDataProviderFactory - ): T - - /** [addProvider] overload for providers that don't need access to [HolderLookup.Provider]. */ - fun addProvider(run: Boolean = true, factory: ADataProviderFactory): T - { - return addProvider(run) { output, _ -> - factory(output) - } - } - - /** Registers client-only providers (models, languages) declared in [block] via [Client]. */ - fun client(block: Client.() -> Unit) - { - Client().apply(block) - } - - /** Registers server-only providers (tags, recipes) declared in [block] via [Common]. */ - fun common(block: Common.() -> Unit) - { - Common().apply(block) - } - - operator fun invoke(block: ADataGenerator.() -> Unit) = apply(block) - - /** Factory for a [DataProvider] that only needs a [PackOutput] to be constructed. */ - fun interface ADataProviderFactory - { - operator fun invoke(output: PackOutput): T - } - - /** Factory for a [DataProvider] that also needs the registry [HolderLookup.Provider] future. */ - fun interface ARegistryAwareDataProviderFactory - { - operator fun invoke(output: PackOutput, registries: CompletableFuture): T - } - - /** Factory for an [ATagsProvider.ItemTagsProvider] that depends on an existing block tags provider. */ - fun interface ItemTagsDataProviderFactory - { - operator fun invoke( - output: PackOutput, - registries: CompletableFuture, - blockTagsProvider: ATagsProvider.BlockTagsProvider - ): ATagsProvider.ItemTagsProvider - } - - /** DSL scope for registering client-side providers; see [ADataGenerator.client]. */ - inner class Client - { - /** Registers an [ALanguageProvider] for [locale] that generates translations in [block]. */ - fun languages(locale: String = "en_us", block: ALanguageProvider.() -> Unit): ALanguageProvider - { - return languages { packOutput -> - object : ALanguageProvider(packOutput, mod, false, locale) - { - override fun generate() - { - this.block() - } - } - } - } - - fun languages(constructor: ADataProviderFactory): ALanguageProvider - { - return addProvider(isClient, constructor) - } - - /** Registers an [ABlockModelProvider] that generates block models in [block]. */ - fun blockModels(block: ABlockModelProvider.() -> Unit): ABlockModelProvider - { - return blockModels { packOutput -> - object : ABlockModelProvider(packOutput, mod, false) - { - override fun generate() - { - this.block() - } - } - } - } - - fun blockModels(constructor: ADataProviderFactory): ABlockModelProvider - { - return addProvider(isClient, constructor) - } - - /** Registers an [AItemModelProvider] that generates item models in [block]. */ - fun itemModels(block: AItemModelProvider.() -> Unit): AItemModelProvider - { - return itemModels { packOutput -> - object : AItemModelProvider(packOutput, mod, false) - { - override fun generate() - { - this.block() - } - } - } - } - - fun itemModels(constructor: ADataProviderFactory): AItemModelProvider - { - return addProvider(isClient, constructor) - } - - /** Registers an [ABlockStateProvider] that generates blockstate JSONs in [block]. */ - fun blockStates(block: ABlockStateProvider.() -> Unit): ABlockStateProvider - { - return blockStates { packOutput -> - object : ABlockStateProvider(packOutput, mod, false) - { - override fun generate() - { - this.block() - } - } - } - } - - fun blockStates(constructor: ADataProviderFactory): ABlockStateProvider - { - return addProvider(isClient, constructor) - } - } - - /** DSL scope for registering server-side providers; see [ADataGenerator.common]. */ - inner class Common - { - /** The block tags provider registered via [blockTags], if any; used by [itemTags] to derive item tags from block tags. */ - lateinit var blockTagsProvider: ATagsProvider.BlockTagsProvider - - /** Registers an [ATagsProvider.BlockTagsProvider] that declares block tags in [block]. */ - fun blockTags(block: ATagsProvider.BlockTagsProvider.(registries: HolderLookup.Provider) -> Unit): ATagsProvider.BlockTagsProvider - { - return blockTags { packOutput, registries -> - object : ATagsProvider.BlockTagsProvider(packOutput, mod, registries, false) - { - override fun generate(registries: HolderLookup.Provider) - { - this.block(registries) - } - } - } - } - - fun blockTags(constructor: ARegistryAwareDataProviderFactory): ATagsProvider.BlockTagsProvider - { - return addProvider(isServer, constructor).also { - blockTagsProvider = it - } - } - - /** - * Registers an [ATagsProvider.ItemTagsProvider] that declares item tags in [block]. - * Constructs it with [blockTagsProvider] when a block tags provider was already - * registered via [blockTags], enabling `copy(blockTag, itemTag)`. - */ - fun itemTags(block: ATagsProvider.ItemTagsProvider.(registries: HolderLookup.Provider) -> Unit): ATagsProvider.ItemTagsProvider - { - return if (::blockTagsProvider.isInitialized) - itemTags { packOutput, registries, blockTagsProvider -> - object : ATagsProvider.ItemTagsProvider(packOutput, mod, registries, blockTagsProvider, false) - { - override fun generate(registries: HolderLookup.Provider) - { - this.block(registries) - } - } - } - else - itemTags { packOutput, registries -> - object : ATagsProvider.ItemTagsProvider(packOutput, mod, registries, false) - { - override fun generate(registries: HolderLookup.Provider) - { - this.block(registries) - } - } - } - } - - fun itemTags(constructor: ARegistryAwareDataProviderFactory): ATagsProvider.ItemTagsProvider - { - return addProvider(isServer, constructor) - } - - fun itemTags(constructor: ItemTagsDataProviderFactory): ATagsProvider.ItemTagsProvider - { - if (!::blockTagsProvider.isInitialized) - throw IllegalStateException("You did not register a block tags provider. you must do that to use this overload") - return addProvider(isServer) { packOutput, registries -> - constructor(packOutput, registries, blockTagsProvider) - } - } - - /** Registers an [ATagsProvider.BiomeTagsProvider] that declares biome tags in [block]. */ - fun biomeTags(block: ATagsProvider.BiomeTagsProvider.(registries: HolderLookup.Provider) -> Unit): ATagsProvider.BiomeTagsProvider - { - return biomeTags { packOutput, registries -> - object : ATagsProvider.BiomeTagsProvider(packOutput, mod, registries, false) - { - override fun generate(registries: HolderLookup.Provider) - { - this.block(registries) - } - } - } - } - - fun biomeTags(constructor: ARegistryAwareDataProviderFactory): ATagsProvider.BiomeTagsProvider - { - return addProvider(isServer, constructor) - } - - /** Registers an [ATagsProvider.EntityTypeTagsProvider] that declares entity type tags in [block]. */ - fun entityTags(block: ATagsProvider.EntityTypeTagsProvider.(registries: HolderLookup.Provider) -> Unit): ATagsProvider.EntityTypeTagsProvider - { - return entityTags { packOutput, registries -> - object : ATagsProvider.EntityTypeTagsProvider(packOutput, mod, registries, false) - { - override fun generate(registries: HolderLookup.Provider) - { - this.block(registries) - } - } - } - } - - fun entityTags(constructor: ARegistryAwareDataProviderFactory): ATagsProvider.EntityTypeTagsProvider - { - return addProvider(isServer, constructor) - } - - /** Registers an [ATagsProvider.FluidTagsProvider] that declares fluid tags in [block]. */ - fun fluidTags(block: ATagsProvider.FluidTagsProvider.(registries: HolderLookup.Provider) -> Unit): ATagsProvider.FluidTagsProvider - { - return fluidTags { packOutput, registries -> - object : ATagsProvider.FluidTagsProvider(packOutput, mod, registries, false) - { - override fun generate(registries: HolderLookup.Provider) - { - this.block(registries) - } - } - } - } - - fun fluidTags(constructor: ARegistryAwareDataProviderFactory): ATagsProvider.FluidTagsProvider - { - return addProvider(isServer, constructor) - } - - /** Registers an [ARecipeProvider] that declares recipes via [block]. */ - fun recipes(block: ARecipeProvider.(recipeOutput: RecipeOutput) -> Unit): ARecipeProvider - { - return addProvider(isServer) { packOutput, registries -> - return@addProvider object : ARecipeProvider(packOutput, mod, registries, false) - { - override fun generate(recipeOutput: RecipeOutput) - { - this.block(recipeOutput) - } - } - } - } - - fun recipes(constructor: ARegistryAwareDataProviderFactory): ARecipeProvider - { - return addProvider(isServer, constructor) - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt deleted file mode 100644 index 94c57929b..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.kernelpanicsoft.archie.data - -/** - * Cross-loader switch reporting whether the current run is a datagen run. - * - * Loader implementations resolve [isDataGen] from run configuration system properties set by - * the `runDatagen` Gradle tasks. - */ -expect object ADataGeneratorPlatform -{ - val isDataGen: Boolean -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt deleted file mode 100644 index f0d798256..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt +++ /dev/null @@ -1,20 +0,0 @@ -package net.kernelpanicsoft.archie.data - -import net.kernelpanicsoft.archie.events.AEvents -import net.kernelpanicsoft.archie.events.AEvents.GatherDataHandler -import net.kernelpanicsoft.archie.events.AEventObject -import dev.architectury.event.Event -import dev.architectury.platform.Mod - -/** - * Convenience [AEventObject] base for hooking into [AEvents.GATHER_DATA], the event fired by - * the loader during a datagen run. Implement [handler] to build and run an [ADataGenerator]. - */ -abstract class ADatagenEventObject(mod: Mod) : - AEventObject( - mod - ) -{ - override val event: Event = AEvents.GATHER_DATA - override val handlerConstructor: GatherDataHandler.Companion = GatherDataHandler.Companion -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/IADataProvider.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/IADataProvider.kt deleted file mode 100644 index 9aa234512..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/IADataProvider.kt +++ /dev/null @@ -1,41 +0,0 @@ -package net.kernelpanicsoft.archie.data - -import dev.architectury.platform.Mod -import dev.architectury.platform.Platform -import net.minecraft.data.DataProvider -import net.minecraft.data.PackOutput -import net.minecraft.resources.ResourceLocation - -/** - * Common contract shared by Archie's [DataProvider] implementations, adding the current [mod] - * and a couple of ID/path helpers used when generating output files. - */ -interface IADataProvider : DataProvider -{ - val output: PackOutput - - /** The mod this provider is generating data for. */ - val mod: Mod - - /** Whether datagen should abort with an error instead of logging and continuing. */ - val exitOnError: Boolean - - /** Builds a [ResourceLocation] in [mod]'s namespace, e.g. for output file paths. */ - fun modLoc(name: String): ResourceLocation - { - return ResourceLocation.fromNamespaceAndPath(mod.modId, name) - } - - /** Builds a [ResourceLocation] in the `minecraft` namespace. */ - fun mcLoc(name: String): ResourceLocation - { - return ResourceLocation.withDefaultNamespace(name) - } - - /** - * Formats a provider display [name] (used for `getName()`), prefixing it with [mod]'s name - * on loaders other than Fabric so providers from different mods are distinguishable in - * datagen logs. - */ - fun format(name: String): String = if (Platform.isFabric()) name else "${mod.name}/$name" -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/ALanguageProvider.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/ALanguageProvider.kt deleted file mode 100644 index 75213be0c..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/ALanguageProvider.kt +++ /dev/null @@ -1,154 +0,0 @@ -package net.kernelpanicsoft.archie.data.client - -import com.google.gson.JsonObject -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.data.IADataProvider -import dev.architectury.platform.Mod -import net.minecraft.data.CachedOutput -import net.minecraft.data.DataProvider -import net.minecraft.data.PackOutput -import net.minecraft.world.effect.MobEffect -import net.minecraft.world.entity.EntityType -import net.minecraft.world.item.Item -import net.minecraft.world.item.ItemStack -import net.minecraft.world.item.enchantment.Enchantment -import net.minecraft.world.level.block.Block -import java.nio.file.Path -import java.util.* -import java.util.concurrent.CompletableFuture -import java.util.function.Supplier -import kotlin.system.exitProcess - -/** - * Datagen provider that builds a `assets//lang/.json` translation file. - * Implement [generate] and call the `add*` helpers to register translation keys; use via - * [net.kernelpanicsoft.archie.data.ADataGenerator.Client.languages]. - */ -@Suppress("unused") -abstract class ALanguageProvider( - override val output: PackOutput, - override val mod: Mod, - override val exitOnError: Boolean, - private val locale: String = "en_us" -) : - IADataProvider -{ - private val data: MutableMap = TreeMap() - - /** Called once during [run] to register translations via the `add*` helpers. */ - protected abstract fun generate() - - override fun run(cache: CachedOutput): CompletableFuture<*> - { - runCatching { - generate() - }.onFailure { - Archie.LOGGER.error( - "Data Provider $name failed with exception: ${it.message}\n" + - "Stacktrace: ${it.stackTraceToString()}" - ) - if (exitOnError) exitProcess(-1) - } - if (data.isNotEmpty()) return save( - cache, - output.getOutputFolder(PackOutput.Target.RESOURCE_PACK).resolve(this.mod.modId).resolve("lang").resolve( - this.locale + ".json" - ) - ) - - - return CompletableFuture.allOf() - } - - override fun getName(): String = format("Languages - $locale") - - private fun save(cache: CachedOutput, target: Path): CompletableFuture<*> - { - // TODO: DataProvider.saveStable handles the caching and hashing already, but creating the JSON Object this way seems unreliable. -C - val json = JsonObject() - data.forEach { (property: String?, value: String?) -> - json.addProperty( - property, - value - ) - } - - return DataProvider.saveStable(cache, json, target) - } - - /** Translates a deferred [Block] to [name]; see [add]. */ - fun addBlock(name: String, key: Supplier) - { - add(key.get(), name) - } - - /** Translates [key]'s `descriptionId` to [name]; see [add]. */ - fun add(key: Block, name: String) - { - add(key.descriptionId, name) - } - - /** Translates a deferred [Item] to [name]; see [add]. */ - fun addItem(name: String, key: Supplier) - { - add(key.get(), name) - } - - /** Translates [key]'s `descriptionId` to [name]; see [add]. */ - fun add(key: Item, name: String) - { - add(key.descriptionId, name) - } - - /** Translates a deferred [ItemStack] to [name]; see [add]. */ - fun addItemStack(name: String, key: Supplier) - { - add(key.get(), name) - } - - /** Translates [key]'s `descriptionId` to [name]; see [add]. */ - fun add(key: ItemStack, name: String) - { - add(key.descriptionId, name) - } - -// fun addEnchantment(name: String, key: Supplier) -// { -// add(key.get(), name) -// } -// -// fun add(key: Enchantment, name: String) -// { -// add(key.descriptionId, name) -// } - - /** Translates a deferred [MobEffect] to [name]; see [add]. */ - fun addEffect(name: String, key: Supplier) - { - add(key.get(), name) - } - - /** Translates [key]'s `descriptionId` to [name]; see [add]. */ - fun add(key: MobEffect, name: String) - { - add(key.descriptionId, name) - } - - /** Translates a deferred [EntityType] to [name]; see [add]. */ - fun addEntityType(name: String, key: Supplier>) - { - add(key.get(), name) - } - - /** Translates [key]'s `descriptionId` to [name]; see [add]. */ - fun add(key: EntityType<*>, name: String) - { - add(key.descriptionId, name) - } - - /** Registers a raw translation [key] to [value]. Throws if [key] is already registered. */ - fun add(key: String, value: String) - { - check(data.put(key, value) == null) { "Duplicate translation key $key" } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelBuilder.kt deleted file mode 100644 index 47eb7de99..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelBuilder.kt +++ /dev/null @@ -1,8 +0,0 @@ -package net.kernelpanicsoft.archie.data.client.model - -import net.minecraft.resources.ResourceLocation - -/** [AModelBuilder] for a block model at [outputLocation], produced by [ABlockModelProvider]. */ -class ABlockModelBuilder( - outputLocation: ResourceLocation -) : AModelBuilder(outputLocation) \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelProvider.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelProvider.kt deleted file mode 100644 index 0d57bafe7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelProvider.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.kernelpanicsoft.archie.data.client.model - -import dev.architectury.platform.Mod -import net.minecraft.data.PackOutput - -/** [AModelProvider] that generates block models under `models/block/`. */ -abstract class ABlockModelProvider(output: PackOutput, mod: Mod, exitOnError: Boolean) : - AModelProvider(output, mod, BLOCK_FOLDER, ::ABlockModelBuilder, exitOnError) -{ - override fun getName(): String = format("Block Models") -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockStateProvider.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockStateProvider.kt deleted file mode 100644 index 4ad796513..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockStateProvider.kt +++ /dev/null @@ -1,1583 +0,0 @@ -package net.kernelpanicsoft.archie.data.client.model - -import com.google.common.base.Preconditions -import com.google.gson.* -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.data.IADataProvider -import dev.architectury.platform.Mod -import net.minecraft.core.Direction -import net.minecraft.core.registries.BuiltInRegistries -import net.minecraft.data.CachedOutput -import net.minecraft.data.DataProvider -import net.minecraft.data.PackOutput -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.level.block.* -import net.minecraft.world.level.block.state.BlockState -import net.minecraft.world.level.block.state.properties.* -import org.apache.logging.log4j.LogManager -import org.apache.logging.log4j.Logger -import org.jetbrains.annotations.VisibleForTesting -import java.util.* -import java.util.concurrent.CompletableFuture -import java.util.function.Consumer -import java.util.function.Function -import kotlin.system.exitProcess - -/** - * Datagen provider that builds `blockstates`/`*.json` files, along with the block/item models - * they reference via the embedded [blockModels] and [itemModels] providers. - * - * Implement [generate] and, for each block, call either [getVariantBuilder] (for a simple - * `variants` blockstate) or [getMultipartBuilder] (for a `multipart` blockstate), then use the - * `*Block`/`*BlockWithRenderType` helpers (e.g. `stairsBlock`, `slabBlock`, `fenceBlock`, - * `signBlock`) or [simpleBlock] to wire up standard model shapes. These helpers mirror - * NeoForge's vanilla `BlockStateProvider` datagen helpers, so their names/parameters match that - * API 1:1. Use via [net.kernelpanicsoft.archie.data.ADataGenerator.Client.blockStates] - */ -@Suppress("MemberVisibilityCanBePrivate", "unused") -abstract class ABlockStateProvider( - final override val output: PackOutput, - final override val mod: Mod, - final override val exitOnError: Boolean -) : IADataProvider -{ - @VisibleForTesting - protected val registeredBlocks: MutableMap = - LinkedHashMap() - - private val blockModels: ABlockModelProvider = - object : ABlockModelProvider( - output, - mod, - exitOnError - ) - { - override fun run(cache: CachedOutput): CompletableFuture<*> - { - return CompletableFuture.allOf() - } - - override fun generate() = Unit - } - private val itemModels: AItemModelProvider = - object : AItemModelProvider( - output, - mod, - exitOnError - ) - { - override fun run(cache: CachedOutput): CompletableFuture<*> - { - return CompletableFuture.allOf() - } - - override fun generate() = Unit - } - - override fun run(cache: CachedOutput): CompletableFuture<*> - { - blockModels().clear() - itemModels().clear() - registeredBlocks.clear() - runCatching { - generate() - }.onFailure { - Archie.LOGGER.error( - "Data Provider $name failed with exception: ${it.message}\n" + - "Stacktrace: ${it.stackTraceToString()}" - ) - if (exitOnError) exitProcess(-1) - } - val futures: Array?> = arrayOfNulls(2 + registeredBlocks.size) - var i = 0 - futures[i++] = blockModels().generateAll(cache) - futures[i++] = itemModels().generateAll(cache) - for ((key, value) in registeredBlocks) - { - futures[i++] = saveBlockState(cache, value.toJson(), key) - } - return CompletableFuture.allOf(*futures) - } - - /** Called once during [run] to register blockstates via [getVariantBuilder]/[getMultipartBuilder]. */ - protected abstract fun generate() - - /** - * Gets (or creates) the [AVariantBlockStateBuilder] for block [b], applying [block] to it. - * Throws if [b] was already registered with [getMultipartBuilder] instead. - */ - fun getVariantBuilder(b: Block, block: AVariantBlockStateBuilder.() -> Unit = {}): AVariantBlockStateBuilder - { - if (registeredBlocks.containsKey(b)) - { - val old: IAGeneratedBlockState? = registeredBlocks[b] - Preconditions.checkState(old is AVariantBlockStateBuilder) - return (old as AVariantBlockStateBuilder).apply(block) - } else - { - val ret = AVariantBlockStateBuilder(b).apply(block) - registeredBlocks[b] = ret - return ret - } - } - - /** - * Gets (or creates) the [AMultiPartBlockStateBuilder] for block [b], applying [block] to it. - * Throws if [b] was already registered with [getVariantBuilder] instead. - */ - fun getMultipartBuilder(b: Block, block: AMultiPartBlockStateBuilder.() -> Unit = {}): AMultiPartBlockStateBuilder - { - if (registeredBlocks.containsKey(b)) - { - val old: IAGeneratedBlockState? = registeredBlocks[b] - Preconditions.checkState(old is AMultiPartBlockStateBuilder) - return (old as AMultiPartBlockStateBuilder).apply(block) - } else - { - val ret = AMultiPartBlockStateBuilder(b).apply(block) - registeredBlocks[b] = ret - return ret - } - } - - /** Applies [block] to the block model provider embedded in this blockstate provider. */ - fun blockModels(block: ABlockModelProvider.() -> Unit = {}): ABlockModelProvider - { - return runCatching { - blockModels.apply(block) - }.onFailure { - Archie.LOGGER.error( - "Data Provider $name failed with exception: ${it.message}\n" + - "Stacktrace: ${it.stackTraceToString()}" - ) - if (exitOnError) exitProcess(-1) - }.getOrElse { - blockModels.clear() - blockModels - } - } - - /** Applies [block] to the item model provider embedded in this blockstate provider. */ - fun itemModels(block: AItemModelProvider.() -> Unit = {}): AItemModelProvider - { - return runCatching { - itemModels.apply(block) - }.onFailure { - Archie.LOGGER.error( - "Data Provider $name failed with exception: ${it.message}\n" + - "Stacktrace: ${it.stackTraceToString()}" - ) - if (exitOnError) exitProcess(-1) - }.getOrElse { - itemModels.clear() - itemModels - } - } - - private fun key(block: Block): ResourceLocation - { - return BuiltInRegistries.BLOCK.getKey(block) - } - - private fun name(block: Block): String - { - return key(block).path - } - - /** Returns the conventional `block/` texture location for [block]. */ - fun blockTexture(block: Block): ResourceLocation - { - val name = key(block) - return ResourceLocation.fromNamespaceAndPath( - name.namespace, - AModelProvider.BLOCK_FOLDER + "/" + name.path - ) - } - - private fun extend(rl: ResourceLocation, suffix: String): ResourceLocation - { - return ResourceLocation.fromNamespaceAndPath(rl.namespace, rl.path + suffix) - } - - /** Creates a `block/cube_all` model for [block] using [blockTexture] on every face. */ - fun cubeAll(block: Block): AModelFile - { - return blockModels().cubeAll(name(block), blockTexture(block)) - } - - /** Registers a single-variant blockstate for [block] using [expander] to derive [AConfiguredModel]s from [cubeAll]. */ - fun simpleBlock( - block: Block, - expander: Function> - ) - { - simpleBlock(block, *expander.apply(cubeAll(block))) - } - - /** Registers a single-variant blockstate for [block] pointing at [model] (defaults to [cubeAll]). */ - @JvmOverloads - fun simpleBlock(block: Block, model: AModelFile = cubeAll(block)) - { - simpleBlock(block, AConfiguredModel(model)) - } - - /** Sets [block]'s item model to inherit from [model] with no extra elements/overrides. */ - fun simpleBlockItem(block: Block, model: AModelFile) - { - itemModels().getBuilder(key(block).path).parent(model) - } - - /** Combines [simpleBlock] and [simpleBlockItem] for [block] against the same [model]. */ - fun simpleBlockWithItem(block: Block, model: AModelFile = cubeAll(block)) - { - simpleBlock(block, model) - simpleBlockItem(block, model) - } - - /** Registers a single-variant blockstate for [block] that randomly picks between [models]. */ - fun simpleBlock(block: Block, vararg models: AConfiguredModel) - { - getVariantBuilder(block) - .partialState().setModels(*models) - } - - fun logBlock(block: RotatedPillarBlock) - { - axisBlock(block, blockTexture(block), extend(blockTexture(block), "_top")) - } - - @JvmOverloads - fun axisBlock(block: RotatedPillarBlock, baseName: ResourceLocation = blockTexture(block)) - { - axisBlock(block, extend(baseName, "_side"), extend(baseName, "_end")) - } - - fun axisBlock(block: RotatedPillarBlock, side: ResourceLocation, end: ResourceLocation) - { - axisBlock( - block, - blockModels().cubeColumn(name(block), side, end), - blockModels().cubeColumnHorizontal(name(block) + "_horizontal", side, end) - ) - } - - fun axisBlockWithRenderType(block: RotatedPillarBlock, renderType: String) - { - axisBlockWithRenderType(block, blockTexture(block), renderType) - } - - fun logBlockWithRenderType(block: RotatedPillarBlock, renderType: String) - { - axisBlockWithRenderType(block, blockTexture(block), extend(blockTexture(block), "_top"), renderType) - } - - fun axisBlockWithRenderType(block: RotatedPillarBlock, baseName: ResourceLocation, renderType: String) - { - axisBlockWithRenderType(block, extend(baseName, "_side"), extend(baseName, "_end"), renderType) - } - - fun axisBlockWithRenderType( - block: RotatedPillarBlock, - side: ResourceLocation, - end: ResourceLocation, - renderType: String - ) - { - axisBlock( - block, - blockModels().cubeColumn(name(block), side, end).renderType(renderType), - blockModels().cubeColumnHorizontal(name(block) + "_horizontal", side, end).renderType(renderType) - ) - } - - fun axisBlockWithRenderType(block: RotatedPillarBlock, renderType: ResourceLocation) - { - axisBlockWithRenderType(block, blockTexture(block), renderType) - } - - fun logBlockWithRenderType(block: RotatedPillarBlock, renderType: ResourceLocation) - { - axisBlockWithRenderType(block, blockTexture(block), extend(blockTexture(block), "_top"), renderType) - } - - fun axisBlockWithRenderType(block: RotatedPillarBlock, baseName: ResourceLocation, renderType: ResourceLocation) - { - axisBlockWithRenderType(block, extend(baseName, "_side"), extend(baseName, "_end"), renderType) - } - - fun axisBlockWithRenderType( - block: RotatedPillarBlock, - side: ResourceLocation, - end: ResourceLocation, - renderType: ResourceLocation - ) - { - axisBlock( - block, - blockModels().cubeColumn(name(block), side, end).renderType(renderType), - blockModels().cubeColumnHorizontal(name(block) + "_horizontal", side, end).renderType(renderType) - ) - } - - fun axisBlock( - block: RotatedPillarBlock, - vertical: AModelFile, - horizontal: AModelFile - ) - { - getVariantBuilder(block) - .partialState().with(RotatedPillarBlock.AXIS, Direction.Axis.Y) - .modelForState().modelFile(vertical).addModel() - .partialState().with(RotatedPillarBlock.AXIS, Direction.Axis.Z) - .modelForState().modelFile(horizontal).rotationX(90).addModel() - .partialState().with(RotatedPillarBlock.AXIS, Direction.Axis.X) - .modelForState().modelFile(horizontal).rotationX(90).rotationY(90).addModel() - } - - fun horizontalBlock(block: Block, side: ResourceLocation, front: ResourceLocation, top: ResourceLocation) - { - horizontalBlock(block, blockModels().orientable(name(block), side, front, top)) - } - - @JvmOverloads - fun horizontalBlock( - block: Block, - model: AModelFile, - angleOffset: Int = DEFAULT_ANGLE_OFFSET - ) - { - horizontalBlock( - block, - { model }, - angleOffset - ) - } - - @JvmOverloads - fun horizontalBlock( - block: Block, - modelFunc: Function, - angleOffset: Int = DEFAULT_ANGLE_OFFSET - ) - { - getVariantBuilder(block) - .forAllStates { state: BlockState -> - AConfiguredModel.builder() - .modelFile(modelFunc.apply(state)) - .rotationY( - (state.getValue(BlockStateProperties.HORIZONTAL_FACING) - .toYRot().toInt() + angleOffset) % 360 - ) - .build() - } - } - - @JvmOverloads - fun horizontalFaceBlock( - block: Block, - model: AModelFile, - angleOffset: Int = DEFAULT_ANGLE_OFFSET - ) - { - horizontalFaceBlock( - block, - { model }, - angleOffset - ) - } - - @JvmOverloads - fun horizontalFaceBlock( - block: Block, - modelFunc: Function, - angleOffset: Int = DEFAULT_ANGLE_OFFSET - ) - { - getVariantBuilder(block) - .forAllStates { state: BlockState -> - AConfiguredModel.builder() - .modelFile(modelFunc.apply(state)) - .rotationX(state.getValue(BlockStateProperties.ATTACH_FACE).ordinal * 90) - .rotationY( - ((state.getValue(BlockStateProperties.HORIZONTAL_FACING) - .toYRot() - .toInt() + angleOffset) + (if (state.getValue( - BlockStateProperties.ATTACH_FACE - ) == AttachFace.CEILING - ) 180 else 0)) % 360 - ) - .build() - } - } - - @JvmOverloads - fun directionalBlock( - block: Block, - model: AModelFile, - angleOffset: Int = DEFAULT_ANGLE_OFFSET - ) - { - directionalBlock( - block, - { model }, - angleOffset - ) - } - - @JvmOverloads - fun directionalBlock( - block: Block, - modelFunc: Function, - angleOffset: Int = DEFAULT_ANGLE_OFFSET - ) - { - getVariantBuilder(block) - .forAllStates { state: BlockState -> - val dir = - state.getValue(BlockStateProperties.FACING) - AConfiguredModel.builder() - .modelFile(modelFunc.apply(state)) - .rotationX( - if (dir == Direction.DOWN) 180 else if (dir.axis.isHorizontal) 90 else 0 - ) - .rotationY( - if (dir.axis.isVertical) 0 else ((dir.toYRot() - .toInt()) + angleOffset) % 360 - ) - .build() - } - } - - fun stairsBlock(block: StairBlock, texture: ResourceLocation) - { - stairsBlock(block, texture, texture, texture) - } - - fun stairsBlock(block: StairBlock, name: String, texture: ResourceLocation) - { - stairsBlock(block, name, texture, texture, texture) - } - - fun stairsBlock(block: StairBlock, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation) - { - stairsBlockInternal(block, key(block).toString(), side, bottom, top) - } - - fun stairsBlock( - block: StairBlock, - name: String, - side: ResourceLocation, - bottom: ResourceLocation, - top: ResourceLocation - ) - { - stairsBlockInternal(block, name + "_stairs", side, bottom, top) - } - - fun stairsBlockWithRenderType(block: StairBlock, texture: ResourceLocation, renderType: String) - { - stairsBlockWithRenderType(block, texture, texture, texture, renderType) - } - - fun stairsBlockWithRenderType(block: StairBlock, name: String, texture: ResourceLocation, renderType: String) - { - stairsBlockWithRenderType(block, name, texture, texture, texture, renderType) - } - - fun stairsBlockWithRenderType( - block: StairBlock, - side: ResourceLocation, - bottom: ResourceLocation, - top: ResourceLocation, - renderType: String - ) - { - stairsBlockInternalWithRenderType( - block, - key(block).toString(), - side, - bottom, - top, - ResourceLocation.parse(renderType) - ) - } - - fun stairsBlockWithRenderType( - block: StairBlock, - name: String, - side: ResourceLocation, - bottom: ResourceLocation, - top: ResourceLocation, - renderType: String - ) - { - stairsBlockInternalWithRenderType( - block, - name + "_stairs", - side, - bottom, - top, - ResourceLocation.parse(renderType) - ) - } - - fun stairsBlockWithRenderType(block: StairBlock, texture: ResourceLocation, renderType: ResourceLocation) - { - stairsBlockWithRenderType(block, texture, texture, texture, renderType) - } - - fun stairsBlockWithRenderType( - block: StairBlock, - name: String, - texture: ResourceLocation, - renderType: ResourceLocation - ) - { - stairsBlockWithRenderType(block, name, texture, texture, texture, renderType) - } - - fun stairsBlockWithRenderType( - block: StairBlock, - side: ResourceLocation, - bottom: ResourceLocation, - top: ResourceLocation, - renderType: ResourceLocation - ) - { - stairsBlockInternalWithRenderType(block, key(block).toString(), side, bottom, top, renderType) - } - - fun stairsBlockWithRenderType( - block: StairBlock, - name: String, - side: ResourceLocation, - bottom: ResourceLocation, - top: ResourceLocation, - renderType: ResourceLocation - ) - { - stairsBlockInternalWithRenderType(block, name + "_stairs", side, bottom, top, renderType) - } - - private fun stairsBlockInternal( - block: StairBlock, - baseName: String, - side: ResourceLocation, - bottom: ResourceLocation, - top: ResourceLocation - ) - { - val stairs: AModelFile = - blockModels().stairs(baseName, side, bottom, top) - val stairsInner: AModelFile = - blockModels().stairsInner(baseName + "_inner", side, bottom, top) - val stairsOuter: AModelFile = - blockModels().stairsOuter(baseName + "_outer", side, bottom, top) - stairsBlock(block, stairs, stairsInner, stairsOuter) - } - - private fun stairsBlockInternalWithRenderType( - block: StairBlock, - baseName: String, - side: ResourceLocation, - bottom: ResourceLocation, - top: ResourceLocation, - renderType: ResourceLocation - ) - { - val stairs: AModelFile = - blockModels().stairs(baseName, side, bottom, top).renderType(renderType) - val stairsInner: AModelFile = - blockModels().stairsInner(baseName + "_inner", side, bottom, top).renderType(renderType) - val stairsOuter: AModelFile = - blockModels().stairsOuter(baseName + "_outer", side, bottom, top).renderType(renderType) - stairsBlock(block, stairs, stairsInner, stairsOuter) - } - - fun stairsBlock( - block: StairBlock, - stairs: AModelFile, - stairsInner: AModelFile, - stairsOuter: AModelFile - ) - { - getVariantBuilder(block) - .forAllStatesExcept({ state: BlockState -> - val facing = - state.getValue(StairBlock.FACING) - val half = - state.getValue(StairBlock.HALF) - val shape = - state.getValue(StairBlock.SHAPE) - var yRot = - facing.clockWise.toYRot().toInt() // Stairs model is rotated 90 degrees clockwise for some reason - if (shape == StairsShape.INNER_LEFT || shape == StairsShape.OUTER_LEFT) - { - yRot += 270 // Left facing stairs are rotated 90 degrees clockwise - } - if (shape != StairsShape.STRAIGHT && half == Half.TOP) - { - yRot += 90 // Top stairs are rotated 90 degrees clockwise - } - yRot %= 360 - val uvlock = - yRot != 0 || half == Half.TOP // Don't set uvlock for states that have no rotation - AConfiguredModel.builder() - .modelFile(if (shape == StairsShape.STRAIGHT) stairs else if (shape == StairsShape.INNER_LEFT || shape == StairsShape.INNER_RIGHT) stairsInner else stairsOuter) - .rotationX(if (half == Half.BOTTOM) 0 else 180) - .rotationY(yRot) - .uvLock(uvlock) - .build() - }, StairBlock.WATERLOGGED) - } - - fun slabBlock(block: SlabBlock, doubleslab: ResourceLocation, texture: ResourceLocation) - { - slabBlock(block, doubleslab, texture, texture, texture) - } - - fun slabBlock( - block: SlabBlock, - doubleslab: ResourceLocation, - side: ResourceLocation, - bottom: ResourceLocation, - top: ResourceLocation - ) - { - slabBlock( - block, - blockModels().slab(name(block), side, bottom, top), - blockModels().slabTop(name(block) + "_top", side, bottom, top), - blockModels().getExistingFile(doubleslab) - ) - } - - fun slabBlock( - block: SlabBlock, - bottom: AModelFile, - top: AModelFile, - doubleslab: AModelFile - ) - { - getVariantBuilder(block) - .partialState().with(SlabBlock.TYPE, SlabType.BOTTOM) - .addModels(AConfiguredModel(bottom)) - .partialState().with(SlabBlock.TYPE, SlabType.TOP) - .addModels(AConfiguredModel(top)) - .partialState().with(SlabBlock.TYPE, SlabType.DOUBLE) - .addModels(AConfiguredModel(doubleslab)) - } - - fun buttonBlock(block: ButtonBlock, texture: ResourceLocation) - { - val button: AModelFile = blockModels().button(name(block), texture) - val buttonPressed: AModelFile = - blockModels().buttonPressed(name(block) + "_pressed", texture) - buttonBlock(block, button, buttonPressed) - } - - fun buttonBlock( - block: ButtonBlock, - button: AModelFile, - buttonPressed: AModelFile - ) - { - getVariantBuilder(block).forAllStates(Function> { state: BlockState -> - val facing = - state.getValue(ButtonBlock.FACING) - val face = - state.getValue(ButtonBlock.FACE) - val powered = - state.getValue(ButtonBlock.POWERED) - AConfiguredModel.builder() - .modelFile(if (powered) buttonPressed else button) - .rotationX(if (face == AttachFace.FLOOR) 0 else if (face == AttachFace.WALL) 90 else 180) - .rotationY( - (if (face == AttachFace.CEILING) facing else facing.opposite).toYRot() - .toInt() - ) - .uvLock(face == AttachFace.WALL) - .build() - }) - } - - fun pressurePlateBlock(block: PressurePlateBlock, texture: ResourceLocation) - { - val pressurePlate: AModelFile = - blockModels().pressurePlate(name(block), texture) - val pressurePlateDown: AModelFile = - blockModels().pressurePlateDown(name(block) + "_down", texture) - pressurePlateBlock(block, pressurePlate, pressurePlateDown) - } - - fun pressurePlateBlock( - block: PressurePlateBlock, - pressurePlate: AModelFile, - pressurePlateDown: AModelFile - ) - { - getVariantBuilder(block) - .partialState().with(PressurePlateBlock.POWERED, true) - .addModels(AConfiguredModel(pressurePlateDown)) - .partialState().with(PressurePlateBlock.POWERED, false) - .addModels(AConfiguredModel(pressurePlate)) - } - - fun signBlock(signBlock: StandingSignBlock, wallSignBlock: WallSignBlock, texture: ResourceLocation) - { - val sign: AModelFile = blockModels().sign(name(signBlock), texture) - signBlock(signBlock, wallSignBlock, sign) - } - - fun signBlock( - signBlock: StandingSignBlock, - wallSignBlock: WallSignBlock, - sign: AModelFile - ) - { - simpleBlock(signBlock, sign) - simpleBlock(wallSignBlock, sign) - } - - fun fourWayBlock( - block: CrossCollisionBlock, - post: AModelFile, - side: AModelFile - ) - { - val builder: AMultiPartBlockStateBuilder = - getMultipartBuilder(block) - .part().modelFile(post).addModel().end() - fourWayMultipart(builder, side) - } - - fun fourWayMultipart( - builder: AMultiPartBlockStateBuilder, - side: AModelFile - ) - { - PipeBlock.PROPERTY_BY_DIRECTION.entries.forEach(Consumer> { e: Map.Entry -> - val dir = e.key - if (dir.axis.isHorizontal) - { - builder.part().modelFile(side) - .rotationY(((dir.toYRot().toInt()) + 180) % 360).uvLock(true).addModel() - .condition(e.value, true) - } - }) - } - - fun fenceBlock(block: FenceBlock, texture: ResourceLocation) - { - val baseName = key(block).toString() - fourWayBlock( - block, - blockModels().fencePost(baseName + "_post", texture), - blockModels().fenceSide(baseName + "_side", texture) - ) - } - - fun fenceBlock(block: FenceBlock, name: String, texture: ResourceLocation) - { - fourWayBlock( - block, - blockModels().fencePost(name + "_fence_post", texture), - blockModels().fenceSide(name + "_fence_side", texture) - ) - } - - fun fenceBlockWithRenderType(block: FenceBlock, texture: ResourceLocation, renderType: String) - { - val baseName = key(block).toString() - fourWayBlock( - block, - blockModels().fencePost(baseName + "_post", texture).renderType(renderType), - blockModels().fenceSide(baseName + "_side", texture).renderType(renderType) - ) - } - - fun fenceBlockWithRenderType(block: FenceBlock, name: String, texture: ResourceLocation, renderType: String) - { - fourWayBlock( - block, - blockModels().fencePost(name + "_fence_post", texture).renderType(renderType), - blockModels().fenceSide(name + "_fence_side", texture).renderType(renderType) - ) - } - - fun fenceBlockWithRenderType(block: FenceBlock, texture: ResourceLocation, renderType: ResourceLocation) - { - val baseName = key(block).toString() - fourWayBlock( - block, - blockModels().fencePost(baseName + "_post", texture).renderType(renderType), - blockModels().fenceSide(baseName + "_side", texture).renderType(renderType) - ) - } - - fun fenceBlockWithRenderType( - block: FenceBlock, - name: String, - texture: ResourceLocation, - renderType: ResourceLocation - ) - { - fourWayBlock( - block, - blockModels().fencePost(name + "_fence_post", texture).renderType(renderType), - blockModels().fenceSide(name + "_fence_side", texture).renderType(renderType) - ) - } - - fun fenceGateBlock(block: FenceGateBlock, texture: ResourceLocation) - { - fenceGateBlockInternal(block, key(block).toString(), texture) - } - - fun fenceGateBlock(block: FenceGateBlock, name: String, texture: ResourceLocation) - { - fenceGateBlockInternal(block, name + "_fence_gate", texture) - } - - fun fenceGateBlockWithRenderType(block: FenceGateBlock, texture: ResourceLocation, renderType: String) - { - fenceGateBlockInternalWithRenderType( - block, - key(block).toString(), - texture, - ResourceLocation.parse(renderType) - ) - } - - fun fenceGateBlockWithRenderType( - block: FenceGateBlock, - name: String, - texture: ResourceLocation, - renderType: String - ) - { - fenceGateBlockInternalWithRenderType( - block, - name + "_fence_gate", - texture, - ResourceLocation.parse(renderType) - ) - } - - fun fenceGateBlockWithRenderType(block: FenceGateBlock, texture: ResourceLocation, renderType: ResourceLocation) - { - fenceGateBlockInternalWithRenderType(block, key(block).toString(), texture, renderType) - } - - fun fenceGateBlockWithRenderType( - block: FenceGateBlock, - name: String, - texture: ResourceLocation, - renderType: ResourceLocation - ) - { - fenceGateBlockInternalWithRenderType(block, name + "_fence_gate", texture, renderType) - } - - private fun fenceGateBlockInternal(block: FenceGateBlock, baseName: String, texture: ResourceLocation) - { - val gate: AModelFile = blockModels().fenceGate(baseName, texture) - val gateOpen: AModelFile = - blockModels().fenceGateOpen(baseName + "_open", texture) - val gateWall: AModelFile = - blockModels().fenceGateWall(baseName + "_wall", texture) - val gateWallOpen: AModelFile = - blockModels().fenceGateWallOpen(baseName + "_wall_open", texture) - fenceGateBlock(block, gate, gateOpen, gateWall, gateWallOpen) - } - - private fun fenceGateBlockInternalWithRenderType( - block: FenceGateBlock, - baseName: String, - texture: ResourceLocation, - renderType: ResourceLocation - ) - { - val gate: AModelFile = - blockModels().fenceGate(baseName, texture).renderType(renderType) - val gateOpen: AModelFile = - blockModels().fenceGateOpen(baseName + "_open", texture).renderType(renderType) - val gateWall: AModelFile = - blockModels().fenceGateWall(baseName + "_wall", texture).renderType(renderType) - val gateWallOpen: AModelFile = - blockModels().fenceGateWallOpen(baseName + "_wall_open", texture).renderType(renderType) - fenceGateBlock(block, gate, gateOpen, gateWall, gateWallOpen) - } - - fun fenceGateBlock( - block: FenceGateBlock, - gate: AModelFile, - gateOpen: AModelFile, - gateWall: AModelFile, - gateWallOpen: AModelFile - ) - { - getVariantBuilder(block).forAllStatesExcept({ state: BlockState -> - var model: AModelFile = gate - if (state.getValue(FenceGateBlock.IN_WALL)) - { - model = gateWall - } - if (state.getValue(FenceGateBlock.OPEN)) - { - model = if (model === gateWall) gateWallOpen else gateOpen - } - AConfiguredModel.builder() - .modelFile(model) - .rotationY( - state.getValue(FenceGateBlock.FACING) - .toYRot().toInt() - ) - .uvLock(true) - .build() - }, FenceGateBlock.POWERED) - } - - fun wallBlock(block: WallBlock, texture: ResourceLocation) - { - wallBlockInternal(block, key(block).toString(), texture) - } - - fun wallBlock(block: WallBlock, name: String, texture: ResourceLocation) - { - wallBlockInternal(block, name + "_wall", texture) - } - - fun wallBlockWithRenderType(block: WallBlock, texture: ResourceLocation, renderType: String) - { - wallBlockInternalWithRenderType(block, key(block).toString(), texture, ResourceLocation.parse(renderType)) - } - - fun wallBlockWithRenderType(block: WallBlock, name: String, texture: ResourceLocation, renderType: String) - { - wallBlockInternalWithRenderType(block, name + "_wall", texture, ResourceLocation.parse(renderType)) - } - - fun wallBlockWithRenderType(block: WallBlock, texture: ResourceLocation, renderType: ResourceLocation) - { - wallBlockInternalWithRenderType(block, key(block).toString(), texture, renderType) - } - - fun wallBlockWithRenderType( - block: WallBlock, - name: String, - texture: ResourceLocation, - renderType: ResourceLocation - ) - { - wallBlockInternalWithRenderType(block, name + "_wall", texture, renderType) - } - - private fun wallBlockInternal(block: WallBlock, baseName: String, texture: ResourceLocation) - { - wallBlock( - block, blockModels().wallPost(baseName + "_post", texture), - blockModels().wallSide(baseName + "_side", texture), - blockModels().wallSideTall(baseName + "_side_tall", texture) - ) - } - - private fun wallBlockInternalWithRenderType( - block: WallBlock, - baseName: String, - texture: ResourceLocation, - renderType: ResourceLocation - ) - { - wallBlock( - block, blockModels().wallPost(baseName + "_post", texture).renderType(renderType), - blockModels().wallSide(baseName + "_side", texture).renderType(renderType), - blockModels().wallSideTall(baseName + "_side_tall", texture).renderType(renderType) - ) - } - - fun wallBlock( - block: WallBlock, - post: AModelFile, - side: AModelFile, - sideTall: AModelFile - ) - { - val builder: AMultiPartBlockStateBuilder = - getMultipartBuilder(block) - .part().modelFile(post).addModel() - .condition(WallBlock.UP, true).end() - WALL_PROPS.entries.stream() - .filter { e: Map.Entry> -> - e.key.axis.isHorizontal - } - .forEach { e: Map.Entry> -> - wallSidePart(builder, side, e, WallSide.LOW) - wallSidePart(builder, sideTall, e, WallSide.TALL) - } - } - - private fun wallSidePart( - builder: AMultiPartBlockStateBuilder, - model: AModelFile, - entry: Map.Entry>, - height: WallSide - ) - { - builder.part() - .modelFile(model) - .rotationY(((entry.key.toYRot().toInt()) + 180) % 360) - .uvLock(true) - .addModel() - .condition(entry.value, height) - } - - fun paneBlock(block: IronBarsBlock, pane: ResourceLocation, edge: ResourceLocation) - { - paneBlockInternal(block, key(block).toString(), pane, edge) - } - - fun paneBlock(block: IronBarsBlock, name: String, pane: ResourceLocation, edge: ResourceLocation) - { - paneBlockInternal(block, name + "_pane", pane, edge) - } - - fun paneBlockWithRenderType( - block: IronBarsBlock, - pane: ResourceLocation, - edge: ResourceLocation, - renderType: String - ) - { - paneBlockInternalWithRenderType(block, key(block).toString(), pane, edge, ResourceLocation.parse(renderType)) - } - - fun paneBlockWithRenderType( - block: IronBarsBlock, - name: String, - pane: ResourceLocation, - edge: ResourceLocation, - renderType: String - ) - { - paneBlockInternalWithRenderType(block, name + "_pane", pane, edge, ResourceLocation.parse(renderType)) - } - - fun paneBlockWithRenderType( - block: IronBarsBlock, - pane: ResourceLocation, - edge: ResourceLocation, - renderType: ResourceLocation - ) - { - paneBlockInternalWithRenderType(block, key(block).toString(), pane, edge, renderType) - } - - fun paneBlockWithRenderType( - block: IronBarsBlock, - name: String, - pane: ResourceLocation, - edge: ResourceLocation, - renderType: ResourceLocation - ) - { - paneBlockInternalWithRenderType(block, name + "_pane", pane, edge, renderType) - } - - private fun paneBlockInternal( - block: IronBarsBlock, - baseName: String, - pane: ResourceLocation, - edge: ResourceLocation - ) - { - val post: AModelFile = - blockModels().panePost(baseName + "_post", pane, edge) - val side: AModelFile = - blockModels().paneSide(baseName + "_side", pane, edge) - val sideAlt: AModelFile = - blockModels().paneSideAlt(baseName + "_side_alt", pane, edge) - val noSide: AModelFile = - blockModels().paneNoSide(baseName + "_noside", pane) - val noSideAlt: AModelFile = - blockModels().paneNoSideAlt(baseName + "_noside_alt", pane) - paneBlock(block, post, side, sideAlt, noSide, noSideAlt) - } - - private fun paneBlockInternalWithRenderType( - block: IronBarsBlock, - baseName: String, - pane: ResourceLocation, - edge: ResourceLocation, - renderType: ResourceLocation - ) - { - val post: AModelFile = - blockModels().panePost(baseName + "_post", pane, edge).renderType(renderType) - val side: AModelFile = - blockModels().paneSide(baseName + "_side", pane, edge).renderType(renderType) - val sideAlt: AModelFile = - blockModels().paneSideAlt(baseName + "_side_alt", pane, edge).renderType(renderType) - val noSide: AModelFile = - blockModels().paneNoSide(baseName + "_noside", pane).renderType(renderType) - val noSideAlt: AModelFile = - blockModels().paneNoSideAlt(baseName + "_noside_alt", pane).renderType(renderType) - paneBlock(block, post, side, sideAlt, noSide, noSideAlt) - } - - fun paneBlock( - block: IronBarsBlock, - post: AModelFile, - side: AModelFile, - sideAlt: AModelFile, - noSide: AModelFile, - noSideAlt: AModelFile - ) - { - val builder: AMultiPartBlockStateBuilder = - getMultipartBuilder(block) - .part().modelFile(post).addModel().end() - PipeBlock.PROPERTY_BY_DIRECTION.entries.forEach(Consumer> { e: Map.Entry -> - val dir = e.key - if (dir.axis.isHorizontal) - { - val alt = dir == Direction.SOUTH - builder.part().modelFile(if (alt || dir == Direction.WEST) sideAlt else side) - .rotationY(if (dir.axis === Direction.Axis.X) 90 else 0).addModel() - .condition(e.value, true).end() - .part().modelFile(if (alt || dir == Direction.EAST) noSideAlt else noSide) - .rotationY(if (dir == Direction.WEST) 270 else if (dir == Direction.SOUTH) 90 else 0) - .addModel() - .condition(e.value, false) - } - }) - } - - fun doorBlock(block: DoorBlock, bottom: ResourceLocation, top: ResourceLocation) - { - doorBlockInternal(block, key(block).toString(), bottom, top) - } - - fun doorBlock(block: DoorBlock, name: String, bottom: ResourceLocation, top: ResourceLocation) - { - doorBlockInternal(block, name + "_door", bottom, top) - } - - fun doorBlockWithRenderType(block: DoorBlock, bottom: ResourceLocation, top: ResourceLocation, renderType: String) - { - doorBlockInternalWithRenderType( - block, - key(block).toString(), - bottom, - top, - ResourceLocation.parse(renderType) - ) - } - - fun doorBlockWithRenderType( - block: DoorBlock, - name: String, - bottom: ResourceLocation, - top: ResourceLocation, - renderType: String - ) - { - doorBlockInternalWithRenderType(block, name + "_door", bottom, top, ResourceLocation.parse(renderType)) - } - - fun doorBlockWithRenderType( - block: DoorBlock, - bottom: ResourceLocation, - top: ResourceLocation, - renderType: ResourceLocation - ) - { - doorBlockInternalWithRenderType(block, key(block).toString(), bottom, top, renderType) - } - - fun doorBlockWithRenderType( - block: DoorBlock, - name: String, - bottom: ResourceLocation, - top: ResourceLocation, - renderType: ResourceLocation - ) - { - doorBlockInternalWithRenderType(block, name + "_door", bottom, top, renderType) - } - - private fun doorBlockInternal(block: DoorBlock, baseName: String, bottom: ResourceLocation, top: ResourceLocation) - { - val bottomLeft: AModelFile = - blockModels().doorBottomLeft(baseName + "_bottom_left", bottom, top) - val bottomLeftOpen: AModelFile = - blockModels().doorBottomLeftOpen(baseName + "_bottom_left_open", bottom, top) - val bottomRight: AModelFile = - blockModels().doorBottomRight(baseName + "_bottom_right", bottom, top) - val bottomRightOpen: AModelFile = - blockModels().doorBottomRightOpen(baseName + "_bottom_right_open", bottom, top) - val topLeft: AModelFile = - blockModels().doorTopLeft(baseName + "_top_left", bottom, top) - val topLeftOpen: AModelFile = - blockModels().doorTopLeftOpen(baseName + "_top_left_open", bottom, top) - val topRight: AModelFile = - blockModels().doorTopRight(baseName + "_top_right", bottom, top) - val topRightOpen: AModelFile = - blockModels().doorTopRightOpen(baseName + "_top_right_open", bottom, top) - doorBlock( - block, - bottomLeft, - bottomLeftOpen, - bottomRight, - bottomRightOpen, - topLeft, - topLeftOpen, - topRight, - topRightOpen - ) - } - - private fun doorBlockInternalWithRenderType( - block: DoorBlock, - baseName: String, - bottom: ResourceLocation, - top: ResourceLocation, - renderType: ResourceLocation - ) - { - val bottomLeft: AModelFile = - blockModels().doorBottomLeft(baseName + "_bottom_left", bottom, top).renderType(renderType) - val bottomLeftOpen: AModelFile = - blockModels().doorBottomLeftOpen(baseName + "_bottom_left_open", bottom, top).renderType(renderType) - val bottomRight: AModelFile = - blockModels().doorBottomRight(baseName + "_bottom_right", bottom, top).renderType(renderType) - val bottomRightOpen: AModelFile = - blockModels().doorBottomRightOpen(baseName + "_bottom_right_open", bottom, top).renderType(renderType) - val topLeft: AModelFile = - blockModels().doorTopLeft(baseName + "_top_left", bottom, top).renderType(renderType) - val topLeftOpen: AModelFile = - blockModels().doorTopLeftOpen(baseName + "_top_left_open", bottom, top).renderType(renderType) - val topRight: AModelFile = - blockModels().doorTopRight(baseName + "_top_right", bottom, top).renderType(renderType) - val topRightOpen: AModelFile = - blockModels().doorTopRightOpen(baseName + "_top_right_open", bottom, top).renderType(renderType) - doorBlock( - block, - bottomLeft, - bottomLeftOpen, - bottomRight, - bottomRightOpen, - topLeft, - topLeftOpen, - topRight, - topRightOpen - ) - } - - fun doorBlock( - block: DoorBlock, - bottomLeft: AModelFile, - bottomLeftOpen: AModelFile, - bottomRight: AModelFile, - bottomRightOpen: AModelFile, - topLeft: AModelFile, - topLeftOpen: AModelFile, - topRight: AModelFile, - topRightOpen: AModelFile - ) - { - getVariantBuilder(block).forAllStatesExcept({ state: BlockState -> - var yRot = - state.getValue(DoorBlock.FACING).toYRot() - .toInt() + 90 - val right = - state.getValue(DoorBlock.HINGE) == DoorHingeSide.RIGHT - val open = state.getValue(DoorBlock.OPEN) - val lower = - state.getValue(DoorBlock.HALF) == DoubleBlockHalf.LOWER - if (open) - { - yRot += 90 - } - if (right && open) - { - yRot += 180 - } - yRot %= 360 - - val model: AModelFile = when - { - lower && right && open -> - { - bottomRightOpen - } - - lower && !right && open -> - { - bottomLeftOpen - } - - lower && right && !open -> - { - bottomRight - } - - lower && !right && !open -> - { - bottomLeft - } - - !lower && right && open -> - { - topRightOpen - } - - !lower && !right && open -> - { - topLeftOpen - } - - !lower && right && !open -> - { - topRight - } - - !lower && !right && !open -> - { - topLeft - } - - else -> null - }!! - AConfiguredModel.builder().modelFile(model) - .rotationY(yRot) - .build() - }, DoorBlock.POWERED) - } - - fun trapdoorBlock(block: TrapDoorBlock, texture: ResourceLocation, orientable: Boolean) - { - trapdoorBlockInternal(block, key(block).toString(), texture, orientable) - } - - fun trapdoorBlock(block: TrapDoorBlock, name: String, texture: ResourceLocation, orientable: Boolean) - { - trapdoorBlockInternal(block, name + "_trapdoor", texture, orientable) - } - - fun trapdoorBlockWithRenderType( - block: TrapDoorBlock, - texture: ResourceLocation, - orientable: Boolean, - renderType: String - ) - { - trapdoorBlockInternalWithRenderType( - block, - key(block).toString(), - texture, - orientable, - ResourceLocation.parse(renderType) - ) - } - - fun trapdoorBlockWithRenderType( - block: TrapDoorBlock, - name: String, - texture: ResourceLocation, - orientable: Boolean, - renderType: String - ) - { - trapdoorBlockInternalWithRenderType( - block, - name + "_trapdoor", - texture, - orientable, - ResourceLocation.parse(renderType) - ) - } - - fun trapdoorBlockWithRenderType( - block: TrapDoorBlock, - texture: ResourceLocation, - orientable: Boolean, - renderType: ResourceLocation - ) - { - trapdoorBlockInternalWithRenderType(block, key(block).toString(), texture, orientable, renderType) - } - - fun trapdoorBlockWithRenderType( - block: TrapDoorBlock, - name: String, - texture: ResourceLocation, - orientable: Boolean, - renderType: ResourceLocation - ) - { - trapdoorBlockInternalWithRenderType(block, name + "_trapdoor", texture, orientable, renderType) - } - - private fun trapdoorBlockInternal( - block: TrapDoorBlock, - baseName: String, - texture: ResourceLocation, - orientable: Boolean - ) - { - val bottom: AModelFile = - if (orientable) blockModels().trapdoorOrientableBottom( - baseName + "_bottom", - texture - ) else blockModels().trapdoorBottom(baseName + "_bottom", texture) - val top: AModelFile = - if (orientable) blockModels().trapdoorOrientableTop( - baseName + "_top", - texture - ) else blockModels().trapdoorTop( - baseName + "_top", - texture - ) - val open: AModelFile = - if (orientable) blockModels().trapdoorOrientableOpen( - baseName + "_open", - texture - ) else blockModels().trapdoorOpen( - baseName + "_open", - texture - ) - trapdoorBlock(block, bottom, top, open, orientable) - } - - private fun trapdoorBlockInternalWithRenderType( - block: TrapDoorBlock, - baseName: String, - texture: ResourceLocation, - orientable: Boolean, - renderType: ResourceLocation - ) - { - val bottom: AModelFile = - if (orientable) blockModels().trapdoorOrientableBottom(baseName + "_bottom", texture) - .renderType(renderType) else blockModels().trapdoorBottom(baseName + "_bottom", texture) - .renderType(renderType) - val top: AModelFile = - if (orientable) blockModels().trapdoorOrientableTop(baseName + "_top", texture) - .renderType(renderType) else blockModels().trapdoorTop(baseName + "_top", texture) - .renderType(renderType) - val open: AModelFile = - if (orientable) blockModels().trapdoorOrientableOpen(baseName + "_open", texture) - .renderType(renderType) else blockModels().trapdoorOpen(baseName + "_open", texture) - .renderType(renderType) - trapdoorBlock(block, bottom, top, open, orientable) - } - - fun trapdoorBlock( - block: TrapDoorBlock, - bottom: AModelFile, - top: AModelFile, - open: AModelFile, - orientable: Boolean - ) - { - getVariantBuilder(block).forAllStatesExcept({ state: BlockState -> - var xRot = 0 - var yRot = - state.getValue(TrapDoorBlock.FACING) - .toYRot().toInt() + 180 - val isOpen = - state.getValue(TrapDoorBlock.OPEN) - if (orientable && isOpen && state.getValue(TrapDoorBlock.HALF) == Half.TOP) - { - xRot += 180 - yRot += 180 - } - if (!orientable && !isOpen) - { - yRot = 0 - } - yRot %= 360 - AConfiguredModel.builder().modelFile( - if (isOpen) open else if (state.getValue(TrapDoorBlock.HALF) == Half.TOP) top else bottom - ) - .rotationX(xRot) - .rotationY(yRot) - .build() - }, TrapDoorBlock.POWERED, TrapDoorBlock.WATERLOGGED) - } - - private fun saveBlockState(cache: CachedOutput, stateJson: JsonObject, owner: Block): CompletableFuture<*> - { - val blockName = Preconditions.checkNotNull(key(owner)) - val outputPath = output.getOutputFolder(PackOutput.Target.RESOURCE_PACK) - .resolve(blockName.namespace).resolve("blockstates").resolve(blockName.path + ".json") - return DataProvider.saveStable(cache, stateJson, outputPath) - } - - override fun getName(): String - { - return format("Block States") - } - - class ConfiguredModelList private constructor(models: List) - { - private val models: List - - init - { - Preconditions.checkArgument(models.isNotEmpty()) - this.models = models - } - - constructor(vararg models: AConfiguredModel) : this( - listOf( - *models - ) - ) - - fun toJSON(): JsonElement - { - if (models.size == 1) - { - return models[0].toJSON(false) - } else - { - val ret = JsonArray() - for (m in models) - { - ret.add(m.toJSON(true)) - } - return ret - } - } - - fun append(vararg models: AConfiguredModel): ConfiguredModelList - { - return ConfiguredModelList( - buildList { - addAll(this@ConfiguredModelList.models) - addAll(models) - } - ) - } - } - - companion object - { - private val LOGGER: Logger = LogManager.getLogger() - private val GSON: Gson = - GsonBuilder().setPrettyPrinting().disableHtmlEscaping().create() - - private const val DEFAULT_ANGLE_OFFSET = 180 - - val WALL_PROPS: Map> = - buildMap { - put(Direction.EAST, BlockStateProperties.EAST_WALL) - put(Direction.NORTH, BlockStateProperties.NORTH_WALL) - put(Direction.SOUTH, BlockStateProperties.SOUTH_WALL) - put(Direction.WEST, BlockStateProperties.WEST_WALL) - } - - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AConfiguredModel.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AConfiguredModel.kt deleted file mode 100644 index be2c1b0ab..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AConfiguredModel.kt +++ /dev/null @@ -1,242 +0,0 @@ -package net.kernelpanicsoft.archie.data.client.model - -import com.google.common.base.Preconditions -import com.google.common.collect.ImmutableList -import com.google.common.collect.ObjectArrays -import com.google.gson.JsonObject -import net.minecraft.client.resources.model.BlockModelRotation -import java.util.* -import java.util.function.Function -import java.util.stream.Collectors -import java.util.stream.IntStream - -/** One weighted, rotated variant entry pointing at a [model], as used in blockstate `variants`. Build via [builder]. */ -class AConfiguredModel @JvmOverloads constructor( - model: AModelFile, - rotationX: Int = 0, - rotationY: Int = 0, - uvLock: Boolean = false, - weight: Int = DEFAULT_WEIGHT -) -{ - val model: AModelFile - val rotationX: Int - val rotationY: Int - val uvLock: Boolean - val weight: Int - - init - { - Preconditions.checkNotNull(model) - this.model = model - checkRotation(rotationX, rotationY) - this.rotationX = rotationX - this.rotationY = rotationY - this.uvLock = uvLock - checkWeight(weight) - this.weight = weight - } - - fun toJSON(includeWeight: Boolean): JsonObject - { - val modelJson = JsonObject() - modelJson.addProperty("model", model.location.toString()) - if (rotationX != 0) modelJson.addProperty("x", rotationX) - if (rotationY != 0) modelJson.addProperty("y", rotationY) - if (uvLock) modelJson.addProperty("uvlock", uvLock) - if (includeWeight && weight != DEFAULT_WEIGHT) modelJson.addProperty("weight", weight) - return modelJson - } - - /** - * A builder for one or more [AConfiguredModel]s, optionally backed by a callback that - * consumes the finished result and returns [T] (the owning builder, e.g. an - * [AVariantBlockStateBuilder.PartialBlockstate]). Without a callback (as from the standalone - * [AConfiguredModel.builder]), [addModel] is unavailable; use [build]/[buildLast] instead. - * - * Multiple weighted variants can be configured at once through [nextModel]/[model]. - */ - class Builder @JvmOverloads internal constructor( - private val callback: Function, T>? = null, - private var otherModels: List = listOf() - ) - { - private var model: AModelFile? = null - private var rotationX = 0 - private var rotationY = 0 - private var uvLock = false - private var weight = DEFAULT_WEIGHT - - fun modelFile(model: AModelFile): Builder - { - Preconditions.checkNotNull( - model, - "Model must not be null" - ) - this.model = model - return this - } - - fun rotationX(value: Int): Builder - { - checkRotation(value, rotationY) - rotationX = value - return this - } - - fun rotationY(value: Int): Builder - { - checkRotation(rotationX, value) - rotationY = value - return this - } - - fun uvLock(value: Boolean): Builder - { - uvLock = value - return this - } - - fun weight(value: Int): Builder - { - checkWeight(value) - weight = value - return this - } - - /** Builds only the currently-configured [AConfiguredModel], discarding [otherModels]. */ - fun buildLast(): AConfiguredModel - { - return AConfiguredModel(model!!, rotationX, rotationY, uvLock, weight) - } - - /** Builds every configured model, including any queued via [nextModel]. */ - fun build(): Array - { - return ObjectArrays.concat(otherModels.toTypedArray(), buildLast()) - } - - /** Finalizes [build] and hands the result to the owning builder's callback, returning [T]. */ - fun addModel(): T - { - Preconditions.checkNotNull(callback, "Cannot use addModel() without an owning builder present") - return callback!!.apply(build()) - } - - /** Starts configuring another weighted variant, keeping models built so far. */ - fun nextModel(): Builder - { - return Builder(callback, build().toList()) - } - - /** Configures the current (or, once already configured, the next) variant with [block]. */ - fun model(block: Builder.() -> Unit): Builder - { - if (otherModels.isEmpty()) - block() - else - otherModels = nextModel().apply(block).otherModels - return this - } - } - - companion object - { - const val DEFAULT_WEIGHT: Int = 1 - - private fun validRotations(): IntStream - { - return IntStream.range(0, 4).map { i: Int -> i * 90 } - } - - /** Builds one [AConfiguredModel] of [model] per valid Y rotation (0/90/180/270), all at fixed X rotation [x]. */ - @JvmOverloads - fun allYRotations( - model: AModelFile, - x: Int, - uvlock: Boolean, - weight: Int = DEFAULT_WEIGHT - ): Array - { - return validRotations() - .mapToObj { y: Int -> AConfiguredModel(model, x, y, uvlock, weight) } - .collect(Collectors.toList()).toTypedArray() - } - - /** Builds one [AConfiguredModel] of [model] for every valid X/Y rotation combination. */ - @JvmOverloads - fun allRotations( - model: AModelFile, - uvlock: Boolean, - weight: Int = DEFAULT_WEIGHT - ): Array - { - return validRotations() - .mapToObj { x: Int -> - allYRotations( - model, - x, - uvlock, - weight - ) - } - .flatMap { array: Array -> - Arrays.stream( - array - ) - }.collect(Collectors.toList()).toTypedArray() - } - - fun checkRotation(rotationX: Int, rotationY: Int) - { - Preconditions.checkArgument( - BlockModelRotation.by(rotationX, rotationY) != null, - "Invalid model rotation x=%d, y=%d", - rotationX, - rotationY - ) - } - - fun checkWeight(weight: Int) - { - Preconditions.checkArgument( - weight >= 1, - "Model weight must be greater than or equal to 1. Found: %d", - weight - ) - } - - /** Creates a standalone [Builder] with no owning-builder callback; use [Builder.build]/[Builder.buildLast]. */ - fun builder(block: Builder<*>.() -> Unit = {}): Builder<*> - { - return Builder().apply(block) - } - - fun builder( - outer: AVariantBlockStateBuilder, - state: AVariantBlockStateBuilder.PartialBlockstate - ): Builder - { - return Builder({ models: Array -> - outer.setModels( - state, - *models - ) - }, ImmutableList.of()) - } - - fun builder(outer: AMultiPartBlockStateBuilder): Builder - { - return Builder( - { models: Array -> - val ret: AMultiPartBlockStateBuilder.PartBuilder = - outer.PartBuilder( - ABlockStateProvider.ConfiguredModelList(*models) - ) - outer.addPart(ret) - ret - }, ImmutableList.of() - ) - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ACustomLoaderBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ACustomLoaderBuilder.kt deleted file mode 100644 index d669f953d..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ACustomLoaderBuilder.kt +++ /dev/null @@ -1,84 +0,0 @@ -package net.kernelpanicsoft.archie.data.client.model - -import com.google.common.base.Preconditions -import com.google.gson.JsonObject -import net.minecraft.resources.ResourceLocation - -/** - * Base for a custom geometry loader's model JSON, embedded in an [AModelBuilder] via - * [AModelBuilder.customLoader]. Subclasses add their loader's own fields by overriding - * [toJson]; this base handles the common `loader`/`visibility`/`optional` fields. - */ -abstract class ACustomLoaderBuilder> protected constructor( - /** The id of the associated geometry loader. */ - val loaderId: ResourceLocation, - /** The [AModelBuilder] this loader is being configured on; returned by [end]. */ - protected val parent: T, - val allowInlineElements: Boolean -) -{ - protected val visibility: MutableMap = LinkedHashMap() - private var optional = false - - @Deprecated("Use the (loaderId, parent, allowInlineElements) constructor instead") - protected constructor( - loaderId: ResourceLocation, - parent: T, - ) : this(loaderId, parent, false) - - /** Sets whether the model part named [partName] is initially visible. */ - fun visibility(partName: String, show: Boolean): ACustomLoaderBuilder - { - Preconditions.checkNotNull(partName, "partName must not be null") - visibility[partName] = show - return this - } - - /** - * Mark the custom loader as optional for this model to allow it to be loaded through vanilla paths - * if the loader is not present - */ - fun optional(): ACustomLoaderBuilder - { - Preconditions.checkState( - allowInlineElements, - "Only loaders with support for inline elements can be marked as optional" - ) - this.optional = true - return this - } - - /** Returns to the enclosing [AModelBuilder] this loader was configured on. */ - fun end(): T - { - return parent - } - - open fun toJson(json: JsonObject): JsonObject - { - if (optional) - { - val loaderObj = JsonObject() - loaderObj.addProperty("id", loaderId.toString()) - loaderObj.addProperty("optional", true) - json.add("loader", loaderObj) - } else - { - json.addProperty("loader", loaderId.toString()) - } - - if (visibility.isNotEmpty()) - { - val visibilityObj = JsonObject() - - for ((key, value) in visibility) - { - visibilityObj.addProperty(key, value) - } - - json.add("visibility", visibilityObj) - } - - return json - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelBuilder.kt deleted file mode 100644 index b92a9538b..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelBuilder.kt +++ /dev/null @@ -1,88 +0,0 @@ -package net.kernelpanicsoft.archie.data.client.model - -import com.google.common.base.Preconditions -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import net.minecraft.resources.ResourceLocation - -/** [AModelBuilder] for an item model at [outputLocation], adding support for `overrides` entries. */ -class AItemModelBuilder( - outputLocation: ResourceLocation, -) : AModelBuilder(outputLocation) -{ - protected var overrides: MutableList = ArrayList() - - /** Adds a new override entry, configured by [block]. */ - fun override(block: OverrideBuilder.() -> Unit = {}): OverrideBuilder - { - val ret = OverrideBuilder().apply(block) - overrides.add(ret) - return ret - } - - /** Reconfigures the existing override at [index] with [block]. */ - fun override(index: Int, block: OverrideBuilder.() -> Unit = {}): OverrideBuilder - { - Preconditions.checkElementIndex(index, overrides.size, "override") - return overrides[index].apply(block) - } - - override fun toJson(): JsonObject - { - val root: JsonObject = super.toJson() - if (overrides.isNotEmpty()) - { - val overridesJson = JsonArray() - overrides.stream().map { obj: OverrideBuilder -> obj.toJson() } - .forEach { element: JsonObject? -> - overridesJson.add( - element - ) - } - root.add("overrides", overridesJson) - } - return root - } - - /** Builder for a single `overrides` entry: a [model] shown when its [predicate]s are all satisfied. */ - inner class OverrideBuilder - { - private var model: AModelFile? = null - private val predicates: MutableMap = LinkedHashMap() - - /** Sets the model to use when this override's predicates match. */ - fun model(model: AModelFile): OverrideBuilder - { - this.model = model - return this - } - - /** Requires item property [key] to be at least [value] for this override to apply. */ - fun predicate(key: ResourceLocation, value: Float): OverrideBuilder - { - predicates[key] = value - return this - } - - /** Returns to the enclosing [AItemModelBuilder]. */ - fun end(): AItemModelBuilder - { - return this@AItemModelBuilder - } - - fun toJson(): JsonObject - { - val ret = JsonObject() - val predicatesJson = JsonObject() - predicates.forEach { (key: ResourceLocation, `val`: Float?) -> - predicatesJson.addProperty( - key.toString(), - `val` - ) - } - ret.add("predicate", predicatesJson) - ret.addProperty("model", model?.location.toString()) - return ret - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelProvider.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelProvider.kt deleted file mode 100644 index 35b4849e4..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelProvider.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.kernelpanicsoft.archie.data.client.model - -import dev.architectury.platform.Mod -import net.minecraft.core.registries.BuiltInRegistries -import net.minecraft.data.PackOutput -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.item.Item -import java.util.* - -/** [AModelProvider] that generates item models under `models/item/`. */ -abstract class AItemModelProvider(output: PackOutput, mod: Mod, exitOnError: Boolean) : - AModelProvider(output, mod, ITEM_FOLDER, ::AItemModelBuilder, exitOnError) -{ - /** Registers a `item/generated` model for [item] using its own `item/` texture as `layer0`. */ - fun basicItem(item: Item, block: AItemModelBuilder.() -> Unit = {}): AItemModelBuilder - { - return basicItem(Objects.requireNonNull(BuiltInRegistries.ITEM.getKey(item)), block) - } - - /** Registers a `item/generated` model at [item] using an `item/` texture as `layer0`. */ - fun basicItem(item: ResourceLocation, block: AItemModelBuilder.() -> Unit = {}): AItemModelBuilder - { - return getBuilder(item.toString()) { - parent(AModelFile("item/generated")) - texture("layer0", ResourceLocation.fromNamespaceAndPath(item.namespace, "item/${item.path}")) - }.apply(block) - } - - override fun getName(): String = format("Item Models") -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt deleted file mode 100644 index 4338a6221..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt +++ /dev/null @@ -1,1386 +0,0 @@ -package net.kernelpanicsoft.archie.data.client.model - -import com.google.common.base.Preconditions -import com.google.gson.* -import com.mojang.blaze3d.vertex.PoseStack -import com.mojang.datafixers.util.Either -import com.mojang.math.Transformation -import com.mojang.serialization.Codec -import com.mojang.serialization.JsonOps -import com.mojang.serialization.codecs.RecordCodecBuilder -import net.kernelpanicsoft.archie.data.util.TransformationHelper -import dev.architectury.platform.Platform -import net.minecraft.client.renderer.block.model.* -import net.minecraft.client.renderer.texture.MissingTextureAtlasSprite -import net.minecraft.core.Direction -import net.minecraft.resources.ResourceLocation -import net.minecraft.util.GsonHelper -import net.minecraft.util.Mth -import net.minecraft.world.item.ItemDisplayContext -import org.joml.Quaternionf -import org.joml.Vector3f -import java.lang.reflect.Type -import java.util.* -import java.util.function.* -import java.util.function.Function -import java.util.stream.Collectors -import kotlin.Any -import kotlin.Boolean -import kotlin.Char -import kotlin.Float -import kotlin.FloatArray -import kotlin.IllegalArgumentException -import kotlin.Int -import kotlin.NullPointerException -import kotlin.Number -import kotlin.String -import kotlin.Suppress -import kotlin.Throws -import kotlin.Unit -import kotlin.apply -import kotlin.checkNotNull -import kotlin.floatArrayOf -import kotlin.toString - -/** - * Base fluent builder for a block/item model JSON at [location], mirroring NeoForge's vanilla - * `ModelBuilder` datagen helper 1:1. Set [parent], [texture]s, [renderType], [ao]/[guiLight], and - * either inline [element]s or [customLoader] data, plus display [transforms]/[rootTransforms]. - * Subclassed by [ABlockModelBuilder] and [AItemModelBuilder]; obtained via [AModelProvider]. - */ -@Suppress("unused") -open class AModelBuilder>(location: ResourceLocation) : AModelFile(location) -{ - protected var parent: AModelFile? = null - protected val textures: MutableMap = linkedMapOf() - protected val transforms: TransformsBuilder = TransformsBuilder() - - protected var renderType: String? = null - protected var ambientOcclusion: Boolean = true - protected var guiLight: BlockModel.GuiLight? = null - - protected val elements: MutableList = mutableListOf() - - protected var customLoader: ACustomLoaderBuilder? = null - - private val rootTransforms: RootTransformsBuilder = RootTransformsBuilder() - - @Suppress("UNCHECKED_CAST") - private val self: T - get() = this as T - - operator fun invoke(block: AModelBuilder.() -> Unit) = apply(block) - - /** Sets the model's `parent` reference to [parent]. */ - fun parent(parent: AModelFile): T - { - Preconditions.checkNotNull(parent, "Parent must not be null") - this.parent = parent - return self - } - - /** Binds texture variable [key] to [texture] (a texture id, or a `#other_key` reference). */ - fun texture(key: String, texture: String): T - { - Preconditions.checkNotNull(key, "Key must not be null") - Preconditions.checkNotNull(texture, "Texture must not be null") - if (texture[0] == '#') - { - textures[key] = texture - return self - } else - { - val asLoc: ResourceLocation = if (texture.contains(":")) - { - ResourceLocation.parse(texture) - } else - { - ResourceLocation.fromNamespaceAndPath(location.namespace, texture) - } - return texture(key, asLoc) - } - } - - /** Binds texture variable [key] to [texture]. */ - fun texture(key: String, texture: ResourceLocation): T - { - Preconditions.checkNotNull(key, "Key must not be null") - Preconditions.checkNotNull(texture, "Texture must not be null") - textures[key] = texture.toString() - return self - } - - /** Sets the render type (parsed as a [ResourceLocation]) used to draw this model. */ - fun renderType(renderType: String): T - { - Preconditions.checkNotNull(renderType, "Render type must not be null") - return renderType(ResourceLocation.parse(renderType)) - } - - /** Sets the render type used to draw this model. */ - fun renderType(renderType: ResourceLocation): T - { - Preconditions.checkNotNull(renderType, "Render type must not be null") - this.renderType = renderType.toString() - return self - } - - /** Configures the vanilla per-[ItemDisplayContext] `display` transforms via [block]. */ - fun transforms(block: TransformsBuilder.() -> Unit = {}): TransformsBuilder - { - return transforms.apply(block) - } - - /** Sets whether ambient occlusion is used when rendering this model. */ - fun ao(ao: Boolean): T - { - this.ambientOcclusion = ao - return self - } - - /** Sets whether this model is lit from the front (GUI-style) or diagonally (3D-style); `null` to inherit from [parent]. */ - fun guiLight(light: BlockModel.GuiLight?): T - { - this.guiLight = light - return self - } - - /** Adds a new inline cuboid element, configured by [block]. Throws if [customLoader] disallows inline elements. */ - fun element(block: ElementBuilder.() -> Unit = {}): ElementBuilder - { - Preconditions.checkState( - customLoader == null || customLoader!!.allowInlineElements, - "Custom model loader %s does not support inline elements", - customLoader?.loaderId - ) - val ret = ElementBuilder().apply(block) - elements.add(ret) - return ret - } - - /** Reconfigures the existing element at [index] with [block]. */ - fun element(index: Int, block: ElementBuilder.() -> Unit = {}): ElementBuilder - { - Preconditions.checkState( - customLoader == null || customLoader!!.allowInlineElements, - "Custom model loader %s does not support inline elements", - customLoader?.loaderId - ) - Preconditions.checkElementIndex(index, elements.size, "Element index") - return elements[index].apply(block) - } - - /** The number of inline [element]s configured so far. */ - fun getElementCount(): Int - { - return elements.size - } - - /** - * Builds and attaches an [ACustomLoaderBuilder] via [customLoaderFactory], replacing vanilla - * element-based geometry with a custom loader's own JSON. Forge-like loaders only. - */ - fun ?> customLoader(customLoaderFactory: Function): L - { - check(Platform.isForgeLike()) { "Custom Loader only supported on forge like loaders" } - val customLoader = customLoaderFactory.apply(self)!! - Preconditions.checkState( - customLoader.allowInlineElements || elements.isEmpty(), - "Custom model loader %s does not support inline elements", - customLoader.loaderId - ) - this.customLoader = customLoader - return customLoader - } - - /** Configures NeoForge's extended root-level (pre-display) transform via [block]. */ - fun rootTransforms(block: RootTransformsBuilder.() -> Unit = {}): RootTransformsBuilder - { - return rootTransforms.apply(block) - } - - private fun BlockElement.computeUvsByFace(face: Direction): FloatArray - { - when (face) - { - Direction.DOWN -> - { - return floatArrayOf(this.from.x(), 16.0f - this.to.z(), this.to.x(), 16.0f - this.from.z()) - } - - Direction.UP -> - { - return floatArrayOf(this.from.x(), this.from.z(), this.to.x(), this.to.z()) - } - - Direction.SOUTH -> - { - return floatArrayOf(this.from.x(), 16.0f - this.to.y(), this.to.x(), 16.0f - this.from.y()) - } - - Direction.WEST -> - { - return floatArrayOf(this.from.z(), 16.0f - this.to.y(), this.to.z(), 16.0f - this.from.y()) - } - - Direction.EAST -> - { - } - - else -> - { - return floatArrayOf( - 16.0f - this.to.x(), - 16.0f - this.to.y(), - 16.0f - this.from.x(), - 16.0f - this.from.y() - ) - } - } - return floatArrayOf(16.0f - this.to.z(), 16.0f - this.to.y(), 16.0f - this.from.z(), 16.0f - this.from.y()) - } - - open fun toJson(): JsonObject - { - val root = JsonObject() - - if (this.parent != null) - { - root.addProperty("parent", parent.toString()) - } - - if (!this.ambientOcclusion) - { - root.addProperty("ambientocclusion", this.ambientOcclusion) - } - - if (this.guiLight != null) - { - root.addProperty("gui_light", this.guiLight!!.name) - } - - if (this.renderType != null) - { - root.addProperty("render_type", this.renderType) - } - - val transforms: Map = - this.transforms.build() - if (transforms.isNotEmpty()) - { - val display = JsonObject() - for ((key, vec) in transforms) - { - val transform = JsonObject() - if (vec == PlatformItemTransform.NO_TRANSFORM) continue - val hasRightRotation: Boolean = - vec.rightRotation != PlatformItemTransform.Deserializer.DEFAULT_ROTATION - if (vec.translation != PlatformItemTransform.Deserializer.DEFAULT_TRANSLATION) - { - transform.add("translation", serializeVector3f(vec.translation)) - } - if (vec.rotation != PlatformItemTransform.Deserializer.DEFAULT_ROTATION) - { - transform.add( - if (hasRightRotation) "left_rotation" else "rotation", - serializeVector3f(vec.rotation) - ) - } - if (vec.scale != PlatformItemTransform.Deserializer.DEFAULT_SCALE) - { - transform.add("scale", serializeVector3f(vec.scale)) - } - if (hasRightRotation) - { - transform.add("right_rotation", serializeVector3f(vec.rightRotation)) - } - display.add(key.serializedName, transform) - } - root.add("display", display) - } - - if (textures.isNotEmpty()) - { - val textures = JsonObject() - for ((key, value) in this.textures) - { - textures.addProperty(key, serializeLocOrKey(value)) - } - root.add("textures", textures) - } - - if (this.elements.isNotEmpty()) - { - val elements = JsonArray() - this.elements.stream() - .map { obj: ElementBuilder -> obj.build() } - .forEach { part: BlockElement -> - val partObj = JsonObject() - partObj.add("from", serializeVector3f(part.from)) - partObj.add("to", serializeVector3f(part.to)) - - if (part.rotation != null) - { - val rotation = JsonObject() - rotation.add("origin", serializeVector3f(part.rotation.origin())) - rotation.addProperty("axis", part.rotation.axis().serializedName) - rotation.addProperty("angle", part.rotation.angle()) - if (part.rotation.rescale()) - { - rotation.addProperty("rescale", part.rotation.rescale()) - } - partObj.add("rotation", rotation) - } - - if (!part.shade) - { - partObj.addProperty("shade", part.shade) - } - - if (part is PlatformBlockElement) - { - if (part.faceData is PlatformFaceData.ExtraFaceData && part.faceData != PlatformFaceData.ExtraFaceData.DEFAULT) - { - partObj.add( - "neoforge_data", - PlatformFaceData.ExtraFaceData.CODEC.encodeStart( - JsonOps.INSTANCE, - part.faceData - ).result().get() - ) - } - } - - val faces = JsonObject() - for (dir in Direction.entries) - { - val face = part.faces[dir] ?: continue - - val faceObj = JsonObject() - faceObj.addProperty("texture", serializeLocOrKey(face.texture)) - if (!face.uv.uvs.contentEquals(part.computeUvsByFace(dir))) - { - faceObj.add("uv", Gson().toJsonTree(face.uv.uvs)) - } - if (face.cullForDirection != null) - { - faceObj.addProperty("cullface", face.cullForDirection!!.serializedName) - } - if (face.uv.rotation != 0) - { - faceObj.addProperty("rotation", face.uv.rotation) - } - if (face.tintIndex != -1) - { - faceObj.addProperty("tintindex", face.tintIndex) - } - if (face is PlatformBlockElementFace) - { - when - { - face.faceData is PlatformFaceData.ExtraFaceData && face.faceData != PlatformFaceData.ExtraFaceData.DEFAULT -> - { - faceObj.add( - "neoforge_data", - PlatformFaceData.ExtraFaceData.CODEC.encodeStart( - JsonOps.INSTANCE, - face.faceData - ).result().get() - ) - } - - face.faceData is PlatformFaceData.ForgeFaceData && face.faceData != PlatformFaceData.ForgeFaceData.DEFAULT -> - { - faceObj.add( - "forge_data", - PlatformFaceData.ForgeFaceData.CODEC.encodeStart( - JsonOps.INSTANCE, - face.faceData - ).result().get() - ) - } - } - } - - faces.add(dir.serializedName, faceObj) - } - if (!part.faces.isEmpty()) - { - partObj.add("faces", faces) - } - elements.add(partObj) - } - root.add("elements", elements) - } - - // If there were any transform properties set, add them to the output. - val transform: JsonObject = rootTransforms.toJson() - if (transform.size() > 0) - { - root.add("transform", transform) - } - - return customLoader?.toJson(root) ?: root - } - - private fun serializeLocOrKey(tex: String): String - { - if (tex[0] == '#') - { - return tex - } - return ResourceLocation.parse(tex).toString() - } - - private fun serializeVector3f(vec: Vector3f): JsonArray - { - val ret = JsonArray() - ret.add(serializeFloat(vec.x())) - ret.add(serializeFloat(vec.y())) - ret.add(serializeFloat(vec.z())) - return ret - } - - private fun serializeFloat(f: Float): Number - { - if (f.toInt().toFloat() == f) - { - return f.toInt() - } - return f - } - - /** Builder for one inline cuboid `elements` entry, added via [element]. */ - inner class ElementBuilder - { - private var from = Vector3f() - private var to = Vector3f(16f, 16f, 16f) - private val faces: MutableMap = LinkedHashMap() - private var rotation: RotationBuilder? = null - private var shade = true - private var color = -0x1 - private var blockLight = 0 - private var skyLight = 0 - private var hasAmbientOcclusion = true - - private fun validateCoordinate(coord: Float, name: Char) - { - Preconditions.checkArgument( - !(coord < -16.0f) && !(coord > 32.0f), - "Position $name out of range, must be within [-16, 32]. Found: %d", coord - ) - } - - private fun validatePosition(pos: Vector3f) - { - validateCoordinate(pos.x(), 'x') - validateCoordinate(pos.y(), 'y') - validateCoordinate(pos.z(), 'z') - } - - fun from(x: Float, y: Float, z: Float): ElementBuilder - { - this.from = Vector3f(x, y, z) - validatePosition(this.from) - return this - } - - fun to(x: Float, y: Float, z: Float): ElementBuilder - { - this.to = Vector3f(x, y, z) - validatePosition(this.to) - return this - } - - fun face(dir: Direction, block: FaceBuilder.() -> Unit = {}): FaceBuilder - { - Preconditions.checkNotNull(dir, "Direction must not be null") - return faces.computeIfAbsent( - dir - ) { dir: Direction -> - FaceBuilder( - dir - ) - }.apply(block) - } - - fun rotation(block: RotationBuilder.() -> Unit = {}): RotationBuilder - { - if (this.rotation == null) - { - this.rotation = RotationBuilder() - } - return this.rotation!!.apply(block) - } - - fun shade(shade: Boolean): ElementBuilder - { - this.shade = shade - return this - } - - fun allFaces(action: BiConsumer): ElementBuilder - { - Arrays.stream(Direction.entries.toTypedArray()) - .forEach { d: Direction -> - action.accept( - d, - face(d) - ) - } - return this - } - - fun faces(action: BiConsumer): ElementBuilder - { - faces.entries.stream() - .forEach { e: Map.Entry -> - action.accept( - e.key, - e.value - ) - } - return this - } - - fun textureAll(texture: String): ElementBuilder - { - return allFaces(addTexture(texture)) - } - - fun texture(texture: String): ElementBuilder - { - return faces(addTexture(texture)) - } - - fun cube(texture: String): ElementBuilder - { - return allFaces(addTexture(texture).andThen { dir: Direction?, f: FaceBuilder -> - f.cullface( - dir - ) - }) - } - - fun emissivity(blockLight: Int, skyLight: Int): ElementBuilder - { - this.blockLight = blockLight - this.skyLight = skyLight - return this - } - - fun color(color: Int): ElementBuilder - { - this.color = color - return this - } - - fun ao(ao: Boolean): ElementBuilder - { - this.hasAmbientOcclusion = ao - return this - } - - private fun addTexture(texture: String): BiConsumer - { - return BiConsumer { `$`: Direction?, f: FaceBuilder -> - f.texture( - texture - ) - } - } - - fun build(): BlockElement - { - val faces: Map = - faces.entries.stream() - .collect( - Collectors.toMap( - { it.key }, - { e: Map.Entry -> e.value.build() }, - { k1: BlockElementFace?, k2: BlockElementFace? -> - throw IllegalArgumentException() - }, - { LinkedHashMap() }) - ) - return PlatformBlockElement( - from, to, faces, if (rotation == null) null else rotation!!.build(), shade, when - { - Platform.isNeoForge() -> - PlatformFaceData.ExtraFaceData( - color = color, - blockLight = blockLight, - skyLight = skyLight, - ambientOcclusion = hasAmbientOcclusion - ) - - Platform.isMinecraftForge() -> - PlatformFaceData.ForgeFaceData( - color = color, - blockLight = blockLight, - skyLight = skyLight, - ambientOcclusion = hasAmbientOcclusion - ) - - else -> - PlatformFaceData.None - } - ) - } - - - fun end(): T - { - return self - } - - inner class FaceBuilder internal constructor(dir: Direction) - { - private var cullface: Direction? = null - private var tintindex = -1 - private var texture: String? = MissingTextureAtlasSprite.getLocation().toString() - private lateinit var uvs: FloatArray - private var rotation: FaceRotation = FaceRotation.ZERO - private var color = -0x1 - private var blockLight = 0 - private var skyLight = 0 - private var hasAmbientOcclusion = true - - fun cullface(dir: Direction?): FaceBuilder - { - this.cullface = dir - return this - } - - fun tintindex(index: Int): FaceBuilder - { - this.tintindex = index - return this - } - - fun texture(texture: String): FaceBuilder - { - Preconditions.checkNotNull(texture, "Texture must not be null") - this.texture = texture - return this - } - - fun uvs(u1: Float, v1: Float, u2: Float, v2: Float): FaceBuilder - { - this.uvs = floatArrayOf(u1, v1, u2, v2) - return this - } - - fun rotation(rot: FaceRotation): FaceBuilder - { - Preconditions.checkNotNull(rot, "Rotation must not be null") - this.rotation = rot - return this - } - - fun emissivity( - blockLight: Int, - skyLight: Int - ): FaceBuilder - { - this.blockLight = blockLight - this.skyLight = skyLight - return this - } - - fun color(color: Int): FaceBuilder - { - this.color = color - return this - } - - fun ao(ao: Boolean): FaceBuilder - { - this.hasAmbientOcclusion = ao - return this - } - - fun build(): BlockElementFace - { - checkNotNull(this.texture) { "A model face must have a texture" } - return PlatformBlockElementFace( - cullface, tintindex, texture!!, BlockFaceUV(uvs, rotation.rotation), when - { - Platform.isNeoForge() -> - PlatformFaceData.ExtraFaceData( - color = color, - blockLight = blockLight, - skyLight = skyLight, - ambientOcclusion = hasAmbientOcclusion - ) - - Platform.isMinecraftForge() -> - PlatformFaceData.ForgeFaceData( - color = color, - blockLight = blockLight, - skyLight = skyLight, - ambientOcclusion = hasAmbientOcclusion - ) - - else -> - PlatformFaceData.None - } - ) - } - - fun end(): ElementBuilder - { - return this@ElementBuilder - } - } - - inner class RotationBuilder - { - private lateinit var origin: Vector3f - private lateinit var axis: Direction.Axis - private var angle = 0f - private var rescale = false - - fun origin(x: Float, y: Float, z: Float): RotationBuilder - { - this.origin = Vector3f(x, y, z) - return this - } - - /** - * @param axis the axis of rotation - * @return this builder - * @throws NullPointerException if `axis` is `null` - */ - fun axis(axis: Direction.Axis): RotationBuilder - { - Preconditions.checkNotNull(axis, "Axis must not be null") - this.axis = axis - return this - } - - /** - * @param angle the rotation angle - * @return this builder - * @throws IllegalArgumentException if `angle` is invalid (not one of 0, +/-22.5, +/-45) - */ - fun angle(angle: Float): RotationBuilder - { - // Same logic from BlockPart.Deserializer#parseAngle - Preconditions.checkArgument( - angle == 0.0f || Mth.abs(angle) == 22.5f || Mth.abs( - angle - ) == 45.0f, "Invalid rotation %f found, only -45/-22.5/0/22.5/45 allowed", angle - ) - this.angle = angle - return this - } - - fun rescale(rescale: Boolean): RotationBuilder - { - this.rescale = rescale - return this - } - - fun build(): BlockElementRotation - { - return BlockElementRotation(origin, axis, angle, rescale) - } - - fun end(): ElementBuilder - { - return this@ElementBuilder - } - } - } - - enum class FaceRotation(val rotation: Int) - { - ZERO(0), - CLOCKWISE_90(90), - UPSIDE_DOWN(180), - COUNTERCLOCKWISE_90(270), - } - - class PlatformBlockElement( - from: Vector3f, to: Vector3f, faces: Map, - rotation: BlockElementRotation?, shade: Boolean, val faceData: PlatformFaceData - ) : BlockElement( - from, to, - faces, rotation, shade - ) - - class PlatformBlockElementFace( - cullForDirection: Direction?, - tintIndex: Int, texture: String, uv: BlockFaceUV, val faceData: PlatformFaceData - ) : BlockElementFace(cullForDirection, tintIndex, texture, uv) - - sealed class PlatformFaceData - { - data object None : PlatformFaceData() - data class ExtraFaceData( - val color: Int, - val blockLight: Int, - val skyLight: Int, - val ambientOcclusion: Boolean - ) : PlatformFaceData() - { - companion object - { - val DEFAULT: ExtraFaceData = ExtraFaceData(-0x1, 0, 0, true) - - val COLOR: Codec = Codec.either(Codec.INT, Codec.STRING).xmap( - { either: Either -> - either.map( - Function.identity() - ) { str: String -> - str.toLong(16).toInt() - } - }, - { color: Int? -> - Either.right( - Integer.toHexString( - color!! - ) - ) - }) - - val CODEC: Codec = - RecordCodecBuilder.create { builder: RecordCodecBuilder.Instance -> - builder - .group( - COLOR.optionalFieldOf("color", -0x1).forGetter(ExtraFaceData::color), - Codec.intRange(0, 15).optionalFieldOf("block_light", 0) - .forGetter(ExtraFaceData::blockLight), - Codec.intRange(0, 15).optionalFieldOf("sky_light", 0) - .forGetter(ExtraFaceData::skyLight), - Codec.BOOL.optionalFieldOf("ambient_occlusion", true) - .forGetter(ExtraFaceData::ambientOcclusion) - ) - .apply( - builder - ) { color: Int, blockLight: Int, skyLight: Int, ambientOcclusion: Boolean -> - ExtraFaceData( - color, - blockLight, - skyLight, - ambientOcclusion - ) - } - } - } - } - - data class ForgeFaceData( - val color: Int, - val blockLight: Int, - val skyLight: Int, - val ambientOcclusion: Boolean, - val calculateNormals: Boolean - ) : PlatformFaceData() - { - constructor(color: Int, blockLight: Int, skyLight: Int, ambientOcclusion: Boolean) : this( - color, - blockLight, - skyLight, - ambientOcclusion, - false - ) - - companion object - { - val DEFAULT: ForgeFaceData = ForgeFaceData(-0x1, 0, 0, true, false) - - val COLOR: Codec = Codec.either(Codec.INT, Codec.STRING).xmap( - { either: Either -> - either.map( - Function.identity() - ) { str: String -> - str.toLong(16).toInt() - } - }, - { color: Int? -> - Either.right( - Integer.toHexString( - color!! - ) - ) - }) - - val CODEC: Codec = - RecordCodecBuilder.create { builder: RecordCodecBuilder.Instance -> - builder.group( - COLOR.optionalFieldOf("color", -0x1).forGetter(ForgeFaceData::color), - Codec.intRange(0, 15).optionalFieldOf("block_light", 0) - .forGetter(ForgeFaceData::blockLight), - Codec.intRange(0, 15).optionalFieldOf("sky_light", 0) - .forGetter(ForgeFaceData::skyLight), - Codec.BOOL.optionalFieldOf("ambient_occlusion", true) - .forGetter(ForgeFaceData::ambientOcclusion), - Codec.BOOL.optionalFieldOf("calculate_normals", false) - .forGetter(ForgeFaceData::calculateNormals) - ) - .apply( - builder - ) { color: Int, blockLight: Int, skyLight: Int, ambientOcclusion: Boolean, calculateNormals: Boolean -> - ForgeFaceData( - color, - blockLight, - skyLight, - ambientOcclusion, - calculateNormals - ) - } - } - } - } - } - - inner class TransformsBuilder - { - private val transforms: MutableMap = LinkedHashMap() - - /** - * Begin building a new transform for the given perspective. - * - * @param type the perspective to create or return the builder for - * @return the builder for the given perspective - * @throws NullPointerException if `type` is `null` - */ - fun transform(type: ItemDisplayContext): TransformVecBuilder - { - Preconditions.checkNotNull(type, "Perspective cannot be null") - return transforms.computeIfAbsent( - type - ) { type: ItemDisplayContext? -> - TransformVecBuilder( - type - ) - } - } - - fun build(): Map - { - return transforms.entries.stream() - .collect( - Collectors.toMap( - { it.key }, - { e: Map.Entry -> e.value.build() }, - { k1: PlatformItemTransform?, k2: PlatformItemTransform? -> - throw java.lang.IllegalArgumentException() - }, - { LinkedHashMap() }) - ) - } - - fun end(): T - { - return self - } - - inner class TransformVecBuilder internal constructor(type: ItemDisplayContext?) - { - private var rotation = Vector3f(PlatformItemTransform.Deserializer.DEFAULT_ROTATION) - private var translation = Vector3f(PlatformItemTransform.Deserializer.DEFAULT_TRANSLATION) - private var scale = Vector3f(PlatformItemTransform.Deserializer.DEFAULT_SCALE) - private var rightRotation = Vector3f(PlatformItemTransform.Deserializer.DEFAULT_ROTATION) - - fun rotation(x: Float, y: Float, z: Float): TransformVecBuilder - { - this.rotation = Vector3f(x, y, z) - return this - } - - fun leftRotation(x: Float, y: Float, z: Float): TransformVecBuilder - { - return rotation(x, y, z) - } - - fun translation(x: Float, y: Float, z: Float): TransformVecBuilder - { - this.translation = Vector3f(x, y, z) - return this - } - - fun scale(sc: Float): TransformVecBuilder - { - return scale(sc, sc, sc) - } - - fun scale(x: Float, y: Float, z: Float): TransformVecBuilder - { - this.scale = Vector3f(x, y, z) - return this - } - - fun rightRotation( - x: Float, - y: Float, - z: Float - ): TransformVecBuilder - { - this.rightRotation = Vector3f(x, y, z) - return this - } - - fun build(): PlatformItemTransform - { - return PlatformItemTransform(rotation, translation, scale, rightRotation) - } - - fun end(): TransformsBuilder - { - return this@TransformsBuilder - } - } - } - - class PlatformItemTransform(rotation: Vector3f, translation: Vector3f, scale: Vector3f, rightRotation: Vector3f) - { - val rotation: Vector3f = Vector3f(rotation) - val translation: Vector3f = Vector3f(translation) - val scale: Vector3f = Vector3f(scale) - val rightRotation: Vector3f = Vector3f(rightRotation) - - constructor(rotation: Vector3f, translation: Vector3f, scale: Vector3f) : this( - rotation, - translation, - scale, - Vector3f() - ) - - fun apply(leftHand: Boolean, poseStack: PoseStack) - { - if (this !== NO_TRANSFORM) - { - val f = rotation.x() - var f1 = rotation.y() - var f2 = rotation.z() - if (leftHand) - { - f1 = -f1 - f2 = -f2 - } - val i = if (leftHand) -1 else 1 - poseStack.translate( - i.toFloat() * translation.x(), - translation.y(), translation.z() - ) - poseStack.mulPose( - Quaternionf().rotationXYZ( - f * (Math.PI.toFloat() / 180), - f1 * (Math.PI.toFloat() / 180), - f2 * (Math.PI.toFloat() / 180) - ) - ) - poseStack.scale(scale.x(), scale.y(), scale.z()) - poseStack.mulPose( - TransformationHelper.quatFromXYZ( - rightRotation.x(), - rightRotation.y() * (if (leftHand) -1 else 1).toFloat(), - rightRotation.z() * (if (leftHand) -1 else 1).toFloat(), true - ) - ) - } - } - - override fun equals(other: Any?): Boolean - { - if (this === other) - { - return true - } - if (this.javaClass != other?.javaClass) - { - return false - } - val itemtransform = other as PlatformItemTransform - return this.rotation == itemtransform.rotation && (this.scale == itemtransform.scale) && (this.translation == itemtransform.translation) - } - - override fun hashCode(): Int - { - var i = rotation.hashCode() - i = 31 * i + translation.hashCode() - return 31 * i + scale.hashCode() - } - - class Deserializer protected constructor() : JsonDeserializer - { - @Throws(JsonParseException::class) - override fun deserialize( - json: JsonElement, - type: Type, - context: JsonDeserializationContext - ): PlatformItemTransform - { - val jsonObject = json.asJsonObject - val vector3f = this.getVector3f(jsonObject, "rotation", DEFAULT_ROTATION) - val vector3f2 = this.getVector3f(jsonObject, "translation", DEFAULT_TRANSLATION) - vector3f2.mul(0.0625f) - vector3f2[Mth.clamp(vector3f2.x, -5.0f, 5.0f), Mth.clamp(vector3f2.y, -5.0f, 5.0f)] = - Mth.clamp(vector3f2.z, -5.0f, 5.0f) - val vector3f3 = this.getVector3f(jsonObject, "scale", DEFAULT_SCALE) - vector3f3[Mth.clamp(vector3f3.x, -4.0f, 4.0f), Mth.clamp(vector3f3.y, -4.0f, 4.0f)] = - Mth.clamp(vector3f3.z, -4.0f, 4.0f) - val rightRotation = - this.getVector3f(jsonObject, "right_rotation", DEFAULT_ROTATION) - return PlatformItemTransform(vector3f, vector3f2, vector3f3, rightRotation) - } - - private fun getVector3f(json: JsonObject, key: String, fallback: Vector3f): Vector3f - { - if (!json.has(key)) - { - return fallback - } - val jsonArray = GsonHelper.getAsJsonArray(json, key) - if (jsonArray.size() != 3) - { - throw JsonParseException("Expected 3 " + key + " values, found: " + jsonArray.size()) - } - val fs = FloatArray(3) - for (i in fs.indices) - { - fs[i] = GsonHelper.convertToFloat(jsonArray[i], "$key[$i]") - } - return Vector3f(fs[0], fs[1], fs[2]) - } - - companion object - { - val DEFAULT_ROTATION: Vector3f = Vector3f(0.0f, 0.0f, 0.0f) - val DEFAULT_TRANSLATION: Vector3f = Vector3f(0.0f, 0.0f, 0.0f) - val DEFAULT_SCALE: Vector3f = Vector3f(1.0f, 1.0f, 1.0f) - const val MAX_TRANSLATION: Float = 5.0f - const val MAX_SCALE: Float = 4.0f - } - } - - companion object - { - val NO_TRANSFORM: PlatformItemTransform = - PlatformItemTransform(Vector3f(), Vector3f(), Vector3f(1.0f, 1.0f, 1.0f)) - } - } - - inner class RootTransformsBuilder internal constructor() - { - private var translation = Vector3f() - private var leftRotation = Quaternionf() - private var rightRotation = Quaternionf() - private var scale = ONE - - private var origin: TransformationHelper.TransformOrigin? = null - private var originVec: Vector3f? = null - - /** - * Sets the translation of the root transform. - * - * @param translation the translation - * @return this builder - * @throws NullPointerException if `translation` is `null` - */ - fun translation(translation: Vector3f?): RootTransformsBuilder - { - this.translation = Preconditions.checkNotNull(translation, "Translation must not be null") - return this - } - - /** - * Sets the translation of the root transform. - * - * @param x x translation - * @param y y translation - * @param z z translation - * @return this builder - */ - fun translation(x: Float, y: Float, z: Float): RootTransformsBuilder - { - return translation(Vector3f(x, y, z)) - } - - /** - * Sets the left rotation of the root transform. - * - * @param rotation the left rotation - * @return this builder - * @throws NullPointerException if `rotation` is `null` - */ - fun rotation(rotation: Quaternionf?): RootTransformsBuilder - { - this.leftRotation = Preconditions.checkNotNull(rotation, "Rotation must not be null") - return this - } - - /** - * Sets the left rotation of the root transform. - * - * @param x x rotation - * @param y y rotation - * @param z z rotation - * @param isDegrees whether the rotation is in degrees or radians - * @return this builder - */ - fun rotation(x: Float, y: Float, z: Float, isDegrees: Boolean): RootTransformsBuilder - { - return rotation(TransformationHelper.quatFromXYZ(x, y, z, isDegrees)) - } - - /** - * Sets the left rotation of the root transform. - * - * @param leftRotation the left rotation - * @return this builder - * @throws NullPointerException if `leftRotation` is `null` - */ - fun leftRotation(leftRotation: Quaternionf?): RootTransformsBuilder - { - return rotation(leftRotation) - } - - fun leftRotation(x: Float, y: Float, z: Float, isDegrees: Boolean): RootTransformsBuilder - { - return leftRotation(TransformationHelper.quatFromXYZ(x, y, z, isDegrees)) - } - - fun rightRotation(rightRotation: Quaternionf?): RootTransformsBuilder - { - this.rightRotation = Preconditions.checkNotNull(rightRotation, "Rotation must not be null") - return this - } - - fun rightRotation(x: Float, y: Float, z: Float, isDegrees: Boolean): RootTransformsBuilder - { - return rightRotation( - TransformationHelper.quatFromXYZ( - x, - y, - z, - isDegrees - ) - ) - } - - fun postRotation(postRotation: Quaternionf?): RootTransformsBuilder - { - return rightRotation(postRotation) - } - - fun postRotation(x: Float, y: Float, z: Float, isDegrees: Boolean): RootTransformsBuilder - { - return postRotation(TransformationHelper.quatFromXYZ(x, y, z, isDegrees)) - } - - fun scale(scale: Float): RootTransformsBuilder - { - return scale(Vector3f(scale, scale, scale)) - } - - fun scale(xScale: Float, yScale: Float, zScale: Float): RootTransformsBuilder - { - return scale(Vector3f(xScale, yScale, zScale)) - } - - fun scale(scale: Vector3f?): RootTransformsBuilder - { - this.scale = Preconditions.checkNotNull(scale, "Scale must not be null") - return this - } - - fun transform(transformation: Transformation): RootTransformsBuilder - { - Preconditions.checkNotNull(transformation, "Transformation must not be null") - this.translation = transformation.translation - this.leftRotation = transformation.leftRotation - this.rightRotation = transformation.rightRotation - this.scale = transformation.scale - return this - } - - fun origin(origin: Vector3f?): RootTransformsBuilder - { - this.originVec = Preconditions.checkNotNull(origin, "Origin must not be null") - this.origin = null - return this - } - - fun origin(origin: TransformationHelper.TransformOrigin?): RootTransformsBuilder - { - this.origin = - Preconditions.checkNotNull( - origin, - "Origin must not be null" - ) - this.originVec = null - return this - } - - fun end(): AModelBuilder - { - return this@AModelBuilder - } - - fun toJson(): JsonObject - { - // Write the transform to an object - val transform = JsonObject() - - if (!translation.equals(0f, 0f, 0f)) - { - transform.add("translation", writeVec3(translation)) - } - - if (scale != ONE) - { - transform.add("scale", writeVec3(scale)) - } - - if (!leftRotation.equals(0f, 0f, 0f, 1f)) - { - transform.add("rotation", writeQuaternion(leftRotation)) - } - - if (!rightRotation.equals(0f, 0f, 0f, 1f)) - { - transform.add("post_rotation", writeQuaternion(rightRotation)) - } - - if (origin != null) - { - transform.addProperty("origin", origin!!.getSerializedName()) - } else if (originVec != null && !originVec!!.equals(0f, 0f, 0f)) - { - transform.add("origin", writeVec3(originVec!!)) - } - - return transform - } - } - - companion object - { - private val ONE = Vector3f(1f, 1f, 1f) - - private fun writeVec3(vector: Vector3f): JsonArray - { - val array = JsonArray() - array.add(vector.x()) - array.add(vector.y()) - array.add(vector.z()) - return array - } - - private fun writeQuaternion(quaternion: Quaternionf): JsonArray - { - val array = JsonArray() - array.add(quaternion.x()) - array.add(quaternion.y()) - array.add(quaternion.z()) - array.add(quaternion.w()) - return array - } - } - - -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelFile.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelFile.kt deleted file mode 100644 index df0eb9c2b..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelFile.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.kernelpanicsoft.archie.data.client.model - -import net.minecraft.resources.ResourceLocation - -/** A reference to a model JSON file by [location], usable as another model's `parent` or a variant's model. */ -open class AModelFile(val location: ResourceLocation) -{ - constructor(location: String) : this(ResourceLocation.parse(location)) - - override fun toString(): String - { - return location.toString() - } - -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelProvider.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelProvider.kt deleted file mode 100644 index e94bceeb4..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelProvider.kt +++ /dev/null @@ -1,498 +0,0 @@ -package net.kernelpanicsoft.archie.data.client.model - -import dev.architectury.platform.Mod -import com.google.common.base.Preconditions -import com.google.gson.Gson -import com.google.gson.GsonBuilder -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.data.IADataProvider -import net.minecraft.data.CachedOutput -import net.minecraft.data.DataProvider -import net.minecraft.data.PackOutput -import net.minecraft.resources.ResourceLocation -import org.jetbrains.annotations.VisibleForTesting -import java.nio.file.Path -import java.util.concurrent.CompletableFuture -import java.util.function.Function -import kotlin.system.exitProcess - -/** - * Datagen provider that builds model JSON files of builder type [T] (block or item models) - * under [folder], mirroring NeoForge's vanilla `ModelProvider` datagen helpers 1:1 in name and - * parameters (e.g. `cube`, `cubeAll`, `door*`, `fence*`, `pane*`, `trapdoor*`, `torch*`). Start a - * model with [getBuilder] or [withExistingParent], or use one of the vanilla-shape helpers. - */ -abstract class AModelProvider>( - final override val output: PackOutput, - final override val mod: Mod, - private val folder: String, - private val factory: Function, - final override val exitOnError: Boolean -) : IADataProvider -{ - @VisibleForTesting - val generatedModels: MutableMap = mutableMapOf() - - /** Called once during [generateAll] to register models via [getBuilder]/the shape helpers. */ - protected abstract fun generate() - - override fun getName(): String = format("Models") - - - /** Gets (or creates) the builder for the model at [path] (in [mod]'s namespace under [folder] unless [path] has its own namespace/subfolder), applying [block]. */ - fun getBuilder(path: String, block: T.() -> Unit = {}): T - { - Preconditions.checkNotNull(path, "Path must not be null") - val outputLoc = - extendWithFolder(if (path.contains(":")) ResourceLocation.parse(path) else ResourceLocation.fromNamespaceAndPath(mod.modId, path)) - return generatedModels.computeIfAbsent(outputLoc, factory).apply(block) - } - - private fun extendWithFolder(rl: ResourceLocation): ResourceLocation - { - if (rl.path.contains("/")) - { - return rl - } - return ResourceLocation.fromNamespaceAndPath(rl.namespace, folder + "/" + rl.path) - } - - /** [withExistingParent] with [parent] resolved as a `minecraft`-namespaced id. */ - fun withExistingParent(name: String, parent: String, block: T.() -> Unit = {}): T - { - return withExistingParent(name, mcLoc(parent)).apply(block) - } - - /** Gets (or creates) the builder for the model named [name], with its `parent` set to the existing model at [parent]. */ - fun withExistingParent(name: String, parent: ResourceLocation, block: T.() -> Unit = {}): T - { - return getBuilder(name).parent(getExistingFile(parent)).apply(block) - } - - fun cube( - name: String, - down: ResourceLocation, - up: ResourceLocation, - north: ResourceLocation, - south: ResourceLocation, - east: ResourceLocation, - west: ResourceLocation - ): T - { - return withExistingParent(name, "cube") - .texture("down", down) - .texture("up", up) - .texture("north", north) - .texture("south", south) - .texture("east", east) - .texture("west", west) - } - - private fun singleTexture(name: String, parent: String, texture: ResourceLocation): T - { - return singleTexture(name, mcLoc(parent), texture) - } - - fun singleTexture(name: String, parent: ResourceLocation, texture: ResourceLocation): T - { - return singleTexture(name, parent, "texture", texture) - } - - private fun singleTexture(name: String, parent: String, textureKey: String, texture: ResourceLocation): T - { - return singleTexture(name, mcLoc(parent), textureKey, texture) - } - - fun singleTexture(name: String, parent: ResourceLocation, textureKey: String, texture: ResourceLocation): T - { - return withExistingParent(name, parent) - .texture(textureKey, texture) - } - - fun cubeAll(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/cube_all", "all", texture) - } - - fun cubeTop(name: String, side: ResourceLocation, top: ResourceLocation): T - { - return withExistingParent(name, "$BLOCK_FOLDER/cube_top") - .texture("side", side) - .texture("top", top) - } - - private fun sideBottomTop( - name: String, - parent: String, - side: ResourceLocation, - bottom: ResourceLocation, - top: ResourceLocation - ): T - { - return withExistingParent(name, parent) - .texture("side", side) - .texture("bottom", bottom) - .texture("top", top) - } - - fun cubeBottomTop(name: String, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation): T - { - return sideBottomTop(name, "$BLOCK_FOLDER/cube_bottom_top", side, bottom, top) - } - - fun cubeColumn(name: String, side: ResourceLocation, end: ResourceLocation): T - { - return withExistingParent(name, "$BLOCK_FOLDER/cube_column") - .texture("side", side) - .texture("end", end) - } - - fun cubeColumnHorizontal(name: String, side: ResourceLocation, end: ResourceLocation): T - { - return withExistingParent(name, "$BLOCK_FOLDER/cube_column_horizontal") - .texture("side", side) - .texture("end", end) - } - - fun orientableVertical(name: String, side: ResourceLocation, front: ResourceLocation): T - { - return withExistingParent(name, "$BLOCK_FOLDER/orientable_vertical") - .texture("side", side) - .texture("front", front) - } - - fun orientableWithBottom( - name: String, - side: ResourceLocation, - front: ResourceLocation, - bottom: ResourceLocation, - top: ResourceLocation - ): T - { - return withExistingParent(name, "$BLOCK_FOLDER/orientable_with_bottom") - .texture("side", side) - .texture("front", front) - .texture("bottom", bottom) - .texture("top", top) - } - - fun orientable(name: String, side: ResourceLocation, front: ResourceLocation, top: ResourceLocation): T - { - return withExistingParent(name, "$BLOCK_FOLDER/orientable") - .texture("side", side) - .texture("front", front) - .texture("top", top) - } - - fun crop(name: String, crop: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/crop", "crop", crop) - } - - fun cross(name: String, cross: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/cross", "cross", cross) - } - - fun stairs(name: String, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation): T - { - return sideBottomTop(name, "$BLOCK_FOLDER/stairs", side, bottom, top) - } - - fun stairsOuter(name: String, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation): T - { - return sideBottomTop(name, "$BLOCK_FOLDER/outer_stairs", side, bottom, top) - } - - fun stairsInner(name: String, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation): T - { - return sideBottomTop(name, "$BLOCK_FOLDER/inner_stairs", side, bottom, top) - } - - fun slab(name: String, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation): T - { - return sideBottomTop(name, "$BLOCK_FOLDER/slab", side, bottom, top) - } - - fun slabTop(name: String, side: ResourceLocation, bottom: ResourceLocation, top: ResourceLocation): T - { - return sideBottomTop(name, "$BLOCK_FOLDER/slab_top", side, bottom, top) - } - - fun button(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/button", texture) - } - - fun buttonPressed(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/button_pressed", texture) - } - - fun buttonInventory(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/button_inventory", texture) - } - - fun pressurePlate(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/pressure_plate_up", texture) - } - - fun pressurePlateDown(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/pressure_plate_down", texture) - } - - fun sign(name: String, texture: ResourceLocation): T - { - return getBuilder(name).texture("particle", texture) - } - - fun fencePost(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/fence_post", texture) - } - - fun fenceSide(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/fence_side", texture) - } - - fun fenceInventory(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/fence_inventory", texture) - } - - fun fenceGate(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_fence_gate", texture) - } - - fun fenceGateOpen(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_fence_gate_open", texture) - } - - fun fenceGateWall(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_fence_gate_wall", texture) - } - - fun fenceGateWallOpen(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_fence_gate_wall_open", texture) - } - - fun wallPost(name: String, wall: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_wall_post", "wall", wall) - } - - fun wallSide(name: String, wall: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_wall_side", "wall", wall) - } - - fun wallSideTall(name: String, wall: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_wall_side_tall", "wall", wall) - } - - fun wallInventory(name: String, wall: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/wall_inventory", "wall", wall) - } - - private fun pane(name: String, parent: String, pane: ResourceLocation, edge: ResourceLocation): T - { - return withExistingParent(name, "$BLOCK_FOLDER/$parent") - .texture("pane", pane) - .texture("edge", edge) - } - - fun panePost(name: String, pane: ResourceLocation, edge: ResourceLocation): T - { - return pane(name, "template_glass_pane_post", pane, edge) - } - - fun paneSide(name: String, pane: ResourceLocation, edge: ResourceLocation): T - { - return pane(name, "template_glass_pane_side", pane, edge) - } - - fun paneSideAlt(name: String, pane: ResourceLocation, edge: ResourceLocation): T - { - return pane(name, "template_glass_pane_side_alt", pane, edge) - } - - fun paneNoSide(name: String, pane: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_glass_pane_noside", "pane", pane) - } - - fun paneNoSideAlt(name: String, pane: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_glass_pane_noside_alt", "pane", pane) - } - - private fun door(name: String, model: String, bottom: ResourceLocation, top: ResourceLocation): T - { - return withExistingParent(name, "$BLOCK_FOLDER/$model") - .texture("bottom", bottom) - .texture("top", top) - } - - fun doorBottomLeft(name: String, bottom: ResourceLocation, top: ResourceLocation): T - { - return door(name, "door_bottom_left", bottom, top) - } - - fun doorBottomLeftOpen(name: String, bottom: ResourceLocation, top: ResourceLocation): T - { - return door(name, "door_bottom_left_open", bottom, top) - } - - fun doorBottomRight(name: String, bottom: ResourceLocation, top: ResourceLocation): T - { - return door(name, "door_bottom_right", bottom, top) - } - - fun doorBottomRightOpen(name: String, bottom: ResourceLocation, top: ResourceLocation): T - { - return door(name, "door_bottom_right_open", bottom, top) - } - - fun doorTopLeft(name: String, bottom: ResourceLocation, top: ResourceLocation): T - { - return door(name, "door_top_left", bottom, top) - } - - fun doorTopLeftOpen(name: String, bottom: ResourceLocation, top: ResourceLocation): T - { - return door(name, "door_top_left_open", bottom, top) - } - - fun doorTopRight(name: String, bottom: ResourceLocation, top: ResourceLocation): T - { - return door(name, "door_top_right", bottom, top) - } - - fun doorTopRightOpen(name: String, bottom: ResourceLocation, top: ResourceLocation): T - { - return door(name, "door_top_right_open", bottom, top) - } - - fun trapdoorBottom(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_trapdoor_bottom", texture) - } - - fun trapdoorTop(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_trapdoor_top", texture) - } - - fun trapdoorOpen(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_trapdoor_open", texture) - } - - fun trapdoorOrientableBottom(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_orientable_trapdoor_bottom", texture) - } - - fun trapdoorOrientableTop(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_orientable_trapdoor_top", texture) - } - - fun trapdoorOrientableOpen(name: String, texture: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_orientable_trapdoor_open", texture) - } - - fun torch(name: String, torch: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_torch", "torch", torch) - } - - fun torchWall(name: String, torch: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/template_torch_wall", "torch", torch) - } - - fun carpet(name: String, wool: ResourceLocation): T - { - return singleTexture(name, "$BLOCK_FOLDER/carpet", "wool", wool) - } - - /** A model builder that's not registered/saved to disk, meant for inline use inside a custom model loader's JSON. */ - fun nested(): T - { - return factory.apply(ResourceLocation.parse("dummy:dummy")) - } - - /** References the model at [path] (extended with [folder] if it has no subfolder) without requiring it to already be built by this provider. */ - fun getExistingFile(path: ResourceLocation): AModelFile - { - val ret = - AModelFile( - extendWithFolder(path) - ) - return ret - } - - /** Discards every model registered so far via [getBuilder]. */ - fun clear() - { - generatedModels.clear() - } - - override fun run(cache: CachedOutput): CompletableFuture<*> - { - clear() - runCatching { - generate() - }.onFailure { - Archie.LOGGER.error( - "Data Provider $name failed with exception: ${it.message}\n" + - "Stacktrace: ${it.stackTraceToString()}" - ) - if (exitOnError) exitProcess(-1) - } - return generateAll(cache) - } - - /** Writes every model currently registered via [getBuilder] to disk under [cache]. */ - fun generateAll(cache: CachedOutput): CompletableFuture<*> - { - val futures: Array?> = arrayOfNulls( - generatedModels.size - ) - - for ((i, model) in generatedModels.values.withIndex()) - { - val target = getPath(model) - futures[i] = DataProvider.saveStable(cache, model.toJson(), target) - } - - return CompletableFuture.allOf(*futures) - } - - protected fun getPath(model: T): Path - { - val loc: ResourceLocation = model.location - return output.getOutputFolder(PackOutput.Target.RESOURCE_PACK).resolve(loc.namespace).resolve("models") - .resolve(loc.path + ".json") - } - - companion object - { - const val BLOCK_FOLDER: String = "block" - const val ITEM_FOLDER: String = "item" - - private val GSON: Gson = GsonBuilder().setPrettyPrinting().create() - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AMultiPartBlockStateBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AMultiPartBlockStateBuilder.kt deleted file mode 100644 index 53a089ed7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AMultiPartBlockStateBuilder.kt +++ /dev/null @@ -1,271 +0,0 @@ -package net.kernelpanicsoft.archie.data.client.model - -import com.google.common.base.Preconditions -import com.google.common.collect.Multimap -import com.google.common.collect.MultimapBuilder -import com.google.gson.JsonArray -import com.google.gson.JsonObject -import net.minecraft.world.level.block.Block -import net.minecraft.world.level.block.state.properties.Property -import java.util.* - -/** - * Builds a `multipart`-style blockstate JSON for [owner], where each [PartBuilder] applies its - * model(s) whenever its `when` conditions (block property values, optionally grouped with - * AND/OR) match. Obtain via [ABlockStateProvider.getMultipartBuilder]. - */ -class AMultiPartBlockStateBuilder(private val owner: Block) : IAGeneratedBlockState -{ - private val parts: MutableList = ArrayList() - - private var config: (PartBuilder.() -> Unit)? = null - - /** Starts an [AConfiguredModel.Builder] whose [AConfiguredModel.Builder.addModel] creates and adds a new unconditional [PartBuilder]. */ - fun part(): AConfiguredModel.Builder - { - return AConfiguredModel.builder(this) - } - - /** Builds a new [PartBuilder] with its model(s) declared in [block], adds it, and applies any pending [configure] callback. */ - fun part(block: AConfiguredModel.Builder.() -> Unit): PartBuilder - { - return AConfiguredModel.builder(this) - .apply(block) - .addModel() - .apply { - config?.let { it() } - } - } - - /** Registers an already-built [part]. */ - fun addPart(part: PartBuilder): AMultiPartBlockStateBuilder - { - parts.add(part) - return this - } - - /** Sets a callback run against every part built by [part] after this call, e.g. to add shared conditions. */ - fun configure(block: (PartBuilder.() -> Unit)?) - { - config = block - } - - /** Serializes to the `{"multipart": [...]}` blockstate JSON. */ - override fun toJson(): JsonObject - { - val variants = JsonArray() - for (part in parts) - { - variants.add(part.toJson()) - } - val main = JsonObject() - main.add("multipart", variants) - return main - } - - /** A single `multipart` entry: applies [models] when its (possibly nested) conditions match. */ - inner class PartBuilder internal constructor(models: ABlockStateProvider.ConfiguredModelList) - { - var models: ABlockStateProvider.ConfiguredModelList = models - var useOr: Boolean = false - val conditions: Multimap, Comparable<*>> = - MultimapBuilder.linkedHashKeys().arrayListValues().build() - val nestedConditionGroups: MutableList = ArrayList() - - /** Combines this part's [conditions] with OR instead of the default AND. */ - fun useOr(): PartBuilder - { - this.useOr = true - return this - } - - /** Requires [prop] to equal one of [values] (OR'd together) for this part to apply. Cannot be mixed with [nestedGroup]. */ - @SafeVarargs - fun > condition(prop: Property, vararg values: T): PartBuilder - { - Preconditions.checkNotNull(prop, "Property must not be null") - Preconditions.checkNotNull(values, "Value list must not be null") - Preconditions.checkArgument(values.isNotEmpty(), "Value list must not be empty") - Preconditions.checkArgument( - !conditions.containsKey(prop), - "Cannot set condition for property \"%s\" more than once", - prop.name - ) - Preconditions.checkArgument( - canApplyTo(owner), "IProperty %s is not valid for the block %s", prop, - owner - ) - Preconditions.checkState( - nestedConditionGroups.isEmpty(), - "Can't have normal conditions if there are already nested condition groups" - ) - conditions.putAll(prop, listOf(*values)) - return this - } - - /** Starts a nested [ConditionGroup] under this part. Cannot be mixed with [condition]. */ - fun nestedGroup(): ConditionGroup - { - Preconditions.checkState( - conditions.isEmpty, - "Can't have nested condition groups if there are already normal conditions" - ) - val group = ConditionGroup() - nestedConditionGroups.add(group) - return group - } - - /** Returns to the enclosing [AMultiPartBlockStateBuilder]. */ - fun end(): AMultiPartBlockStateBuilder - { - return this@AMultiPartBlockStateBuilder - } - - /** Serializes this part's `when`/`apply` entry. */ - fun toJson(): JsonObject - { - val out = JsonObject() - if (!conditions.isEmpty) - { - out.add("when", toJson(this.conditions, this.useOr)) - } else if (nestedConditionGroups.isNotEmpty()) - { - out.add("when", toJson(this.nestedConditionGroups, this.useOr)) - } - out.add("apply", models.toJSON()) - return out - } - - /** Whether every property referenced by this part's conditions exists on [b]. */ - fun canApplyTo(b: Block): Boolean - { - return b.stateDefinition.properties.containsAll(conditions.keySet()) - } - - /** A nested AND/OR group of conditions within a [PartBuilder]'s `when` clause. */ - inner class ConditionGroup - { - val conditions: Multimap, Comparable<*>> = - MultimapBuilder.linkedHashKeys().arrayListValues().build() - val nestedConditionGroups: MutableList = ArrayList() - private var parent: ConditionGroup? = null - var useOr: Boolean = false - - @SafeVarargs - fun ?> condition(prop: Property, vararg values: T): ConditionGroup - { - Preconditions.checkNotNull(prop, "Property must not be null") - Preconditions.checkNotNull(values, "Value list must not be null") - Preconditions.checkArgument(values.isNotEmpty(), "Value list must not be empty") - Preconditions.checkArgument( - !conditions.containsKey(prop), - "Cannot set condition for property \"%s\" more than once", - prop.name - ) - Preconditions.checkArgument( - canApplyTo(owner), "IProperty %s is not valid for the block %s", prop, - owner - ) - Preconditions.checkState( - nestedConditionGroups.isEmpty(), - "Can't have normal conditions if there are already nested condition groups" - ) - this.conditions.putAll(prop, listOf(*values)) - return this - } - - fun nestedGroup(): ConditionGroup - { - Preconditions.checkState( - conditions.isEmpty, - "Can't have nested condition groups if there are already normal conditions" - ) - val group = ConditionGroup() - group.parent = this - this.nestedConditionGroups.add(group) - return group - } - - fun endNestedGroup(): ConditionGroup - { - checkNotNull(parent) { "This condition group is not nested, use end() instead" } - return parent!! - } - - fun end(): PartBuilder - { - check(this.parent == null) { "This is a nested condition group, use endNestedGroup() instead" } - return this@PartBuilder - } - - fun useOr(): ConditionGroup - { - this.useOr = true - return this - } - - fun toJson(): JsonObject - { - if (!this.conditions.isEmpty) - { - return toJson(this.conditions, this.useOr) - } else if (this.nestedConditionGroups.isNotEmpty()) - { - return toJson(this.nestedConditionGroups, this.useOr) - } - return JsonObject() - } - } - } - - companion object - { - @Suppress("UNCHECKED_CAST") - private fun propertyValueName(key: Property<*>, value: Comparable<*>): String - { - val typedKey = key as Property> - val typedValue = value as Comparable - return typedKey.getName(typedValue) - } - - private fun toJson(conditions: List, useOr: Boolean): JsonObject - { - val groupJson = JsonObject() - val innerGroupJson = JsonArray() - groupJson.add(if (useOr) "OR" else "AND", innerGroupJson) - for (group in conditions) - { - innerGroupJson.add(group.toJson()) - } - return groupJson - } - - private fun toJson(conditions: Multimap, Comparable<*>>, useOr: Boolean): JsonObject - { - var groupJson = JsonObject() - for ((key, value) in conditions.asMap()) - { - val activeString = StringBuilder() - for (`val` in value) - { - if (activeString.isNotEmpty()) activeString.append("|") - activeString.append(propertyValueName(key, `val`)) - } - groupJson.addProperty(key.name, activeString.toString()) - } - if (useOr) - { - val innerWhen = JsonArray() - for ((key, value) in groupJson.entrySet()) - { - val obj = JsonObject() - obj.add(key, value) - innerWhen.add(obj) - } - groupJson = JsonObject() - groupJson.add("OR", innerWhen) - } - return groupJson - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AVariantBlockStateBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AVariantBlockStateBuilder.kt deleted file mode 100644 index fd9df7e16..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AVariantBlockStateBuilder.kt +++ /dev/null @@ -1,328 +0,0 @@ -package net.kernelpanicsoft.archie.data.client.model - -import com.google.common.base.Preconditions -import com.google.common.collect.ImmutableMap -import com.google.common.collect.Lists -import com.google.common.collect.Maps -import com.google.gson.JsonObject -import net.minecraft.world.level.block.Block -import net.minecraft.world.level.block.state.BlockState -import net.minecraft.world.level.block.state.properties.Property -import java.util.* -import java.util.function.Function -import java.util.function.Predicate - -/** - * Builds a `variants`-style blockstate JSON for [owner], mapping [PartialBlockstate] property - * combinations to one or more [AConfiguredModel]s. Obtain via - * [ABlockStateProvider.getVariantBuilder]; every possible [BlockState] of [owner] must end up - * covered by some registered [PartialBlockstate] before [toJson] is called (e.g. via - * [forAllStates]/[forAllStatesExcept], or individual [partialState] + [setModels] calls). - */ -class AVariantBlockStateBuilder internal constructor(val owner: Block) : IAGeneratedBlockState -{ - private val models: MutableMap = - LinkedHashMap() - private val coveredStates: MutableSet = HashSet() - - /** The configured models, keyed by the partial state they apply to. */ - fun getModels(): Map - { - return models - } - - /** Serializes to the `{"variants": {...}}` blockstate JSON. Throws if any state of [owner] is uncovered. */ - override fun toJson(): JsonObject - { - val missingStates: MutableList = Lists.newArrayList( - owner.stateDefinition.possibleStates - ) - missingStates.removeAll(coveredStates) - Preconditions.checkState( - missingStates.isEmpty(), "Blockstate for block %s does not cover all states. Missing: %s", - owner, missingStates - ) - val variants = JsonObject() - getModels().entries.stream() - .sorted(java.util.Map.Entry.comparingByKey(PartialBlockstate.comparingByProperties())) - .forEach { entry: Map.Entry -> - variants.add( - entry.key.toString(), - entry.value.toJSON() - ) - } - val main = JsonObject() - main.add("variants", variants) - return main - } - - /** Adds [models] as (further) choices for [state], appending if [state] was already configured. */ - fun addModels( - state: PartialBlockstate, - vararg models: AConfiguredModel - ): AVariantBlockStateBuilder - { - Preconditions.checkNotNull(state, "state must not be null") - Preconditions.checkArgument(models.isNotEmpty(), "Cannot set models to empty array") - Preconditions.checkArgument( - state.owner === owner, "Cannot set models for a different block. Found: %s, Current: %s", - state.owner, owner - ) - if (!this.models.containsKey(state)) - { - Preconditions.checkArgument( - disjointToAll(state), - "Cannot set models for a state for which a partial match has already been configured" - ) - this.models[state] = ABlockStateProvider.ConfiguredModelList(*models) - for (fullState in owner.stateDefinition.possibleStates) - { - if (state.test(fullState)) - { - coveredStates.add(fullState) - } - } - } else - { - this.models.compute(state - ) { _: PartialBlockstate, cml: ABlockStateProvider.ConfiguredModelList? -> - cml?.append( - *models - ) - } - } - return this - } - - /** Sets [model] as the choices for [state]. Throws if [state] was already configured. */ - fun setModels( - state: PartialBlockstate, - vararg model: AConfiguredModel - ): AVariantBlockStateBuilder - { - Preconditions.checkArgument( - !models.containsKey(state), - "Cannot set models for a state that has already been configured: %s", - state - ) - addModels(state, *model) - return this - } - - private fun disjointToAll(newState: PartialBlockstate): Boolean - { - return coveredStates.stream().noneMatch(newState) - } - - /** Starts a new [PartialBlockstate] with no properties set yet, to be refined via [PartialBlockstate.with]. */ - fun partialState(): PartialBlockstate - { - return PartialBlockstate(owner, this) - } - - /** Configures every possible [BlockState] of [owner] individually via [mapper]. */ - fun forAllStates(mapper: Function>): AVariantBlockStateBuilder - { - return forAllStatesExcept(mapper) - } - - /** Like [forAllStates], but groups states that only differ in [ignored] properties under one partial state. */ - fun forAllStatesExcept( - mapper: Function>, - vararg ignored: Property<*> - ): AVariantBlockStateBuilder - { - val seen: MutableSet = HashSet() - for (fullState in owner.stateDefinition.possibleStates) - { - val propertyValues: MutableMap, Comparable<*>> = Maps.newLinkedHashMap(fullState.values) - for (p in ignored) - { - propertyValues.remove(p) - } - val partialState = PartialBlockstate( - owner, propertyValues, this - ) - if (seen.add(partialState)) - { - setModels(partialState, *mapper.apply(fullState)) - } - } - return this - } - - /** - * An immutable, partially or fully specified combination of block properties, matching every - * [BlockState] of [owner] that agrees with [setStates] (unset properties match any value). - * Refine with [with]; assign models with [addModels]/[setModels]/[modelForState]. - */ - class PartialBlockstate internal constructor( - val owner: Block, - setStates: Map, Comparable<*>>, - private val outerBuilder: AVariantBlockStateBuilder - ) : - Predicate - { - val setStates: SortedMap, Comparable<*>> - - internal constructor(owner: Block, outerBuilder: AVariantBlockStateBuilder) : this( - owner, - ImmutableMap.of, Comparable<*>>(), - outerBuilder - ) - - init - { - for (entry in setStates.entries) - { - val prop = entry.key - val value = entry.value - Preconditions.checkArgument( - owner.stateDefinition.properties.contains(prop), "Property %s not found on block %s", entry, - this.owner - ) - Preconditions.checkArgument( - prop.possibleValues.contains(value), - "%s is not a valid value for %s", - value, - prop - ) - } - this.setStates = Maps.newTreeMap(Comparator.comparing { obj: Property<*> -> obj.name }) - this.setStates.putAll(setStates) - } - - /** Returns a new [PartialBlockstate] with [prop] additionally pinned to [value]. Throws if [prop] is already set. */ - fun > with(prop: Property, value: T): PartialBlockstate - { - Preconditions.checkArgument(!setStates.containsKey(prop), "Property %s has already been set", prop) - val newState: MutableMap, Comparable<*>> = HashMap(setStates) - newState[prop] = value - return PartialBlockstate(owner, newState, outerBuilder) - } - - private fun checkValidOwner() - { - Preconditions.checkNotNull( - outerBuilder, - "Partial blockstate must have a valid owner to perform this action" - ) - } - - /** Starts an [AConfiguredModel.Builder] whose [AConfiguredModel.Builder.addModel] assigns the result to this state. */ - fun modelForState(): AConfiguredModel.Builder - { - checkValidOwner() - return AConfiguredModel.builder(outerBuilder, this) - } - - /** Adds [models] as (further) choices for this state; see [AVariantBlockStateBuilder.addModels]. */ - fun addModels(vararg models: AConfiguredModel): PartialBlockstate - { - checkValidOwner() - outerBuilder!!.addModels(this, *models) - return this - } - - /** Sets [models] as the choices for this state; see [AVariantBlockStateBuilder.setModels]. */ - fun setModels(vararg models: AConfiguredModel): AVariantBlockStateBuilder - { - checkValidOwner() - return outerBuilder!!.setModels(this, *models) - } - - /** Starts a new, unrelated [PartialBlockstate] on the same owning builder; see [AVariantBlockStateBuilder.partialState]. */ - fun partialState(): PartialBlockstate - { - checkValidOwner() - return outerBuilder!!.partialState() - } - - override fun equals(other: Any?): Boolean - { - if (this === other) return true - if (other == null || javaClass != other.javaClass) return false - val that = other as PartialBlockstate - return owner == that.owner && setStates == that.setStates - } - - override fun hashCode(): Int - { - return Objects.hash(owner, setStates) - } - - override fun test(blockState: BlockState): Boolean - { - if (blockState.block !== owner) - { - return false - } - for ((key, value) in setStates) - { - if (blockState.getValue(key) !== value) - { - return false - } - } - return true - } - - override fun toString(): String - { - val ret = StringBuilder() - for ((key, value) in setStates) - { - if (ret.isNotEmpty()) - { - ret.append(',') - } - @Suppress("UNCHECKED_CAST") - ret.append(key.name) - .append('=') - .append( - (key as Property>).getName( - value as Comparable - ) - ) - } - return ret.toString() - } - - companion object - { - /** Comparator ordering states by property values, approximating vanilla's blockstate JSON ordering. */ - fun comparingByProperties(): Comparator - { - // Sort variants inversely by property values, to approximate vanilla style - return Comparator { s1: PartialBlockstate, s2: PartialBlockstate -> - val propUniverse: SortedSet> = - TreeSet( - s1.setStates.comparator().reversed() - ) - propUniverse.addAll(s1.setStates.keys) - propUniverse.addAll(s2.setStates.keys) - for (prop in propUniverse) - { - val val1 = s1.setStates[prop] - val val2 = s2.setStates[prop] - if (val1 == null && val2 != null) - { - return@Comparator -1 - } else if (val2 == null && val1 != null) - { - return@Comparator 1 - } else if (val1 != null && val2 != null) - { - @Suppress("UNCHECKED_CAST") val cmp = (val1 as Comparable).compareTo(val2) - if (cmp != 0) - { - return@Comparator cmp - } - } - } - 0 - } - } - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/IAGeneratedBlockState.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/IAGeneratedBlockState.kt deleted file mode 100644 index dc1bc95da..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/IAGeneratedBlockState.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.kernelpanicsoft.archie.data.client.model - -import com.google.gson.JsonObject - -/** Something that can be serialized as a complete `blockstates` JSON document, e.g. [AVariantBlockStateBuilder]/[AMultiPartBlockStateBuilder]. */ -interface IAGeneratedBlockState -{ - /** Serializes this blockstate to its JSON representation. */ - fun toJson(): JsonObject -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AAndCondition.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AAndCondition.kt deleted file mode 100644 index 209ea31ab..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AAndCondition.kt +++ /dev/null @@ -1,35 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.google.common.base.Joiner -import com.mojang.serialization.MapCodec -import com.mojang.serialization.codecs.RecordCodecBuilder -import net.kernelpanicsoft.archie.Archie -import net.minecraft.resources.ResourceLocation - -/** Condition that holds only when every one of [children] holds (logical AND). */ -data class AAndCondition(override val children: List) : - AGroupCondition() -{ - constructor(vararg values: IACondition) : this(values.toList()) - - override fun reducer(a: Boolean, b: Boolean): Boolean = a and b - - override val codec: MapCodec = CODEC - override val identifier: ResourceLocation = ID - - override fun toString(): String - { - return "(${Joiner.on(" && ").join(children)})" - } - - companion object - { - val CODEC: MapCodec = - RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> - builder.group( - IACondition.CODEC.listOf().fieldOf("children").forGetter(AAndCondition::children) - ).apply(builder, ::AAndCondition) - } - val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "and") - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ABuiltinConditions.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ABuiltinConditions.kt deleted file mode 100644 index e05139993..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ABuiltinConditions.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.mojang.serialization.MapCodec -import net.minecraft.resources.ResourceLocation - -/** Registers Archie's built-in [IACondition] types (logical combinators + [AModLoadedCondition]/[ARegistryCondition]/[APlatformCondition]) so their codecs can decode from datapacks. */ -object ABuiltinConditions -{ - /** Registers every built-in condition type; called once during [net.kernelpanicsoft.archie.Archie.init]. */ - fun init() - { - register(AModLoadedCondition.ID, AModLoadedCondition.CODEC) - register(ARegistryCondition.ID, ARegistryCondition.CODEC) - register(APlatformCondition.ID, APlatformCondition.CODEC) - - register(AAndCondition.ID, AAndCondition.CODEC) - register(AOrCondition.ID, AOrCondition.CODEC) - register(AXorCondition.ID, AXorCondition.CODEC) - register(ANotCondition.ID, ANotCondition.CODEC) - register(AEqualsCondition.ID, AEqualsCondition.CODEC) - - register(ATrueCondition.ID, ATrueCondition.CODEC) - register(AFalseCondition.ID, AFalseCondition.CODEC) - } - - private inline fun register(identifier: ResourceLocation, codec: MapCodec) - { - IACondition.register(identifier, codec) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt deleted file mode 100644 index f16f848c2..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt +++ /dev/null @@ -1,62 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import net.minecraft.core.Registry -import net.minecraft.resources.ResourceKey -import net.minecraft.resources.ResourceLocation - -/** - * DSL for building [IACondition] trees with infix/operator combinators (`and`, `or`, `xor`, - * `eql`, their negated `n*` counterparts, and `!` operator aliases) plus factory - * functions for the leaf conditions ([mod], [registry], [platform], [TRUE], [FALSE]). - * - * Import the members (`import ...AConditionBuilder.*`) to write conditions like - * `mod("architectury") and platform(FABRIC)`. - */ -object AConditionBuilder -{ - /** [AAndCondition] of `this` and [other]. */ - - - fun and(vararg values: IACondition): IACondition = AAndCondition(*values) - fun or(vararg values: IACondition): IACondition = AOrCondition(*values) - fun xor(vararg values: IACondition): IACondition = AXorCondition(*values) - fun eql(vararg values: IACondition): IACondition = AEqualsCondition(*values) - - fun nand(vararg values: IACondition): IACondition = !and(*values) - fun nor(vararg values: IACondition): IACondition = !or(*values) - fun xnor(vararg values: IACondition): IACondition = !xor(*values) - fun neql(vararg values: IACondition): IACondition = !eql(*values) - - infix fun IACondition.and(other: IACondition): IACondition = and(this, other) - infix fun IACondition.or(other: IACondition): IACondition = or(this, other) - infix fun IACondition.xor(other: IACondition): IACondition = xor(this, other) - infix fun IACondition.eql(other: IACondition): IACondition = eql(this, other) - - infix fun IACondition.nand(other: IACondition): IACondition = !(this and other) - infix fun IACondition.nor(other: IACondition): IACondition = !(this or other) - infix fun IACondition.xnor(other: IACondition): IACondition = !(this xor other) - infix fun IACondition.neql(other: IACondition): IACondition = !(this eql other) - - operator fun IACondition.not(): IACondition = ANotCondition(this) - - /** Always-true condition; see [ATrueCondition]. */ - val TRUE = ATrueCondition - - /** Always-false condition; see [AFalseCondition]. */ - val FALSE = AFalseCondition - - /** Condition that holds when every mod id in [mods] is loaded. */ - fun mod(vararg mods: String): IACondition = AModLoadedCondition(*mods) - - /** Condition that holds when every one of [entries] is registered in [registry]. */ - fun registry(registry: ResourceKey>, vararg entries: ResourceLocation): IACondition = ARegistryCondition(registry.location(), *entries) - - /** Condition that holds when every one of [entries] is registered in [registry]. */ - fun registry(registry: Registry<*>, vararg entries: ResourceLocation): IACondition = ARegistryCondition(registry.key().location(), *entries) - - /** Condition that holds when the running loader's platform id equals [platform]; see [FABRIC]/[NEOFORGE]. */ - fun platform(platform: String): IACondition = APlatformCondition(platform) - - const val FABRIC = "fabric" - const val NEOFORGE = "neoforge" -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt deleted file mode 100644 index 425be65ae..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt +++ /dev/null @@ -1,29 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.mojang.serialization.Codec -import com.mojang.serialization.MapCodec -import net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider -import net.minecraft.core.HolderLookup -import net.minecraft.data.recipes.RecipeOutput -import net.minecraft.data.recipes.RecipeProvider -import net.minecraft.resources.ResourceLocation -import java.util.concurrent.CompletableFuture - -/** - * Cross-loader hooks that plug [IACondition] into each loader's native datapack condition - * system, since Fabric and NeoForge each have their own recipe/tag condition machinery. - */ -expect object AConditionsPlatform -{ - /** Registers condition type keyed by [identifier], decodable with [codec]; backs [IACondition.register]. */ - fun register(identifier: ResourceLocation, codec: MapCodec) - - /** Attaches [condition] to the next recipe written to [output] via the loader's native mechanism. */ - fun withCondition(output: RecipeOutput, condition: IACondition): RecipeOutput - - /** The dispatch codec decoding any registered [IACondition]; backs [IACondition.CODEC]. */ - fun codec(): Codec - - /** Wraps [child] in a loader-specific [RecipeProvider] so [withCondition] can attach conditions on Fabric; `null` where not needed. */ - fun fabricRecipeProvider(child: ARecipeProvider, registries: CompletableFuture): RecipeProvider? -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AEqualsCondition.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AEqualsCondition.kt deleted file mode 100644 index 9280541dd..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AEqualsCondition.kt +++ /dev/null @@ -1,36 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.google.common.base.Joiner -import com.mojang.serialization.MapCodec -import com.mojang.serialization.codecs.RecordCodecBuilder -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.util.rem -import net.minecraft.resources.ResourceLocation - -/** Condition that folds [children]'s results pairwise with `==` (holds when they agree). */ -data class AEqualsCondition(override val children: List) : - AGroupCondition() -{ - constructor(vararg values: IACondition) : this(values.toList()) - - override fun reducer(a: Boolean, b: Boolean): Boolean = a == b - - override val codec: MapCodec = CODEC - override val identifier: ResourceLocation = ID - - override fun toString(): String - { - return "(${Joiner.on(" == ").join(children)})" - } - - companion object - { - val CODEC: MapCodec = - RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> - builder.group( - IACondition.CODEC.listOf().fieldOf("children").forGetter(AEqualsCondition::children), - ).apply(builder, ::AEqualsCondition) - } - val ID: ResourceLocation = Archie % "equals" - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AFalseCondition.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AFalseCondition.kt deleted file mode 100644 index 138bed941..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AFalseCondition.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.mojang.serialization.MapCodec -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.util.rem -import net.minecraft.resources.ResourceLocation - -/** Condition that never holds. */ -data object AFalseCondition : - IACondition -{ - val CODEC: MapCodec = MapCodec.unit(AFalseCondition).stable() - val ID: ResourceLocation = Archie % "false" - override fun test(context: IACondition.IContext): Boolean - { - return false - } - - override val codec: MapCodec = CODEC - override val identifier: ResourceLocation = ID - - override fun toString(): String - { - return "false" - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AGroupCondition.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AGroupCondition.kt deleted file mode 100644 index e0c46fc90..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AGroupCondition.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -/** - * Base for [IACondition]s that combine [children] pairwise via [reducer], e.g. [AAndCondition], - * [AOrCondition], [AXorCondition]. [children] must be non-empty. - */ -abstract class AGroupCondition : IACondition -{ - abstract val children: List - - /** Combines two child results into one; applied left-to-right across [children]. */ - abstract fun reducer(a: Boolean, b: Boolean): Boolean - - override fun test(context: IACondition.IContext): Boolean = - children.map { it.test(context) }.reduce(::reducer) -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AModLoadedCondition.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AModLoadedCondition.kt deleted file mode 100644 index f890955ba..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AModLoadedCondition.kt +++ /dev/null @@ -1,46 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.google.common.base.Joiner -import com.mojang.serialization.Codec -import com.mojang.serialization.MapCodec -import com.mojang.serialization.codecs.RecordCodecBuilder -import net.kernelpanicsoft.archie.Archie -import dev.architectury.platform.Platform -import net.minecraft.resources.ResourceLocation - -/** Condition that holds when every mod id in [mods] is loaded, per [Platform.isModLoaded]. */ -data class AModLoadedCondition(val mods: List) : IACondition -{ - constructor(vararg mods: String) : this(mods.toList()) - override fun test(context: IACondition.IContext): Boolean - { - return mods.map(Platform::isModLoaded).reduce { a, b -> a && b } - } - - override val codec: MapCodec = CODEC - override val identifier: ResourceLocation = ID - - override fun toString(): String - { - return "mod_loaded(${Joiner.on(", ").join(mods.map { "\"$it\"" })})" - } - - companion object - { - val CODEC: MapCodec = - RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> - builder - .group( - Codec.STRING.listOf().fieldOf("mods").forGetter(AModLoadedCondition::mods) - ) - .apply( - builder - ) { mods: List -> - AModLoadedCondition( - mods - ) - } - } - val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "mod_loaded") - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ANotCondition.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ANotCondition.kt deleted file mode 100644 index 7163f2ccd..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ANotCondition.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.mojang.serialization.MapCodec -import com.mojang.serialization.codecs.RecordCodecBuilder -import net.kernelpanicsoft.archie.Archie -import net.minecraft.resources.ResourceLocation - -/** Condition that inverts the result of [child] (logical NOT). */ -data class ANotCondition(val child: IACondition) : - IACondition -{ - override fun test(context: IACondition.IContext): Boolean - { - return !child.test(context) - } - - override val codec: MapCodec = CODEC - override val identifier: ResourceLocation = ID - - override fun toString(): String - { - return "!$child" - } - companion object - { - val CODEC: MapCodec = - RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> - builder.group( - IACondition.CODEC.fieldOf("child").forGetter(ANotCondition::child) - ).apply(builder, ::ANotCondition) - } - val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "not") - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AOrCondition.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AOrCondition.kt deleted file mode 100644 index a728aa2da..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AOrCondition.kt +++ /dev/null @@ -1,35 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.google.common.base.Joiner -import com.mojang.serialization.MapCodec -import com.mojang.serialization.codecs.RecordCodecBuilder -import net.kernelpanicsoft.archie.Archie -import net.minecraft.resources.ResourceLocation - -/** Condition that holds when at least one of [children] holds (logical OR). */ -data class AOrCondition(override val children: List) : - AGroupCondition() -{ - constructor(vararg values: IACondition) : this(values.toList()) - - override fun reducer(a: Boolean, b: Boolean): Boolean = a or b - - override val codec: MapCodec = CODEC - override val identifier: ResourceLocation = ID - - override fun toString(): String - { - return "(${Joiner.on(" || ").join(children)})" - } - - companion object - { - val CODEC: MapCodec = - RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> - builder.group( - IACondition.CODEC.listOf().fieldOf("children").forGetter(AOrCondition::children), - ).apply(builder, ::AOrCondition) - } - val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "or") - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/APlatformCondition.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/APlatformCondition.kt deleted file mode 100644 index fd7ea81a7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/APlatformCondition.kt +++ /dev/null @@ -1,47 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.mojang.serialization.Codec -import com.mojang.serialization.MapCodec -import com.mojang.serialization.codecs.RecordCodecBuilder -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.APlatform -import kotlinx.serialization.Transient -import net.minecraft.resources.ResourceLocation - -/** Condition that holds when the running loader's [APlatform.platform] id equals [platform]. */ -data class APlatformCondition(val platform: String) : IACondition -{ - override fun test(context: IACondition.IContext): Boolean - { - return APlatform.platform == platform - } - - @Transient - override val codec: MapCodec = CODEC - @Transient - override val identifier: ResourceLocation = ID - - override fun toString(): String - { - return "platform(\"$platform\")" - } - - companion object - { - val CODEC: MapCodec = - RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> - builder - .group( - Codec.STRING.fieldOf("platform").forGetter(APlatformCondition::platform) - ) - .apply( - builder - ) { platform: String -> - APlatformCondition( - platform - ) - } - } - val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "platform") - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ARegistryCondition.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ARegistryCondition.kt deleted file mode 100644 index f8d1fb863..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ARegistryCondition.kt +++ /dev/null @@ -1,49 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.google.common.base.Joiner -import com.mojang.serialization.MapCodec -import com.mojang.serialization.codecs.RecordCodecBuilder -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.serialization.serializers.ResourceLocationSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.Transient -import net.minecraft.core.Registry -import net.minecraft.resources.ResourceKey -import net.minecraft.resources.ResourceLocation - -/** Condition that holds when every id in [entries] is registered in the registry keyed by [registry]. */ -data class ARegistryCondition(private val registry: @Serializable(with = ResourceLocationSerializer::class) ResourceLocation, private val entries: List<@Serializable(with = ResourceLocationSerializer::class) ResourceLocation>) : - IACondition -{ - constructor(registry: ResourceLocation, vararg entries: ResourceLocation) : this(registry, entries.toList()) - - override fun test(context: IACondition.IContext): Boolean - { - val registryRef: ResourceKey> = ResourceKey.createRegistryKey(registry) - val registry: Registry = context.getRegistry(registryRef) - return entries.all { registry.keySet().contains(it) } - } - - @Transient - override val codec: MapCodec = CODEC - @Transient - override val identifier: ResourceLocation = ID - - override fun toString(): String - { - return "registry(\"$registry\", ${Joiner.on(", ").join(entries.map { "\"$it\"" })})" - } - - companion object - { - val CODEC: MapCodec = - RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> - builder.group( - ResourceLocation.CODEC.optionalFieldOf("registry", ResourceLocation.parse("item")).forGetter(ARegistryCondition::registry), - ResourceLocation.CODEC.listOf().fieldOf("entries").forGetter(ARegistryCondition::entries) - ).apply(builder, ::ARegistryCondition) - } - val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "registry") - } - -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ATrueCondition.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ATrueCondition.kt deleted file mode 100644 index daecc9b88..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ATrueCondition.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.mojang.serialization.MapCodec -import net.kernelpanicsoft.archie.Archie -import net.minecraft.resources.ResourceLocation - -/** Condition that always holds. */ -data object ATrueCondition : - IACondition -{ - val CODEC: MapCodec = MapCodec.unit(ATrueCondition).stable() - val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "true") - override fun test(context: IACondition.IContext): Boolean - { - return true - } - - override val codec: MapCodec = CODEC - override val identifier: ResourceLocation = ID - - override fun toString(): String - { - return "true" - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AXorCondition.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AXorCondition.kt deleted file mode 100644 index daec85946..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AXorCondition.kt +++ /dev/null @@ -1,35 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.google.common.base.Joiner -import com.mojang.serialization.MapCodec -import com.mojang.serialization.codecs.RecordCodecBuilder -import net.kernelpanicsoft.archie.Archie -import net.minecraft.resources.ResourceLocation - -/** Condition that holds when an odd number of [children] hold (logical XOR, folded pairwise). */ -data class AXorCondition(override val children: List) : - AGroupCondition() -{ - constructor(vararg values: IACondition) : this(values.toList()) - - override fun reducer(a: Boolean, b: Boolean): Boolean = a xor b - - override val codec: MapCodec = CODEC - override val identifier: ResourceLocation = ID - - override fun toString(): String - { - return "(${Joiner.on(" ^^ ").join(children)})" - } - - companion object - { - val CODEC: MapCodec = - RecordCodecBuilder.mapCodec { builder: RecordCodecBuilder.Instance -> - builder.group( - IACondition.CODEC.listOf().fieldOf("children").forGetter(AXorCondition::children), - ).apply(builder, ::AXorCondition) - } - val ID: ResourceLocation = ResourceLocation.fromNamespaceAndPath(Archie.MOD_ID, "xor") - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt deleted file mode 100644 index b780e1b21..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt +++ /dev/null @@ -1,18 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import net.minecraft.data.recipes.RecipeOutput - -/** Attaches [condition] to the next recipe written to this [RecipeOutput]. */ -fun RecipeOutput.withCondition(condition: IACondition): RecipeOutput -{ - return withCondition { condition } -} - -/** Attaches the [IACondition] built by [block] (with [AConditionBuilder] in scope) to the next recipe written to this [RecipeOutput]. */ -fun RecipeOutput.withCondition(block: AConditionBuilder.() -> IACondition): RecipeOutput -{ - return AConditionsPlatform.withCondition(this, AConditionBuilder.block()) -} - -/** Builds an [IACondition] with [AConditionBuilder] in scope, e.g. `buildCondition { mod("architectury") }`. */ -inline fun buildCondition(block: AConditionBuilder.() -> IACondition): IACondition = AConditionBuilder.block() \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt deleted file mode 100644 index de779e370..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt +++ /dev/null @@ -1,84 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.mojang.serialization.Codec -import com.mojang.serialization.MapCodec -import net.minecraft.core.Holder -import net.minecraft.core.Registry -import net.minecraft.resources.ResourceKey -import net.minecraft.resources.ResourceLocation -import net.minecraft.tags.TagKey - -/** - * A cross-loader condition, evaluated at datapack load time, that decides whether the entry it's - * attached to (a recipe, tag entry, etc.) should be active. Mirrors NeoForge's/Fabric's native - * condition systems but is decoded through [CODEC] so the same condition classes work on both - * loaders. - * - * Built-in implementations live alongside this file (e.g. [AAndCondition], [AOrCondition], - * [ANotCondition], [ATrueCondition], [AModLoadedCondition], [ARegistryCondition]); see - * [ABuiltinConditions] for the full set and [AConditionBuilder] for a DSL to combine them. - * Register custom conditions with [register]. - */ -interface IACondition -{ - /** Evaluates this condition against [context], returning whether it holds. */ - fun test(context: IContext): Boolean - - /** The codec used to (de)serialize this condition to/from JSON. */ - val codec: MapCodec - - /** The condition type's registered id, matching the key it was [register]ed under. */ - val identifier: ResourceLocation - - /** Read-only view of loaded tags/registries available to [test] while a condition is evaluated. */ - interface IContext - { - /** - * Return the requested tag if available, or an empty tag otherwise. - */ - fun getTag(key: TagKey): Collection> - { - return getAllTags(key.registry()).getOrDefault(key.location(), setOf>()) - } - - /** - * Return all the loaded tags for the passed registry, or an empty map if none is available. - * Note that the map and the tags are unmodifiable. - */ - fun getAllTags(registry: ResourceKey>): Map>> - - /** - * Return the registry entry for the specified [ResourceKey] - */ - fun getRegistryEntry(key: ResourceKey) : T? - { - return getRegistry(ResourceKey.createRegistryKey(key.registry()))[key] - } - - /** - * Return the registry entry for the given [ResourceLocation] in the [Registry] specified by the registry key - */ - fun getRegistryEntry(registry: ResourceKey>, key: ResourceLocation): T? - { - return getRegistryEntry(ResourceKey.create(registry, key)) - } - - /** - * Return the [Registry] for the given registry key - */ - fun getRegistry(registry: ResourceKey>): Registry - } - - companion object - { - /** Registers condition type [T] under [identifier] so it can be decoded via [CODEC]. */ - inline fun register(identifier: ResourceLocation, codec: MapCodec) - { - AConditionsPlatform.register(identifier, codec) - } - - /** The dispatch codec that decodes any registered [IACondition] type by its [identifier]. */ - val CODEC: Codec - get() = AConditionsPlatform.codec() - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt deleted file mode 100644 index 3576c2927..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt +++ /dev/null @@ -1,109 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting - -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.data.IADataProvider -import net.kernelpanicsoft.archie.data.common.conditions.AConditionsPlatform -import net.kernelpanicsoft.archie.data.common.crafting.recipies.ArchieCookingRecipeBuilder -import net.kernelpanicsoft.archie.data.common.crafting.recipies.ArchieShapedRecipeBuilder -import net.kernelpanicsoft.archie.data.common.crafting.recipies.ArchieShapelessRecipeBuilder -import dev.architectury.platform.Mod -import net.minecraft.core.HolderLookup -import net.minecraft.data.CachedOutput -import net.minecraft.data.PackOutput -import net.minecraft.data.recipes.* -import net.minecraft.tags.TagKey -import net.minecraft.world.item.Item -import net.minecraft.world.item.crafting.BlastingRecipe -import net.minecraft.world.item.crafting.CampfireCookingRecipe -import net.minecraft.world.item.crafting.SmeltingRecipe -import net.minecraft.world.item.crafting.SmokingRecipe -import net.minecraft.world.level.ItemLike -import java.util.concurrent.CompletableFuture -import kotlin.system.exitProcess - -/** - * Datagen provider that builds recipe JSONs on top of vanilla's [RecipeProvider]. Implement - * [generate] and use the `shaped`/`shapeless`/`smelting`/`blasting`/`smoking`/`cooking` DSL - * helpers, or vanilla's [RecipeBuilder]s directly, to register recipes into the given - * [RecipeOutput]. Use via [net.kernelpanicsoft.archie.data.ADataGenerator.Common.recipes]. - */ -@Suppress("unused") -abstract class ARecipeProvider( - override val output: PackOutput, - override val mod: Mod, - registries: CompletableFuture, - override val exitOnError: Boolean -) : RecipeProvider(output, registries), - IADataProvider -{ - - /** On Fabric, a wrapping [RecipeProvider] needed for [AConditionsPlatform.withCondition] support; `null` elsewhere. */ - private val fabricParent: RecipeProvider? = AConditionsPlatform.fabricRecipeProvider(this, registries) - - override fun run(cachedOutput: CachedOutput): CompletableFuture<*> - { - return if (fabricParent != null) - fabricParent.run(cachedOutput) - else - super.run(cachedOutput) - } - - final override fun buildRecipes(recipeOutput: RecipeOutput) - { - runCatching { - generate(recipeOutput) - }.onFailure { - Archie.LOGGER.error( - "Data Provider $name failed with exception: ${it.message}\n" + - "Stacktrace: ${it.stackTraceToString()}" - ) - if (exitOnError) exitProcess(-1) - } - } - - /** Called once during [buildRecipes] to register recipes into [recipeOutput]. */ - abstract fun generate(recipeOutput: RecipeOutput) - - override fun getName(): String = format("Recipes") - - /** Builds a shaped crafting recipe declared in [block]. */ - fun shaped( - block: ArchieShapedRecipeBuilder.() -> Unit - ): ArchieShapedRecipeBuilder = ArchieShapedRecipeBuilder.shaped(block) - - /** Builds a shapeless crafting recipe declared in [block]. */ - fun shapeless( - block: ArchieShapelessRecipeBuilder.() -> Unit - ): ArchieShapelessRecipeBuilder = ArchieShapelessRecipeBuilder.shapeless(block) - - /** Builds a furnace smelting recipe declared in [block]. */ - fun smelting( - block: ArchieCookingRecipeBuilder.() -> Unit - ): ArchieCookingRecipeBuilder = ArchieCookingRecipeBuilder.smelting(block) - - /** Builds a blast furnace recipe declared in [block]. */ - fun blasting( - block: ArchieCookingRecipeBuilder.() -> Unit - ): ArchieCookingRecipeBuilder = ArchieCookingRecipeBuilder.blasting(block) - - /** Builds a smoker recipe declared in [block]. */ - fun smoking( - block: ArchieCookingRecipeBuilder.() -> Unit - ): ArchieCookingRecipeBuilder = ArchieCookingRecipeBuilder.smoking(block) - - /** Builds a campfire cooking recipe declared in [block]. */ - fun cooking( - block: ArchieCookingRecipeBuilder.() -> Unit - ): ArchieCookingRecipeBuilder = ArchieCookingRecipeBuilder.cooking(block) - - /** Adds an unlock criterion requiring [ingredient] to have been obtained, named after it. */ - @Suppress("UNCHECKED_CAST") - fun T.unlockedBy(ingredient: ItemLike): T = - unlockedBy(getHasName(ingredient), has(ingredient)) as T - - /** Adds an unlock criterion requiring any item in [tag] to have been obtained, named `has_`. */ - @Suppress("UNCHECKED_CAST") - fun T.unlockedBy(tag: TagKey): T = - unlockedBy("has_${tag.location.path}", has(tag)) as T - -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAllIngredient.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAllIngredient.kt deleted file mode 100644 index a6429423e..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAllIngredient.kt +++ /dev/null @@ -1,60 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -import com.mojang.serialization.Codec -import com.mojang.serialization.MapCodec -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.util.rem -import net.minecraft.world.item.ItemStack -import net.minecraft.world.item.crafting.Ingredient - -/** Custom ingredient that matches a stack only when every one of its sub-ingredients matches it. Build via [of]. */ -class AAllIngredient private constructor(ingredients: List) : - ACombinedIngredient(ingredients) -{ - override fun test(stack: ItemStack): Boolean - { - return ingredients.all { ingredient -> ingredient.test(stack) } - } - - override val matchingStacks: MutableList by lazy { - // There's always at least one sub ingredient, so accessing ingredients[0] is safe. - val previewStacks: MutableList = - mutableListOf(*ingredients[0].items) - - for (i in 1 until ingredients.size) - { - val ing: Ingredient = ingredients[i] - previewStacks.removeIf { stack: ItemStack -> - !ing.test( - stack - ) - } - } - - previewStacks - } - - override val serializer: IACustomIngredientSerializer<*> = Serializer - - companion object - { - /** Creates a vanilla [Ingredient] that matches only when every one of [ingredients] matches. */ - fun of(vararg ingredients: Ingredient): Ingredient = AAllIngredient(ingredients.toList()).vanilla - private val ALLOW_EMPTY_CODEC = createCodec(Ingredient.CODEC) - private val DISALLOW_EMPTY_CODEC = createCodec(Ingredient.CODEC_NONEMPTY) - - private fun createCodec(ingredientCodec: Codec): MapCodec - { - return ingredientCodec - .listOf() - .fieldOf("ingredients") - .xmap(::AAllIngredient, AAllIngredient::ingredients) - } - - val Serializer: IACustomIngredientSerializer = - Serializer( - Archie % "all", - ::AAllIngredient, ALLOW_EMPTY_CODEC, DISALLOW_EMPTY_CODEC - ) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAnyIngredient.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAnyIngredient.kt deleted file mode 100644 index 3ea3659e8..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAnyIngredient.kt +++ /dev/null @@ -1,51 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -import com.mojang.serialization.Codec -import com.mojang.serialization.MapCodec -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.util.rem -import net.minecraft.world.item.ItemStack -import net.minecraft.world.item.crafting.Ingredient -import java.util.* - -/** Custom ingredient that matches a stack when at least one of its sub-ingredients matches it. Build via [of]. */ -class AAnyIngredient private constructor(ingredients: List): ACombinedIngredient(ingredients) -{ - override fun test(stack: ItemStack): Boolean - { - return ingredients.any { ingredient -> ingredient.test(stack) } - } - - override val matchingStacks: MutableList by lazy { - val previewStacks: MutableList = ArrayList() - for (ingredient in ingredients) - { - previewStacks.addAll(listOf(*ingredient.items)) - } - - previewStacks - } - override val serializer: IACustomIngredientSerializer<*> = Serializer - - companion object - { - /** Creates a vanilla [Ingredient] that matches when at least one of [ingredients] matches. */ - fun of(vararg ingredients: Ingredient): Ingredient = AAnyIngredient(ingredients.toList()).vanilla - private val ALLOW_EMPTY_CODEC = createCodec(Ingredient.CODEC) - private val DISALLOW_EMPTY_CODEC = createCodec(Ingredient.CODEC_NONEMPTY) - - private fun createCodec(ingredientCodec: Codec): MapCodec - { - return ingredientCodec - .listOf() - .fieldOf("ingredients") - .xmap(::AAnyIngredient, AAnyIngredient::ingredients) - } - - val Serializer: IACustomIngredientSerializer = - Serializer( - Archie % "any", - ::AAnyIngredient, ALLOW_EMPTY_CODEC, DISALLOW_EMPTY_CODEC - ) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ABuiltinIngredients.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ABuiltinIngredients.kt deleted file mode 100644 index 2e7071633..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ABuiltinIngredients.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -/** Registers Archie's built-in [IACustomIngredient] serializers ([AAllIngredient], [AAnyIngredient], [AComponentsIngredient], [ACustomDataIngredient]). */ -object ABuiltinIngredients -{ - /** Registers every built-in ingredient serializer; called once during [net.kernelpanicsoft.archie.Archie.init]. */ - fun init() - { - IACustomIngredientSerializer.register(AAllIngredient.Serializer) - IACustomIngredientSerializer.register(AAnyIngredient.Serializer) - - IACustomIngredientSerializer.register(AComponentsIngredient.Serializer) - IACustomIngredientSerializer.register(ACustomDataIngredient.Serializer) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACombinedIngredient.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACombinedIngredient.kt deleted file mode 100644 index 2f5b10132..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACombinedIngredient.kt +++ /dev/null @@ -1,64 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -import com.mojang.serialization.MapCodec -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.ByteBufCodecs -import net.minecraft.network.codec.StreamCodec -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.item.crafting.Ingredient -import java.util.function.Function - - -/** - * Base class for [IACustomIngredient]s that combine multiple sub-[ingredients], e.g. [AAllIngredient] - * (matches when every sub-ingredient matches) and [AAnyIngredient] (matches when any does). - */ -abstract class ACombinedIngredient protected constructor(ingredients: List) : - IACustomIngredient -{ - /** The sub-ingredients being combined; always non-empty. */ - val ingredients: List - - init - { - require(ingredients.isNotEmpty()) { "Combined ingredient must have at least one sub-ingredient" } - - this.ingredients = ingredients - } - - /** `true` if any sub-ingredient is a custom ingredient that itself requires testing. */ - override val requiresTesting: Boolean - get() - { - for (ingredient in ingredients) - { - if (ingredient is IACustomIngredientHolder<*> && ingredient.custom.requiresTesting) - { - return true - } - } - - return false - } - - /** Generic [IACustomIngredientSerializer] for [ACombinedIngredient] subtypes, built from a [factory] and empty/non-empty codecs. */ - class Serializer( - override val identifier: ResourceLocation, - private val factory: Function, I>, - private val allowEmptyCodec: MapCodec, - private val disallowEmptyCodec: MapCodec - ) : - IACustomIngredientSerializer - { - override fun getCodec(allowEmpty: Boolean): MapCodec - { - return if (allowEmpty) allowEmptyCodec else disallowEmptyCodec - } - - override val packetCodec: StreamCodec = run { - Ingredient.CONTENTS_STREAM_CODEC.apply(ByteBufCodecs.list()) - .map(factory, ACombinedIngredient::ingredients) - } - } - -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AComponentsIngredient.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AComponentsIngredient.kt deleted file mode 100644 index 455d74225..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AComponentsIngredient.kt +++ /dev/null @@ -1,120 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -import com.mojang.serialization.Codec -import com.mojang.serialization.MapCodec -import com.mojang.serialization.codecs.RecordCodecBuilder -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.serialization.buildComponentPatch -import net.kernelpanicsoft.archie.util.rem -import net.minecraft.core.component.DataComponentPatch -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.item.ItemStack -import net.minecraft.world.item.crafting.Ingredient -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -/** - * Custom ingredient that matches stacks accepted by [base] and additionally requires their data - * components to match [components] (present components must equal the patch's value; absent - * components must stay absent). Build via [of]. - */ -class AComponentsIngredient private constructor(val base: Ingredient, components: DataComponentPatch) : IACustomIngredient -{ - /** Non-empty patch of component values a matching stack must satisfy. */ - val components: DataComponentPatch - - init - { - require(!components.isEmpty) { "ComponentIngredient must have at least one defined component" } - this.components = components - } - - override fun test(stack: ItemStack): Boolean - { - if (!base.test(stack)) return false - - for ((type, value) in components.entrySet()) - { - if (value.isPresent) - { - if (!stack.has(type)) return false - - if (value.get() != stack.get(type)) return false - } else - { - if (stack.has(type)) return false - } - } - - return true - } - - override val matchingStacks: MutableList by lazy { - val stacks: MutableList = base.items.toMutableList() - stacks.replaceAll { stack -> - val copy = stack.copy() - copy.applyComponentsAndValidate(components) - copy - } - stacks - } - - override val requiresTesting: Boolean = true - override val serializer: IACustomIngredientSerializer<*> = Serializer - - object Serializer : IACustomIngredientSerializer - { - private val ID = Archie % "components" - - private val ALLOW_EMPTY_CODEC: MapCodec = createCodec( - Ingredient.CODEC - ) - private val DISALLOW_EMPTY_CODEC: MapCodec = createCodec( - Ingredient.CODEC_NONEMPTY - ) - - private val PACKET_CODEC: StreamCodec = StreamCodec.composite( - Ingredient.CONTENTS_STREAM_CODEC, - AComponentsIngredient::base, - DataComponentPatch.STREAM_CODEC, - AComponentsIngredient::components, - ::AComponentsIngredient - ) - - private fun createCodec(ingredientCodec: Codec): MapCodec - { - return RecordCodecBuilder.mapCodec { instance: RecordCodecBuilder.Instance -> - instance.group( - ingredientCodec.fieldOf("base").forGetter(AComponentsIngredient::base), - DataComponentPatch.CODEC.fieldOf("components").forGetter(AComponentsIngredient::components) - ).apply( - instance, ::AComponentsIngredient - ) - } - } - - override val identifier: ResourceLocation = ID - - override fun getCodec(allowEmpty: Boolean): MapCodec = - if (allowEmpty) ALLOW_EMPTY_CODEC else DISALLOW_EMPTY_CODEC - - override val packetCodec: StreamCodec = PACKET_CODEC - } - - companion object - { - /** Creates a vanilla [Ingredient] matching [base] stacks whose components satisfy [components]. */ - fun of(base: Ingredient, components: DataComponentPatch): Ingredient = AComponentsIngredient(base, components).vanilla - - /** [of] overload that builds the component patch with [builderAction]. */ - @OptIn(ExperimentalContracts::class) - fun of(base: Ingredient, builderAction: DataComponentPatch.Builder.() -> Unit): Ingredient - { - contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } - return of(base, buildComponentPatch(builderAction)) - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomDataIngredient.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomDataIngredient.kt deleted file mode 100644 index dcfb843fd..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomDataIngredient.kt +++ /dev/null @@ -1,122 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -import com.mojang.serialization.Codec -import com.mojang.serialization.MapCodec -import com.mojang.serialization.codecs.RecordCodecBuilder -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.serialization.buildCompoundTag -import net.benwoodworth.knbt.NbtCompoundBuilder -import net.kernelpanicsoft.archie.util.rem -import net.minecraft.core.component.DataComponents -import net.minecraft.nbt.CompoundTag -import net.minecraft.nbt.TagParser -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.ByteBufCodecs -import net.minecraft.network.codec.StreamCodec -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.item.ItemStack -import net.minecraft.world.item.component.CustomData -import net.minecraft.world.item.crafting.Ingredient -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -/** - * Custom ingredient that matches stacks accepted by [base] whose `minecraft:custom_data` - * component NBT is matched by [nbt] (a partial/sub-tag match, not exact equality). Build via [of]. - */ -class ACustomDataIngredient private constructor( - val base: Ingredient, - - nbt: CompoundTag -) : IACustomIngredient -{ - /** Non-empty NBT that a matching stack's custom data must be matched by. */ - val nbt: CompoundTag - - init - { - require(!nbt.isEmpty) { "NBT cannot be null or empty; use components ingredient for strict matching" } - this.nbt = nbt - } - - override fun test(stack: ItemStack): Boolean - { - if (!base.test(stack)) return false - - val nbt: CustomData? = stack[DataComponents.CUSTOM_DATA] - - return nbt?.matchedBy(this.nbt) ?: false - } - - override val matchingStacks: MutableList by lazy { - val stacks: MutableList = base.items.toMutableList() - stacks.replaceAll { stack -> - val copy: ItemStack = stack.copy() - copy.update(DataComponents.CUSTOM_DATA, CustomData.EMPTY) { - CustomData.of(it.copyTag().merge(this.nbt)) - } - copy - } - - stacks - } - - override val requiresTesting: Boolean = true - override val serializer: IACustomIngredientSerializer<*> = Serializer - - - object Serializer : IACustomIngredientSerializer - { - private val ID = Archie % "custom_data" - - private val ALLOW_EMPTY_CODEC: MapCodec = createCodec( - Ingredient.CODEC - ) - private val DISALLOW_EMPTY_CODEC: MapCodec = createCodec( - Ingredient.CODEC_NONEMPTY - ) - - private val PACKET_CODEC: StreamCodec = StreamCodec.composite( - Ingredient.CONTENTS_STREAM_CODEC, - ACustomDataIngredient::base, - ByteBufCodecs.COMPOUND_TAG, - ACustomDataIngredient::nbt, - ::ACustomDataIngredient - ) - - private fun createCodec(ingredientCodec: Codec): MapCodec - { - return RecordCodecBuilder.mapCodec { instance: RecordCodecBuilder.Instance -> - instance.group( - ingredientCodec.fieldOf("base").forGetter(ACustomDataIngredient::base), - TagParser.LENIENT_CODEC.fieldOf("nbt").forGetter(ACustomDataIngredient::nbt) - ).apply( - instance, ::ACustomDataIngredient - ) - } - } - - override val identifier: ResourceLocation = ID - - override fun getCodec(allowEmpty: Boolean): MapCodec = - if (allowEmpty) ALLOW_EMPTY_CODEC else DISALLOW_EMPTY_CODEC - - override val packetCodec: StreamCodec = PACKET_CODEC - } - - companion object - { - /** Creates a vanilla [Ingredient] matching [base] stacks whose custom data is matched by [nbt]. */ - fun of(base: Ingredient, nbt: CompoundTag): Ingredient = ACustomDataIngredient(base, nbt).vanilla - - /** [of] overload that builds the NBT with [builderAction]. */ - @OptIn(ExperimentalContracts::class) - fun of(base: Ingredient, builderAction: NbtCompoundBuilder.() -> Unit) - { - contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } - of(base, buildCompoundTag(builderAction)) - } - } - -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.common.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.common.kt deleted file mode 100644 index 53fe000e7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.common.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -import net.minecraft.world.item.crafting.Ingredient - -/** Cross-loader hook converting an [IACustomIngredient] into a vanilla [Ingredient]; backs [IACustomIngredient.vanilla]. */ -internal expect object ACustomIngredientPlatform -{ - fun vanillaOf(custom: IACustomIngredient): Ingredient -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.common.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.common.kt deleted file mode 100644 index 92a36a728..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.common.kt +++ /dev/null @@ -1,8 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - - -/** Cross-loader hook registering an [IACustomIngredientSerializer]; backs [IACustomIngredientSerializer.register]. */ -internal expect object ACustomIngredientSerializerPlatform -{ - fun register(serializer: IACustomIngredientSerializer) -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredient.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredient.kt deleted file mode 100644 index 8eb851491..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredient.kt +++ /dev/null @@ -1,59 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -import net.minecraft.world.item.ItemStack -import net.minecraft.world.item.crafting.Ingredient -import org.jetbrains.annotations.ApiStatus - -/** - * Interface that modders can implement to create new recipe-matching behaviors beyond vanilla - * [Ingredient]s, ported from Fabric's custom ingredient API to work cross-loader. - * - * This is not directly implemented on vanilla [Ingredient]s; use [vanilla] to convert a custom - * ingredient into one. On disk, a custom ingredient is encoded by its [serializer], keyed by - * that serializer's registered identifier, plus whatever extra fields the serializer needs. - * - * @see IACustomIngredientSerializer - */ -interface IACustomIngredient -{ - /** - * Checks whether [stack] matches this ingredient. Must not modify [stack]. - */ - fun test(stack: ItemStack): Boolean - - /** - * The stacks that match this ingredient, for display purposes (e.g. in a recipe viewer). - * - * Guidelines for good compatibility: - * - These stacks need not be exhaustive or perfectly accurate, except when [requiresTesting] - * is `false`, in which case they must correspond exactly to every accepted item. - * - At least one stack must be returned, or the ingredient is considered - * [empty][Ingredient.isEmpty]. - * - Try to include at least one stack per accepted item, so inspecting mods can enumerate - * what the ingredient might accept. - * - * No caching is required here; the ingredient itself already caches this. - */ - val matchingStacks: MutableList - - /** - * Whether [test] must always be called to know if a stack matches, as opposed to relying on - * [matchingStacks] alone. `false` when this ingredient ignores extra stack data (like - * components/NBT) and matching is fully determined by item type. - */ - val requiresTesting: Boolean - - /** - * The serializer for this ingredient. Must have been registered via - * [IACustomIngredientSerializer.register]. - */ - val serializer: IACustomIngredientSerializer<*> - - /** Converts this custom ingredient into a vanilla [Ingredient] behaving the same way. */ - @get:ApiStatus.NonExtendable - val vanilla: Ingredient - get() - { - return ACustomIngredientPlatform.vanillaOf(this) - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientHolder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientHolder.kt deleted file mode 100644 index 8a36a7eb3..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientHolder.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -/** - * Implemented by the vanilla `Ingredient` produced from [IACustomIngredient.vanilla], exposing - * the wrapped [custom] ingredient so it can be recovered from a vanilla `Ingredient` reference. - */ -interface IACustomIngredientHolder -{ - /** The custom ingredient this vanilla `Ingredient` was converted from. */ - val custom: T -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientSerializer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientSerializer.kt deleted file mode 100644 index 6f41b2283..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientSerializer.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -import com.mojang.serialization.MapCodec -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.item.crafting.Ingredient - - -/** - * Serializer for an [IACustomIngredient] of type [T]. - * - * All instances must be registered using [register] for deserialization to work. - */ -interface IACustomIngredientSerializer -{ - /** The id this serializer is registered under; used to identify it in recipe JSON. */ - val identifier: ResourceLocation - - /** - * The codec used to read the ingredient from recipe JSON files. - * - * @param allowEmpty Whether an ingredient matching no items should be accepted, mirroring - * [Ingredient.CODEC] vs `Ingredient.CODEC_NONEMPTY`. - */ - fun getCodec(allowEmpty: Boolean): MapCodec - - /** The codec used to sync the ingredient to the client over the network. */ - val packetCodec: StreamCodec - - companion object - { - /** - * Registers [serializer] under its [identifier][IACustomIngredientSerializer.identifier]. - * - * @throws IllegalArgumentException if a serializer is already registered under that identifier - */ - fun register(serializer: IACustomIngredientSerializer) - { - return ACustomIngredientSerializerPlatform.register(serializer) - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieCookingRecipeBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieCookingRecipeBuilder.kt deleted file mode 100644 index 98bda5872..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieCookingRecipeBuilder.kt +++ /dev/null @@ -1,109 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.recipies - -import net.minecraft.advancements.Criterion -import net.minecraft.data.recipes.RecipeCategory -import net.minecraft.data.recipes.RecipeOutput -import net.minecraft.data.recipes.SimpleCookingRecipeBuilder -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.item.Item -import net.minecraft.world.item.crafting.AbstractCookingRecipe -import net.minecraft.world.item.crafting.BlastingRecipe -import net.minecraft.world.item.crafting.CampfireCookingRecipe -import net.minecraft.world.item.crafting.Ingredient -import net.minecraft.world.item.crafting.RecipeSerializer -import net.minecraft.world.item.crafting.SmeltingRecipe -import net.minecraft.world.item.crafting.SmokingRecipe -import net.minecraft.world.level.ItemLike -import kotlin.properties.Delegates - -/** - * DSL builder for a [T] cooking recipe (smelting/blasting/smoking/campfire), wrapping vanilla's - * [SimpleCookingRecipeBuilder]. Set [category], [result], [ingredient], [experience], and - * [cookingTime], then save with [IARecipeBuilder.save]/[net.minecraft.data.recipes.RecipeBuilder.save]. - * Build via the type-specific [smelting]/[blasting]/[smoking]/[cooking] factories, or the - * corresponding methods on [net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider]. - */ -class ArchieCookingRecipeBuilder( - private val factory: AbstractCookingRecipe.Factory, - private val serializer: RecipeSerializer -) : IARecipeBuilder -{ - private val builder: SimpleCookingRecipeBuilder by lazy { - SimpleCookingRecipeBuilder.generic( - ingredient, - category, - result, - experience, - cookingTime, - serializer, - factory - ) - } - private val criteria: MutableMap> = mutableMapOf() - - lateinit var category: RecipeCategory - lateinit var result: ItemLike - lateinit var ingredient: Ingredient - var experience by Delegates.notNull() - var cookingTime by Delegates.notNull() - - var group: String? = null - - private fun checkVars() - { - check(::category.isInitialized) { "You must specify a recipe category dumbass!" } - check(::result.isInitialized) { "You must specify a recipe result dumbass!" } - check(::ingredient.isInitialized) { "You must specify a recipe ingredient dumbass!" } - check(runCatching { experience }.isSuccess) { "You must specify an experience amount dumbass!" } - check(runCatching { cookingTime }.isSuccess) { "You must specify a cooking time dumbass!" } - } - - override fun unlockedBy(name: String, criterion: Criterion<*>): ArchieCookingRecipeBuilder - { - criteria[name] = criterion - return this - } - - override fun group(groupName: String?): ArchieCookingRecipeBuilder - { - group = groupName - return this - } - - override fun getResult(): Item - { - checkVars() - return builder.result - } - - override fun save(recipeOutput: RecipeOutput, id: ResourceLocation) - { - checkVars() - criteria.forEach { (k, v) -> - builder.unlockedBy(k, v) - } - builder.group(group) - - builder.save(recipeOutput, id) - } - - companion object - { - /** Builds a furnace smelting recipe. */ - fun smelting(block: ArchieCookingRecipeBuilder.() -> Unit): ArchieCookingRecipeBuilder = - ArchieCookingRecipeBuilder(::SmeltingRecipe, RecipeSerializer.SMELTING_RECIPE).apply(block) - - /** Builds a blast furnace recipe. */ - fun blasting(block: ArchieCookingRecipeBuilder.() -> Unit): ArchieCookingRecipeBuilder = - ArchieCookingRecipeBuilder(::BlastingRecipe, RecipeSerializer.BLASTING_RECIPE).apply(block) - - /** Builds a smoker recipe. */ - fun smoking(block: ArchieCookingRecipeBuilder.() -> Unit): ArchieCookingRecipeBuilder = - ArchieCookingRecipeBuilder(::SmokingRecipe, RecipeSerializer.SMOKING_RECIPE).apply(block) - - /** Builds a campfire cooking recipe. */ - fun cooking(block: ArchieCookingRecipeBuilder.() -> Unit): ArchieCookingRecipeBuilder = - ArchieCookingRecipeBuilder(::CampfireCookingRecipe, RecipeSerializer.CAMPFIRE_COOKING_RECIPE).apply(block) - } - -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapedRecipeBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapedRecipeBuilder.kt deleted file mode 100644 index f484ec6d3..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapedRecipeBuilder.kt +++ /dev/null @@ -1,133 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.recipies - -import net.minecraft.advancements.Criterion -import net.minecraft.data.recipes.RecipeCategory -import net.minecraft.data.recipes.RecipeOutput -import net.minecraft.data.recipes.ShapedRecipeBuilder -import net.minecraft.resources.ResourceLocation -import net.minecraft.tags.TagKey -import net.minecraft.world.item.Item -import net.minecraft.world.item.crafting.Ingredient -import net.minecraft.world.level.ItemLike - -/** - * DSL builder for a shaped crafting recipe, wrapping vanilla's [ShapedRecipeBuilder]. Set - * [category], [result], and (optionally) [count], declare the grid with [pattern] and [key], and - * save with [IARecipeBuilder.save]/[net.minecraft.data.recipes.RecipeBuilder.save]. Build via - * [net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider.shaped]. - */ -class ArchieShapedRecipeBuilder : IARecipeBuilder -{ - - - private val builder: ShapedRecipeBuilder by lazy { ShapedRecipeBuilder(category, result, count) } - - private val rows: MutableList = mutableListOf() - private val key: MutableMap = mutableMapOf() - private val criteria: MutableMap> = mutableMapOf() - - lateinit var category: RecipeCategory - lateinit var result: ItemLike - var count = 1 - - var group: String? = null - var showNotification = true - - /** Declares the recipe's shape via [Pattern.unaryPlus] on each row string, e.g. `+"XXX"`. */ - fun pattern(block: Pattern.() -> Unit) - { - Pattern().apply(block) - } - - /** Declares the recipe's shape as a sequence of row strings, top to bottom. */ - fun pattern( - vararg lines: String, - ) - { - rows.addAll(lines) - } - - /** DSL scope for declaring pattern rows one at a time; see [pattern]. */ - inner class Pattern - { - /** Adds this string as the next pattern row. */ - operator fun String.unaryPlus() - { - rows.add(this) - } - } - - /** Declares which [Ingredient] each pattern symbol maps to via [Key.to]. */ - fun key(block: Key.() -> Unit) - { - Key().apply(block) - } - - /** DSL scope for mapping pattern symbols to ingredients; see [key]. */ - inner class Key - { - infix fun Char.to(tag: TagKey) - { - this to Ingredient.of(tag) - } - - infix fun Char.to(item: ItemLike) - { - this to Ingredient.of(item) - } - - infix fun Char.to(ingredient: Ingredient) - { - require(!key.containsKey(this)) { "Symbol '$this' is already defined!" } - require(this != ' ') { "Symbol ' ' (whitespace) is reserved and cannot be defined" } - key[this] = ingredient - } - } - - - override fun unlockedBy(name: String, criterion: Criterion<*>): ArchieShapedRecipeBuilder - { - criteria[name] = criterion - return this - } - - override fun group(groupName: String?): ArchieShapedRecipeBuilder - { - group = groupName - return this - } - - override fun getResult(): Item - { - check(::category.isInitialized) { "You must specify a recipe category dumbass!" } - check(::result.isInitialized) { "You must specify a recipe result dumbass!" } - return builder.result - } - - override fun save(recipeOutput: RecipeOutput, id: ResourceLocation) - { - check(::category.isInitialized) { "You must specify a recipe category dumbass!" } - check(::result.isInitialized) { "You must specify a recipe result dumbass!" } - rows.forEach { - builder.pattern(it) - } - key.forEach { (k, v) -> - builder.define(k, v) - } - criteria.forEach { (k, v) -> - builder.unlockedBy(k, v) - } - builder.group(group) - builder.showNotification(showNotification) - - builder.save(recipeOutput, id) - } - - companion object - { - fun shaped(block: ArchieShapedRecipeBuilder.() -> Unit): ArchieShapedRecipeBuilder - { - return ArchieShapedRecipeBuilder().apply(block) - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapelessRecipeBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapelessRecipeBuilder.kt deleted file mode 100644 index 99cad6deb..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapelessRecipeBuilder.kt +++ /dev/null @@ -1,111 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.recipies - -import net.minecraft.advancements.Criterion -import net.minecraft.core.NonNullList -import net.minecraft.data.recipes.RecipeCategory -import net.minecraft.data.recipes.RecipeOutput -import net.minecraft.data.recipes.ShapelessRecipeBuilder -import net.minecraft.resources.ResourceLocation -import net.minecraft.tags.TagKey -import net.minecraft.world.item.Item -import net.minecraft.world.item.crafting.Ingredient -import net.minecraft.world.level.ItemLike - -/** - * DSL builder for a shapeless crafting recipe, wrapping vanilla's [ShapelessRecipeBuilder]. Set - * [category], [result], and (optionally) [count], declare inputs with [ingredients], and save - * with [IARecipeBuilder.save]/[net.minecraft.data.recipes.RecipeBuilder.save]. Build via - * [net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider.shapeless]. - */ -class ArchieShapelessRecipeBuilder : IARecipeBuilder -{ - private val builder: ShapelessRecipeBuilder by lazy { ShapelessRecipeBuilder(category, result, count) } - - private val ingredients: NonNullList = NonNullList.create() - private val criteria: MutableMap> = mutableMapOf() - - lateinit var category: RecipeCategory - lateinit var result: ItemLike - var count = 1 - - var group: String? = null - - /** Declares input ingredients via [Ingredients.of], e.g. `2 of Items.STICK`. */ - fun ingredients(block: Ingredients.() -> Unit) - { - Ingredients().apply(block) - } - - /** DSL scope for adding input ingredients by quantity; see [ingredients]. */ - inner class Ingredients - { - infix fun Int.of(tag: TagKey) - { - check(this >= 1) { "Cannot add less than 1 of ingredient" } - repeat(this) - { - ingredients.add(Ingredient.of(tag)) - } - } - - infix fun Int.of(item: ItemLike) - { - check(this >= 1) { "Cannot add less than 1 of ingredient" } - repeat(this) - { - ingredients.add(Ingredient.of(item)) - } - } - - infix fun Int.of(ingredient: Ingredient) - { - check(this >= 1) { "Cannot add less than 1 of ingredient" } - repeat(this) - { - ingredients.add(ingredient) - } - } - } - - override fun unlockedBy(name: String, criterion: Criterion<*>): ArchieShapelessRecipeBuilder - { - criteria[name] = criterion - return this - } - - override fun group(groupName: String?): ArchieShapelessRecipeBuilder - { - group = groupName - return this - } - - override fun getResult(): Item - { - check(::category.isInitialized) { "You must specify a recipe category dumbass!" } - check(::result.isInitialized) { "You must specify a recipe result dumbass!" } - return builder.result - } - - override fun save(recipeOutput: RecipeOutput, id: ResourceLocation) - { - check(::category.isInitialized) { "You must specify a recipe category dumbass!" } - check(::result.isInitialized) { "You must specify a recipe result dumbass!" } - ingredients.forEach { - builder.requires(it) - } - criteria.forEach { (k, v) -> - builder.unlockedBy(k, v) - } - builder.group(group) - - builder.save(recipeOutput, id) - } - - companion object - { - fun shapeless(block: ArchieShapelessRecipeBuilder.() -> Unit): ArchieShapelessRecipeBuilder - { - return ArchieShapelessRecipeBuilder().apply(block) - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt deleted file mode 100644 index f9f4a4040..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt +++ /dev/null @@ -1,21 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.recipies - -import net.kernelpanicsoft.archie.data.common.conditions.AConditionBuilder -import net.kernelpanicsoft.archie.data.common.conditions.IACondition -import net.kernelpanicsoft.archie.data.common.conditions.withCondition -import net.minecraft.data.recipes.RecipeBuilder -import net.minecraft.data.recipes.RecipeOutput -import net.minecraft.resources.ResourceLocation - -/** [RecipeBuilder] extension adding a condition-aware [save] overload. */ -interface IARecipeBuilder : RecipeBuilder -{ - /** Saves this recipe to [recipeOutput] under [id] (or the default id if `null`), gated by the [IACondition] built from [condition]. */ - fun save(recipeOutput: RecipeOutput, id: ResourceLocation? = null, condition: AConditionBuilder.() -> IACondition) - { - if (id != null) - save(recipeOutput.withCondition(condition), id) - else - save(recipeOutput.withCondition(condition)) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt deleted file mode 100644 index af825addb..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt +++ /dev/null @@ -1,1090 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.tags - -import net.minecraft.core.Registry -import net.minecraft.core.registries.Registries -import net.minecraft.resources.ResourceKey -import net.minecraft.resources.ResourceLocation -import net.minecraft.tags.ItemTags -import net.minecraft.tags.TagKey -import net.minecraft.world.entity.EntityType -import net.minecraft.world.item.DyeColor -import net.minecraft.world.item.Item -import net.minecraft.world.level.biome.Biome -import net.minecraft.world.level.block.Block -import net.minecraft.world.level.block.state.BlockBehaviour -import net.minecraft.world.level.material.Fluid - -/** - * Constants for the `c` (common) convention tags shared across the modding ecosystem, grouped by - * registry ([Blocks], [Items], [Fluids], [EntityTypes], [Biomes]). Each constant is a [TagKey] - * that can be used directly when building recipes/tags; entries not documented inline are - * self-explanatory from their name. - */ -@Suppress("unused") -object ACommonTags -{ - /** Registers every group's tags (currently a no-op per group; tag keys are created eagerly as constants). */ - fun init() - { - Blocks.init() - EntityTypes.init() - Items.init() - Fluids.init() - Biomes.init() - } - - /** Base for a group of [TagKey] constants in a single [registry], tracked in [tags] for lookup by id. */ - abstract class Tags private constructor( - private val registry: ResourceKey>, - private val tags: MutableMap> - ) : Map> by tags - { - constructor(registry: ResourceKey>) : this(registry, mutableMapOf()) - - /** Creates (and tracks) a `c:` tag key. */ - protected fun tag(name: String): TagKey = tag("c", name) - - /** Creates (and tracks) a `:` tag key. */ - protected fun tag(namespace: String, name: String): TagKey - { - return TagKey.create(registry, ResourceLocation.fromNamespaceAndPath(namespace, name)).also { tags[it.location] = it } - } - - /** Tracks a pre-existing [tag] key (e.g. a vanilla tag) alongside this group's own tags. */ - protected fun existing(tag: TagKey): TagKey - { - return tag.also { tags[it.location] = it } - } - - } - - object Blocks : Tags(Registries.BLOCK) - { - internal fun init() - { - } - - val ENDERMAN_PLACE_ON_BLACKLIST: TagKey = tag("neoforge", "enderman_place_on_blacklist") - val NEEDS_WOOD_TOOL: TagKey = tag("neoforge", "needs_wood_tool") - val NEEDS_GOLD_TOOL: TagKey = tag("neoforge", "needs_gold_tool") - val NEEDS_NETHERITE_TOOL: TagKey = tag("neoforge", "needs_netherite_tool") - - // `c` tags for common conventions - val BARRELS: TagKey = tag("barrels") - val BARRELS_WOODEN: TagKey = tag("barrels/wooden") - val BOOKSHELVES: TagKey = tag("bookshelves") - - /** - * For blocks that are similar to amethyst where their budding block produces buds and cluster blocks - */ - val BUDDING_BLOCKS: TagKey = tag("budding_blocks") - - /** - * For blocks that are similar to amethyst where they have buddings forming from budding blocks - */ - val BUDS: TagKey = tag("buds") - val CHAINS: TagKey = tag("chains") - val CHESTS: TagKey = tag("chests") - val CHESTS_ENDER: TagKey = tag("chests/ender") - val CHESTS_TRAPPED: TagKey = tag("chests/trapped") - val CHESTS_WOODEN: TagKey = tag("chests/wooden") - - /** - * For blocks that are similar to amethyst where they have clusters forming from budding blocks - */ - val CLUSTERS: TagKey = tag("clusters") - val COBBLESTONES: TagKey = tag("cobblestones") - val COBBLESTONES_NORMAL: TagKey = tag("cobblestones/normal") - val COBBLESTONES_INFESTED: TagKey = tag("cobblestones/infested") - val COBBLESTONES_MOSSY: TagKey = tag("cobblestones/mossy") - val COBBLESTONES_DEEPSLATE: TagKey = tag("cobblestones/deepslate") - - /** - * Tag that holds all blocks that can be dyed a specific color. - * (Does not include color blending blocks that would behave similar to leather armor item) - */ - val DYED: TagKey = tag("dyed") - val DYED_BLACK: TagKey = tag("dyed/black") - val DYED_BLUE: TagKey = tag("dyed/blue") - val DYED_BROWN: TagKey = tag("dyed/brown") - val DYED_CYAN: TagKey = tag("dyed/cyan") - val DYED_GRAY: TagKey = tag("dyed/gray") - val DYED_GREEN: TagKey = tag("dyed/green") - val DYED_LIGHT_BLUE: TagKey = tag("dyed/light_blue") - val DYED_LIGHT_GRAY: TagKey = tag("dyed/light_gray") - val DYED_LIME: TagKey = tag("dyed/lime") - val DYED_MAGENTA: TagKey = tag("dyed/magenta") - val DYED_ORANGE: TagKey = tag("dyed/orange") - val DYED_PINK: TagKey = tag("dyed/pink") - val DYED_PURPLE: TagKey = tag("dyed/purple") - val DYED_RED: TagKey = tag("dyed/red") - val DYED_WHITE: TagKey = tag("dyed/white") - val DYED_YELLOW: TagKey = tag("dyed/yellow") - val END_STONES: TagKey = tag("end_stones") - val FENCE_GATES: TagKey = tag("fence_gates") - val FENCE_GATES_WOODEN: TagKey = tag("fence_gates/wooden") - val FENCES: TagKey = tag("fences") - val FENCES_NETHER_BRICK: TagKey = tag("fences/nether_brick") - val FENCES_WOODEN: TagKey = tag("fences/wooden") - - val GLASS_BLOCKS: TagKey = tag("glass_blocks") - val GLASS_BLOCKS_COLORLESS: TagKey = tag("glass_blocks/colorless") - - /** - * Glass which is made from cheap resources like sand and only minor additional ingredients like dyes - */ - val GLASS_BLOCKS_CHEAP: TagKey = tag("glass_blocks/cheap") - val GLASS_BLOCKS_STAINED: TagKey = tag("glass_blocks/stained") - val GLASS_BLOCKS_TINTED: TagKey = tag("glass_blocks/tinted") - - val GLASS_PANES: TagKey = tag("glass_panes") - val GLASS_PANES_COLORLESS: TagKey = tag("glass_panes/colorless") - val GLASS_PANES_STAINED: TagKey = tag("glass_panes/stained") - - val GRAVELS: TagKey = tag("gravels") - - /** - * Tag that holds all blocks that recipe viewers should not show to users. - * Recipe viewers may use this to automatically find the corresponding BlockItem to hide. - */ - val HIDDEN_FROM_RECIPE_VIEWERS: TagKey = tag("hidden_from_recipe_viewers") - val NETHERRACKS: TagKey = tag("netherrack") - val OBSIDIANS: TagKey = tag("obsidians") - - /** - * Blocks which are often replaced by deepslate ores, i.e. the ores in the tag [.ORES_IN_GROUND_DEEPSLATE], during world generation - */ - val ORE_BEARING_GROUND_DEEPSLATE: TagKey = tag("ore_bearing_ground/deepslate") - - /** - * Blocks which are often replaced by netherrack ores, i.e. the ores in the tag [.ORES_IN_GROUND_NETHERRACK], during world generation - */ - val ORE_BEARING_GROUND_NETHERRACK: TagKey = tag("ore_bearing_ground/netherrack") - - /** - * Blocks which are often replaced by stone ores, i.e. the ores in the tag [.ORES_IN_GROUND_STONE], during world generation - */ - val ORE_BEARING_GROUND_STONE: TagKey = tag("ore_bearing_ground/stone") - - /** - * Ores which on average result in more than one resource worth of materials - */ - val ORE_RATES_DENSE: TagKey = tag("ore_rates/dense") - - /** - * Ores which on average result in one resource worth of materials - */ - val ORE_RATES_SINGULAR: TagKey = tag("ore_rates/singular") - - /** - * Ores which on average result in less than one resource worth of materials - */ - val ORE_RATES_SPARSE: TagKey = tag("ore_rates/sparse") - val ORES: TagKey = tag("ores") - val ORES_COAL: TagKey = tag("ores/coal") - val ORES_COPPER: TagKey = tag("ores/copper") - val ORES_DIAMOND: TagKey = tag("ores/diamond") - val ORES_EMERALD: TagKey = tag("ores/emerald") - val ORES_GOLD: TagKey = tag("ores/gold") - val ORES_IRON: TagKey = tag("ores/iron") - val ORES_LAPIS: TagKey = tag("ores/lapis") - val ORES_NETHERITE_SCRAP: TagKey = tag("ores/netherite_scrap") - val ORES_QUARTZ: TagKey = tag("ores/quartz") - val ORES_REDSTONE: TagKey = tag("ores/redstone") - - /** - * Ores in deepslate (or in equivalent blocks in the tag [.ORE_BEARING_GROUND_DEEPSLATE]) which could logically use deepslate as recipe input or output - */ - val ORES_IN_GROUND_DEEPSLATE: TagKey = tag("ores_in_ground/deepslate") - - /** - * Ores in netherrack (or in equivalent blocks in the tag [.ORE_BEARING_GROUND_NETHERRACK]) which could logically use netherrack as recipe input or output - */ - val ORES_IN_GROUND_NETHERRACK: TagKey = tag("ores_in_ground/netherrack") - - /** - * Ores in stone (or in equivalent blocks in the tag [.ORE_BEARING_GROUND_STONE]) which could logically use stone as recipe input or output - */ - val ORES_IN_GROUND_STONE: TagKey = tag("ores_in_ground/stone") - val PLAYER_WORKSTATIONS_CRAFTING_TABLES: TagKey = tag("player_workstations/crafting_tables") - val PLAYER_WORKSTATIONS_FURNACES: TagKey = tag("player_workstations/furnaces") - - /** - * Blocks should be included in this tag if their movement/relocation can cause serious issues such - * as world corruption upon being moved or for balance reason where the block should not be able to be relocated. - * Example: Chunk loaders or pipes where other mods that move blocks do not respect - * [BlockBehaviour.BlockStateBase.getPistonPushReaction]. - */ - val RELOCATION_NOT_SUPPORTED: TagKey = tag("relocation_not_supported") - val ROPES: TagKey = tag("ropes") - - val SANDS: TagKey = tag("sands") - val SANDS_COLORLESS: TagKey = tag("sands/colorless") - val SANDS_RED: TagKey = tag("sands/red") - - val SANDSTONE_BLOCKS: TagKey = tag("sandstone/blocks") - val SANDSTONE_SLABS: TagKey = tag("sandstone/slabs") - val SANDSTONE_STAIRS: TagKey = tag("sandstone/stairs") - val SANDSTONE_RED_BLOCKS: TagKey = tag("sandstone/red_blocks") - val SANDSTONE_RED_SLABS: TagKey = tag("sandstone/red_slabs") - val SANDSTONE_RED_STAIRS: TagKey = tag("sandstone/red_stairs") - val SANDSTONE_UNCOLORED_BLOCKS: TagKey = tag("sandstone/uncolored_blocks") - val SANDSTONE_UNCOLORED_SLABS: TagKey = tag("sandstone/uncolored_slabs") - val SANDSTONE_UNCOLORED_STAIRS: TagKey = tag("sandstone/uncolored_stairs") - - val SHULKER_BOXES: TagKey = tag("shulker_boxes") - - /** - * Tag that holds all head based blocks such as Skeleton Skull or Player Head. (Named skulls to match minecraft:skulls item tag) - */ - val SKULLS: TagKey = tag("skulls") - - /** - * Natural stone-like blocks that can be used as a base ingredient in recipes that takes stone. - */ - val STONES: TagKey = tag("stones") - - /** - * A storage block is generally a block that has a recipe to craft a bulk of 1 kind of resource to a block - * and has a mirror recipe to reverse the crafting with no loss in resources. - * - * - * Honey Block is special in that the reversing recipe is not a perfect mirror of the crafting recipe - * and so, it is considered a special case and not given a storage block tag. - */ - val STORAGE_BLOCKS: TagKey = tag("storage_blocks") - val STORAGE_BLOCKS_AMETHYST: TagKey = tag("storage_blocks/amethyst") - val STORAGE_BLOCKS_BONE_MEAL: TagKey = tag("storage_blocks/bone_meal") - val STORAGE_BLOCKS_COAL: TagKey = tag("storage_blocks/coal") - val STORAGE_BLOCKS_COPPER: TagKey = tag("storage_blocks/copper") - val STORAGE_BLOCKS_DIAMOND: TagKey = tag("storage_blocks/diamond") - val STORAGE_BLOCKS_DRIED_KELP: TagKey = tag("storage_blocks/dried_kelp") - val STORAGE_BLOCKS_EMERALD: TagKey = tag("storage_blocks/emerald") - val STORAGE_BLOCKS_GOLD: TagKey = tag("storage_blocks/gold") - val STORAGE_BLOCKS_IRON: TagKey = tag("storage_blocks/iron") - val STORAGE_BLOCKS_LAPIS: TagKey = tag("storage_blocks/lapis") - val STORAGE_BLOCKS_NETHERITE: TagKey = tag("storage_blocks/netherite") - val STORAGE_BLOCKS_QUARTZ: TagKey = tag("storage_blocks/quartz") - val STORAGE_BLOCKS_RAW_COPPER: TagKey = tag("storage_blocks/raw_copper") - val STORAGE_BLOCKS_RAW_GOLD: TagKey = tag("storage_blocks/raw_gold") - val STORAGE_BLOCKS_RAW_IRON: TagKey = tag("storage_blocks/raw_iron") - val STORAGE_BLOCKS_REDSTONE: TagKey = tag("storage_blocks/redstone") - val STORAGE_BLOCKS_SLIME: TagKey = tag("storage_blocks/slime") - val STORAGE_BLOCKS_WHEAT: TagKey = tag("storage_blocks/wheat") - val VILLAGER_JOB_SITES: TagKey = tag("villager_job_sites") - } - - object EntityTypes : Tags>(Registries.ENTITY_TYPE) - { - internal fun init() - { - } - - val BOSSES: TagKey> = tag("bosses") - val MINECARTS: TagKey> = tag("minecarts") - val BOATS: TagKey> = tag("boats") - - /** - * Entities should be included in this tag if they are not allowed to be picked up by items or grabbed in a way - * that a player can easily move the entity to anywhere they want. Ideal for special entities that should not - * be able to be put into a mob jar for example. - */ - val CAPTURING_NOT_SUPPORTED: TagKey> = tag("capturing_not_supported") - - /** - * Entities should be included in this tag if they are not allowed to be teleported in any way. - * This is more for mods that allow teleporting entities within the same dimension. Any mod that is - * teleporting entities to new dimensions should be checking canChangeDimensions method on the entity itself. - */ - val TELEPORTING_NOT_SUPPORTED: TagKey> = tag("teleporting_not_supported") - } - - object Items : Tags(Registries.ITEM) - { - internal fun init() - { - } - - - /** - * Controls what items can be consumed for enchanting such as Enchanting Tables. - * This tag defaults to [net.minecraft.world.item.Items.LAPIS_LAZULI] when not present in any datapacks, including forge client on vanilla server - */ - val ENCHANTING_FUELS: TagKey = tag("neoforge", "enchanting_fuels") - - - // `c` tags for common conventions - val BARRELS: TagKey = tag("barrels") - val BARRELS_WOODEN: TagKey = tag("barrels/wooden") - val BONES: TagKey = tag("bones") - val BOOKSHELVES: TagKey = tag("bookshelves") - val BRICKS: TagKey = tag("bricks") - val BRICKS_NORMAL: TagKey = tag("bricks/normal") - val BRICKS_NETHER: TagKey = tag("bricks/nether") - val BUCKETS: TagKey = tag("buckets") - val BUCKETS_EMPTY: TagKey = tag("buckets/empty") - - /** - * Does not include entity water buckets. - * If checking for the fluid this bucket holds in code, please use `net.neoforged.neoforge.fluids.capability.wrappers.FluidBucketWrapper.getFluid` instead. - */ - val BUCKETS_WATER: TagKey = tag("buckets/water") - - /** - * If checking for the fluid this bucket holds in code, please use `net.neoforged.neoforge.fluids.capability.wrappers.FluidBucketWrapper.getFluid` instead. - */ - val BUCKETS_LAVA: TagKey = tag("buckets/lava") - val BUCKETS_MILK: TagKey = tag("buckets/milk") - val BUCKETS_POWDER_SNOW: TagKey = tag("buckets/powder_snow") - val BUCKETS_ENTITY_WATER: TagKey = tag("buckets/entity_water") - - /** - * For blocks that are similar to amethyst where their budding block produces buds and cluster blocks - */ - val BUDDING_BLOCKS: TagKey = tag("budding_blocks") - - /** - * For blocks that are similar to amethyst where they have buddings forming from budding blocks - */ - val BUDS: TagKey = tag("buds") - val CHAINS: TagKey = tag("chains") - val CHESTS: TagKey = tag("chests") - val CHESTS_ENDER: TagKey = tag("chests/ender") - val CHESTS_TRAPPED: TagKey = tag("chests/trapped") - val CHESTS_WOODEN: TagKey = tag("chests/wooden") - val COBBLESTONES: TagKey = tag("cobblestones") - val COBBLESTONES_NORMAL: TagKey = tag("cobblestones/normal") - val COBBLESTONES_INFESTED: TagKey = tag("cobblestones/infested") - val COBBLESTONES_MOSSY: TagKey = tag("cobblestones/mossy") - val COBBLESTONES_DEEPSLATE: TagKey = tag("cobblestones/deepslate") - - /** - * For blocks that are similar to amethyst where they have clusters forming from budding blocks - */ - val CLUSTERS: TagKey = tag("clusters") - val CROPS: TagKey = tag("crops") - val CROPS_BEETROOT: TagKey = tag("crops/beetroot") - val CROPS_CARROT: TagKey = tag("crops/carrot") - val CROPS_NETHER_WART: TagKey = tag("crops/nether_wart") - val CROPS_POTATO: TagKey = tag("crops/potato") - val CROPS_WHEAT: TagKey = tag("crops/wheat") - val DUSTS: TagKey = tag("dusts") - val DUSTS_PRISMARINE: TagKey = tag("dusts/prismarine") - val DUSTS_REDSTONE: TagKey = tag("dusts/redstone") - val DUSTS_GLOWSTONE: TagKey = tag("dusts/glowstone") - - /** - * Tag that holds all blocks and items that can be dyed a specific color. - * (Does not include color blending items like leather armor - * Use [net.minecraft.tags.ItemTags.DYEABLE] tag instead for color blending items) - * - * - * Note: Use custom ingredients in recipes to do tag intersections and/or tag exclusions - * to make more powerful recipes utilizing multiple tags such as dyed tags for an ingredient. - * See `net.neoforged.neoforge.common.crafting.DifferenceIngredient` and `net.neoforged.neoforge.common.crafting.CompoundIngredient` - * for various custom ingredients available that can also be used in data generation. - */ - val DYED: TagKey = tag("dyed") - val DYED_BLACK: TagKey = tag("dyed/black") - val DYED_BLUE: TagKey = tag("dyed/blue") - val DYED_BROWN: TagKey = tag("dyed/brown") - val DYED_CYAN: TagKey = tag("dyed/cyan") - val DYED_GRAY: TagKey = tag("dyed/gray") - val DYED_GREEN: TagKey = tag("dyed/green") - val DYED_LIGHT_BLUE: TagKey = tag("dyed/light_blue") - val DYED_LIGHT_GRAY: TagKey = tag("dyed/light_gray") - val DYED_LIME: TagKey = tag("dyed/lime") - val DYED_MAGENTA: TagKey = tag("dyed/magenta") - val DYED_ORANGE: TagKey = tag("dyed/orange") - val DYED_PINK: TagKey = tag("dyed/pink") - val DYED_PURPLE: TagKey = tag("dyed/purple") - val DYED_RED: TagKey = tag("dyed/red") - val DYED_WHITE: TagKey = tag("dyed/white") - val DYED_YELLOW: TagKey = tag("dyed/yellow") - - val DYES: TagKey = tag("dyes") - val DYES_BLACK: TagKey = DyeColor.BLACK.tag - val DYES_RED: TagKey = DyeColor.RED.tag - val DYES_GREEN: TagKey = DyeColor.GREEN.tag - val DYES_BROWN: TagKey = DyeColor.BROWN.tag - val DYES_BLUE: TagKey = DyeColor.BLUE.tag - val DYES_PURPLE: TagKey = DyeColor.PURPLE.tag - val DYES_CYAN: TagKey = DyeColor.CYAN.tag - val DYES_LIGHT_GRAY: TagKey = DyeColor.LIGHT_GRAY.tag - val DYES_GRAY: TagKey = DyeColor.GRAY.tag - val DYES_PINK: TagKey = DyeColor.PINK.tag - val DYES_LIME: TagKey = DyeColor.LIME.tag - val DYES_YELLOW: TagKey = DyeColor.YELLOW.tag - val DYES_LIGHT_BLUE: TagKey = DyeColor.LIGHT_BLUE.tag - val DYES_MAGENTA: TagKey = DyeColor.MAGENTA.tag - val DYES_ORANGE: TagKey = DyeColor.ORANGE.tag - val DYES_WHITE: TagKey = DyeColor.WHITE.tag - - private val DyeColor.tag: TagKey - get() = tag("dyes/$name".lowercase()) - - - val EGGS: TagKey = tag("eggs") - val END_STONES: TagKey = tag("end_stones") - val ENDER_PEARLS: TagKey = tag("ender_pearls") - val FEATHERS: TagKey = tag("feathers") - val FENCE_GATES: TagKey = tag("fence_gates") - val FENCE_GATES_WOODEN: TagKey = tag("fence_gates/wooden") - val FENCES: TagKey = tag("fences") - val FENCES_NETHER_BRICK: TagKey = tag("fences/nether_brick") - val FENCES_WOODEN: TagKey = tag("fences/wooden") - val FOODS: TagKey = tag("foods") - - /** - * Apples and other foods that are considered fruits in the culinary field belong in this tag. - * Cherries would go here as they are considered a "stone fruit" within culinary fields. - */ - val FOODS_FRUITS: TagKey = tag("foods/fruits") - - /** - * Tomatoes and other foods that are considered vegetables in the culinary field belong in this tag. - */ - val FOODS_VEGETABLES: TagKey = tag("foods/vegetables") - - /** - * Strawberries, raspberries, and other berry foods belong in this tag. - * Cherries would NOT go here as they are considered a "stone fruit" within culinary fields. - */ - val FOODS_BERRIES: TagKey = tag("foods/berries") - val FOODS_BREADS: TagKey = tag("foods/breads") - val FOODS_COOKIES: TagKey = tag("foods/cookies") - val FOODS_RAW_MEATS: TagKey = tag("foods/raw_meats") - val FOODS_COOKED_MEATS: TagKey = tag("foods/cooked_meats") - val FOODS_RAW_FISHES: TagKey = tag("foods/raw_fishes") - val FOODS_COOKED_FISHES: TagKey = tag("foods/cooked_fishes") - - /** - * Soups, stews, and other liquid food in bowls belongs in this tag. - */ - val FOODS_SOUPS: TagKey = tag("foods/soups") - - /** - * Sweets and candies like lollipops or chocolate belong in this tag. - */ - val FOODS_CANDIES: TagKey = tag("foods/candies") - - /** - * Foods like cake that can be eaten when placed in the world belong in this tag. - */ - val FOODS_EDIBLE_WHEN_PLACED: TagKey = tag("foods/edible_when_placed") - - /** - * For foods that inflict food poisoning-like effects. - * Examples are Rotten Flesh's Hunger or Pufferfish's Nausea, or Poisonous Potato's Poison. - */ - val FOODS_FOOD_POISONING: TagKey = tag("foods/food_poisoning") - val GEMS: TagKey = tag("gems") - val GEMS_DIAMOND: TagKey = tag("gems/diamond") - val GEMS_EMERALD: TagKey = tag("gems/emerald") - val GEMS_AMETHYST: TagKey = tag("gems/amethyst") - val GEMS_LAPIS: TagKey = tag("gems/lapis") - val GEMS_PRISMARINE: TagKey = tag("gems/prismarine") - val GEMS_QUARTZ: TagKey = tag("gems/quartz") - - val GLASS_BLOCKS: TagKey = tag("glass_blocks") - val GLASS_BLOCKS_COLORLESS: TagKey = tag("glass_blocks/colorless") - - /** - * Glass which is made from cheap resources like sand and only minor additional ingredients like dyes - */ - val GLASS_BLOCKS_CHEAP: TagKey = tag("glass_blocks/cheap") - val GLASS_BLOCKS_STAINED: TagKey = tag("glass_blocks/stained") - val GLASS_BLOCKS_TINTED: TagKey = tag("glass_blocks/tinted") - - val GLASS_PANES: TagKey = tag("glass_panes") - val GLASS_PANES_COLORLESS: TagKey = tag("glass_panes/colorless") - val GLASS_PANES_STAINED: TagKey = tag("glass_panes/stained") - - val GRAVELS: TagKey = tag("gravel") - val GUNPOWDERS: TagKey = tag("gunpowder") - - /** - * Tag that holds all items that recipe viewers should not show to users. - */ - val HIDDEN_FROM_RECIPE_VIEWERS: TagKey = tag("hidden_from_recipe_viewers") - val INGOTS: TagKey = tag("ingots") - val INGOTS_COPPER: TagKey = tag("ingots/copper") - val INGOTS_GOLD: TagKey = tag("ingots/gold") - val INGOTS_IRON: TagKey = tag("ingots/iron") - val INGOTS_NETHERITE: TagKey = tag("ingots/netherite") - val LEATHERS: TagKey = tag("leather") - val MUSHROOMS: TagKey = tag("mushrooms") - val NETHER_STARS: TagKey = tag("nether_stars") - val NETHERRACKS: TagKey = tag("netherrack") - val NUGGETS: TagKey = tag("nuggets") - val NUGGETS_GOLD: TagKey = tag("nuggets/gold") - val NUGGETS_IRON: TagKey = tag("nuggets/iron") - val OBSIDIANS: TagKey = tag("obsidians") - - /** - * Blocks which are often replaced by deepslate ores, i.e. the ores in the tag [.ORES_IN_GROUND_DEEPSLATE], during world generation - */ - val ORE_BEARING_GROUND_DEEPSLATE: TagKey = tag("ore_bearing_ground/deepslate") - - /** - * Blocks which are often replaced by netherrack ores, i.e. the ores in the tag [.ORES_IN_GROUND_NETHERRACK], during world generation - */ - val ORE_BEARING_GROUND_NETHERRACK: TagKey = tag("ore_bearing_ground/netherrack") - - /** - * Blocks which are often replaced by stone ores, i.e. the ores in the tag [.ORES_IN_GROUND_STONE], during world generation - */ - val ORE_BEARING_GROUND_STONE: TagKey = tag("ore_bearing_ground/stone") - - /** - * Ores which on average result in more than one resource worth of materials - */ - val ORE_RATES_DENSE: TagKey = tag("ore_rates/dense") - - /** - * Ores which on average result in one resource worth of materials - */ - val ORE_RATES_SINGULAR: TagKey = tag("ore_rates/singular") - - /** - * Ores which on average result in less than one resource worth of materials - */ - val ORE_RATES_SPARSE: TagKey = tag("ore_rates/sparse") - val ORES: TagKey = tag("ores") - val ORES_COAL: TagKey = tag("ores/coal") - val ORES_COPPER: TagKey = tag("ores/copper") - val ORES_DIAMOND: TagKey = tag("ores/diamond") - val ORES_EMERALD: TagKey = tag("ores/emerald") - val ORES_GOLD: TagKey = tag("ores/gold") - val ORES_IRON: TagKey = tag("ores/iron") - val ORES_LAPIS: TagKey = tag("ores/lapis") - val ORES_NETHERITE_SCRAP: TagKey = tag("ores/netherite_scrap") - val ORES_QUARTZ: TagKey = tag("ores/quartz") - val ORES_REDSTONE: TagKey = tag("ores/redstone") - - /** - * Ores in deepslate (or in equivalent blocks in the tag [.ORE_BEARING_GROUND_DEEPSLATE]) which could logically use deepslate as recipe input or output - */ - val ORES_IN_GROUND_DEEPSLATE: TagKey = tag("ores_in_ground/deepslate") - - /** - * Ores in netherrack (or in equivalent blocks in the tag [.ORE_BEARING_GROUND_NETHERRACK]) which could logically use netherrack as recipe input or output - */ - val ORES_IN_GROUND_NETHERRACK: TagKey = tag("ores_in_ground/netherrack") - - /** - * Ores in stone (or in equivalent blocks in the tag [.ORE_BEARING_GROUND_STONE]) which could logically use stone as recipe input or output - */ - val ORES_IN_GROUND_STONE: TagKey = tag("ores_in_ground/stone") - val PLAYER_WORKSTATIONS_CRAFTING_TABLES: TagKey = tag("player_workstations/crafting_tables") - val PLAYER_WORKSTATIONS_FURNACES: TagKey = tag("player_workstations/furnaces") - val RAW_BLOCKS: TagKey = tag("raw_blocks") - val RAW_BLOCKS_COPPER: TagKey = tag("raw_blocks/copper") - val RAW_BLOCKS_GOLD: TagKey = tag("raw_blocks/gold") - val RAW_BLOCKS_IRON: TagKey = tag("raw_blocks/iron") - val RAW_MATERIALS: TagKey = tag("raw_materials") - val RAW_MATERIALS_COPPER: TagKey = tag("raw_materials/copper") - val RAW_MATERIALS_GOLD: TagKey = tag("raw_materials/gold") - val RAW_MATERIALS_IRON: TagKey = tag("raw_materials/iron") - - /** - * For rod-like materials to be used in recipes. - */ - val RODS: TagKey = tag("rods") - val RODS_BLAZE: TagKey = tag("rods/blaze") - val RODS_BREEZE: TagKey = tag("rods/breeze") - - /** - * For stick-like materials to be used in recipes. - * One example is a mod adds stick variants such as Spruce Sticks but would like stick recipes to be able to use it. - */ - val RODS_WOODEN: TagKey = tag("rods/wooden") - val ROPES: TagKey = tag("ropes") - - val SANDS: TagKey = tag("sands") - val SANDS_COLORLESS: TagKey = tag("sands/colorless") - val SANDS_RED: TagKey = tag("sands/red") - - val SANDSTONE_BLOCKS: TagKey = tag("sandstone/blocks") - val SANDSTONE_SLABS: TagKey = tag("sandstone/slabs") - val SANDSTONE_STAIRS: TagKey = tag("sandstone/stairs") - val SANDSTONE_RED_BLOCKS: TagKey = tag("sandstone/red_blocks") - val SANDSTONE_RED_SLABS: TagKey = tag("sandstone/red_slabs") - val SANDSTONE_RED_STAIRS: TagKey = tag("sandstone/red_stairs") - val SANDSTONE_UNCOLORED_BLOCKS: TagKey = tag("sandstone/uncolored_blocks") - val SANDSTONE_UNCOLORED_SLABS: TagKey = tag("sandstone/uncolored_slabs") - val SANDSTONE_UNCOLORED_STAIRS: TagKey = tag("sandstone/uncolored_stairs") - - val SEEDS: TagKey = tag("seeds") - val SEEDS_BEETROOT: TagKey = tag("seeds/beetroot") - val SEEDS_MELON: TagKey = tag("seeds/melon") - val SEEDS_PUMPKIN: TagKey = tag("seeds/pumpkin") - val SEEDS_WHEAT: TagKey = tag("seeds/wheat") - val SHULKER_BOXES: TagKey = tag("shulker_boxes") - val SLIMEBALLS: TagKey = tag("slimeballs") - - /** - * Natural stone-like blocks that can be used as a base ingredient in recipes that takes stone. - */ - val STONES: TagKey = tag("stones") - - val SKULLS: TagKey = existing(ItemTags.SKULLS) - - /** - * A storage block is generally a block that has a recipe to craft a bulk of 1 kind of resource to a block - * and has a mirror recipe to reverse the crafting with no loss in resources. - * - * - * Honey Block is special in that the reversing recipe is not a perfect mirror of the crafting recipe - * and so, it is considered a special case and not given a storage block tag. - */ - val STORAGE_BLOCKS: TagKey = tag("storage_blocks") - val STORAGE_BLOCKS_AMETHYST: TagKey = tag("storage_blocks/amethyst") - val STORAGE_BLOCKS_BONE_MEAL: TagKey = tag("storage_blocks/bone_meal") - val STORAGE_BLOCKS_COAL: TagKey = tag("storage_blocks/coal") - val STORAGE_BLOCKS_COPPER: TagKey = tag("storage_blocks/copper") - val STORAGE_BLOCKS_DIAMOND: TagKey = tag("storage_blocks/diamond") - val STORAGE_BLOCKS_DRIED_KELP: TagKey = tag("storage_blocks/dried_kelp") - val STORAGE_BLOCKS_EMERALD: TagKey = tag("storage_blocks/emerald") - val STORAGE_BLOCKS_GOLD: TagKey = tag("storage_blocks/gold") - val STORAGE_BLOCKS_IRON: TagKey = tag("storage_blocks/iron") - val STORAGE_BLOCKS_LAPIS: TagKey = tag("storage_blocks/lapis") - val STORAGE_BLOCKS_NETHERITE: TagKey = tag("storage_blocks/netherite") - val STORAGE_BLOCKS_QUARTZ: TagKey = tag("storage_blocks/quartz") - val STORAGE_BLOCKS_RAW_COPPER: TagKey = tag("storage_blocks/raw_copper") - val STORAGE_BLOCKS_RAW_GOLD: TagKey = tag("storage_blocks/raw_gold") - val STORAGE_BLOCKS_RAW_IRON: TagKey = tag("storage_blocks/raw_iron") - val STORAGE_BLOCKS_REDSTONE: TagKey = tag("storage_blocks/redstone") - val STORAGE_BLOCKS_SLIME: TagKey = tag("storage_blocks/slime") - val STORAGE_BLOCKS_WHEAT: TagKey = tag("storage_blocks/wheat") - val STRINGS: TagKey = tag("strings") - val VILLAGER_JOB_SITES: TagKey = tag("villager_job_sites") - - - // Tools and Armors - /** - * A tag containing all existing tools. Do not use this tag for determining a tool's behavior. - * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. - */ - val TOOLS: TagKey = tag("tools") - - /** - * A tag containing all existing axes. Do not use this tag for determining a tool's behavior. - * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. - */ - val TOOLS_AXES: TagKey = existing(ItemTags.AXES) - - /** - * A tag containing all existing hoes. Do not use this tag for determining a tool's behavior. - * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. - */ - val TOOLS_HOES: TagKey = existing(ItemTags.HOES) - - /** - * A tag containing all existing pickaxes. Do not use this tag for determining a tool's behavior. - * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. - */ - val TOOLS_PICKAXES: TagKey = existing(ItemTags.PICKAXES) - - /** - * A tag containing all existing shovels. Do not use this tag for determining a tool's behavior. - * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. - */ - val TOOLS_SHOVELS: TagKey = existing(ItemTags.SHOVELS) - - /** - * A tag containing all existing swords. Do not use this tag for determining a tool's behavior. - * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. - */ - val TOOLS_SWORDS: TagKey = existing(ItemTags.SWORDS) - - /** - * A tag containing all existing shields. Do not use this tag for determining a tool's behavior. - * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. - */ - val TOOLS_SHIELDS: TagKey = tag("tools/shields") - - /** - * A tag containing all existing bows. Do not use this tag for determining a tool's behavior. - * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. - */ - val TOOLS_BOWS: TagKey = tag("tools/bows") - - /** - * A tag containing all existing crossbows. Do not use this tag for determining a tool's behavior. - * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. - */ - val TOOLS_CROSSBOWS: TagKey = tag("tools/crossbows") - - /** - * A tag containing all existing fishing rods. Do not use this tag for determining a tool's behavior. - * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. - */ - val TOOLS_FISHING_RODS: TagKey = tag("tools/fishing_rods") - - /** - * A tag containing all existing spears. Other tools such as throwing knives or boomerangs - * should not be put into this tag and should be put into their own tool tags. - * Do not use this tag for determining a tool's behavior. - * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. - */ - val TOOLS_SPEARS: TagKey = tag("tools/spears") - - /** - * A tag containing all existing shears. Do not use this tag for determining a tool's behavior. - * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. - */ - val TOOLS_SHEARS: TagKey = tag("tools/shears") - - /** - * A tag containing all existing brushes. Do not use this tag for determining a tool's behavior. - * Please use `net.neoforged.neoforge.common.ToolActions` instead for what action a tool can do. - */ - val TOOLS_BRUSHES: TagKey = tag("tools/brushes") - - /** - * Collects the 4 vanilla armor tags into one parent collection for ease. - */ - val ARMORS: TagKey = tag("armors") - - /** - * A tag containing all existing helmets. - */ - val ARMORS_HELMETS: TagKey = existing(ItemTags.HEAD_ARMOR) - - /** - * A tag containing all chestplates. - */ - val ARMORS_CHESTPLATES: TagKey = existing(ItemTags.CHEST_ARMOR) - - /** - * A tag containing all existing leggings. - */ - val ARMORS_LEGGINGS: TagKey = existing(ItemTags.LEG_ARMOR) - - /** - * A tag containing all existing boots. - */ - val ARMORS_BOOTS: TagKey = existing(ItemTags.FOOT_ARMOR) - - /** - * Collects the many enchantable tags into one parent collection for ease. - */ - val ENCHANTABLES: TagKey = tag("enchantables") - - } - - object Fluids : Tags(Registries.FLUID) - { - internal fun init() - { - } - - /** - * Holds all fluids related to water. - * This tag is done to help out multi-loader mods/datapacks where the vanilla water tag has attached behaviors outside Neo. - */ - val WATER: TagKey = tag("water") - - /** - * Holds all fluids related to lava. - * This tag is done to help out multi-loader mods/datapacks where the vanilla lava tag has attached behaviors outside Neo. - */ - val LAVA: TagKey = tag("lava") - - /** - * Holds all fluids related to milk. - */ - val MILK: TagKey = tag("milk") - - /** - * Holds all fluids that are gaseous at room temperature. - */ - val GASEOUS: TagKey = tag("gaseous") - - /** - * Holds all fluids related to honey.

- * (Standard unit for honey bottle is 250mb per bottle) - */ - val HONEY: TagKey = tag("honey") - - /** - * Holds all fluids related to potions. The effects of the potion fluid should be read from NBT. - * The effects and color of the potion fluid should be read from [net.minecraft.core.component.DataComponents.POTION_CONTENTS] - * component that people should be attaching to the fluidstack of this fluid.

- * (Standard unit for potions is 250mb per bottle) - */ - val POTION: TagKey = tag("potion") - - /** - * Holds all fluids related to Suspicious Stew. - * The effects of the suspicious stew fluid should be read from [net.minecraft.core.component.DataComponents.SUSPICIOUS_STEW_EFFECTS] - * component that people should be attaching to the fluidstack of this fluid.

- * (Standard unit for suspicious stew is 250mb per bowl) - */ - val SUSPICIOUS_STEW: TagKey = tag("suspicious_stew") - - /** - * Holds all fluids related to Mushroom Stew.

- * (Standard unit for mushroom stew is 250mb per bowl) - */ - val MUSHROOM_STEW: TagKey = tag("mushroom_stew") - - /** - * Holds all fluids related to Rabbit Stew.

- * (Standard unit for rabbit stew is 250mb per bowl) - */ - val RABBIT_STEW: TagKey = tag("rabbit_stew") - - /** - * Holds all fluids related to Beetroot Soup.

- * (Standard unit for beetroot soup is 250mb per bowl) - */ - val BEETROOT_SOUP: TagKey = tag("beetroot_soup") - - /** - * Tag that holds all fluids that recipe viewers should not show to users. - */ - val HIDDEN_FROM_RECIPE_VIEWERS: TagKey = tag("hidden_from_recipe_viewers") - } - - object Biomes : Tags(Registries.BIOME) - { - internal fun init() - { - } - - /** - * For biomes that should not spawn monsters over time the normal way. - * In other words, their Spawners and Spawn Cost entries have the monster category empty. - * Example: Mushroom Biomes not having Zombies, Creepers, Skeleton, nor any other normal monsters. - */ - val NO_DEFAULT_MONSTERS: TagKey = tag("no_default_monsters") - - /** - * Biomes that should not be locatable/selectable by modded biome-locating items or abilities. - */ - val HIDDEN_FROM_LOCATOR_SELECTION: TagKey = tag("hidden_from_locator_selection") - - val IS_VOID: TagKey = tag("is_void") - - val IS_HOT: TagKey = tag("is_hot") - val IS_HOT_OVERWORLD: TagKey = tag("is_hot/overworld") - val IS_HOT_NETHER: TagKey = tag("is_hot/nether") - val IS_HOT_END: TagKey = tag("is_hot/end") - - val IS_COLD: TagKey = tag("is_cold") - val IS_COLD_OVERWORLD: TagKey = tag("is_cold/overworld") - val IS_COLD_NETHER: TagKey = tag("is_cold/nether") - val IS_COLD_END: TagKey = tag("is_cold/end") - - val IS_SPARSE_VEGETATION: TagKey = tag("is_sparse_vegetation") - val IS_SPARSE_VEGETATION_OVERWORLD: TagKey = tag("is_sparse_vegetation/overworld") - val IS_SPARSE_VEGETATION_NETHER: TagKey = tag("is_sparse_vegetation/nether") - val IS_SPARSE_VEGETATION_END: TagKey = tag("is_sparse_vegetation/end") - val IS_DENSE_VEGETATION: TagKey = tag("is_dense_vegetation") - val IS_DENSE_VEGETATION_OVERWORLD: TagKey = tag("is_dense_vegetation/overworld") - val IS_DENSE_VEGETATION_NETHER: TagKey = tag("is_dense_vegetation/nether") - val IS_DENSE_VEGETATION_END: TagKey = tag("is_dense_vegetation/end") - - val IS_WET: TagKey = tag("is_wet") - val IS_WET_OVERWORLD: TagKey = tag("is_wet/overworld") - val IS_WET_NETHER: TagKey = tag("is_wet/nether") - val IS_WET_END: TagKey = tag("is_wet/end") - val IS_DRY: TagKey = tag("is_dry") - val IS_DRY_OVERWORLD: TagKey = tag("is_dry/overworld") - val IS_DRY_NETHER: TagKey = tag("is_dry/nether") - val IS_DRY_END: TagKey = tag("is_dry/end") - - /** - * Biomes that spawn in the Overworld. - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_OVERWORLD] - * - * - * NOTE: If you do not add to the vanilla Overworld tag, be sure to add to - * [net.minecraft.tags.BiomeTags.HAS_STRONGHOLD] so some Strongholds do not go missing.) - */ - val IS_OVERWORLD: TagKey = tag("is_overworld") - - val IS_CONIFEROUS_TREE: TagKey = tag("is_tree/coniferous") - val IS_SAVANNA_TREE: TagKey = tag("is_tree/savanna") - val IS_JUNGLE_TREE: TagKey = tag("is_tree/jungle") - val IS_DECIDUOUS_TREE: TagKey = tag("is_tree/deciduous") - - /** - * Biomes that spawn as part of giant mountains. - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_MOUNTAIN]) - */ - val IS_MOUNTAIN: TagKey = tag("is_mountain") - val IS_MOUNTAIN_PEAK: TagKey = tag("is_mountain/peak") - val IS_MOUNTAIN_SLOPE: TagKey = tag("is_mountain/slope") - - /** - * For temperate or warmer plains-like biomes. - * For snowy plains-like biomes, see [.IS_SNOWY_PLAINS]. - */ - val IS_PLAINS: TagKey = tag("is_plains") - - /** - * For snowy plains-like biomes. - * For warmer plains-like biomes, see [.IS_PLAINS]. - */ - val IS_SNOWY_PLAINS: TagKey = tag("is_snowy_plains") - - /** - * Biomes densely populated with deciduous trees. - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_FOREST]) - */ - val IS_FOREST: TagKey = tag("is_forest") - val IS_BIRCH_FOREST: TagKey = tag("is_birch_forest") - val IS_FLOWER_FOREST: TagKey = tag("is_flower_forest") - - /** - * Biomes that spawn as a taiga. - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_TAIGA]) - */ - val IS_TAIGA: TagKey = tag("is_taiga") - val IS_OLD_GROWTH: TagKey = tag("is_old_growth") - - /** - * Biomes that spawn as a hills biome. (Previously was called Extreme Hills biome in past) - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_HILL]) - */ - val IS_HILL: TagKey = tag("is_hill") - val IS_WINDSWEPT: TagKey = tag("is_windswept") - - /** - * Biomes that spawn as a jungle. - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_JUNGLE]) - */ - val IS_JUNGLE: TagKey = tag("is_jungle") - - /** - * Biomes that spawn as a savanna. - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_SAVANNA]) - */ - val IS_SAVANNA: TagKey = tag("is_savanna") - val IS_SWAMP: TagKey = tag("is_swamp") - val IS_DESERT: TagKey = tag("is_desert") - - /** - * Biomes that spawn as a badlands. - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_BADLANDS]) - */ - val IS_BADLANDS: TagKey = tag("is_badlands") - - /** - * Biomes that are dedicated to spawning on the shoreline of a body of water. - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_BEACH]) - */ - val IS_BEACH: TagKey = tag("is_beach") - val IS_STONY_SHORES: TagKey = tag("is_stony_shores") - val IS_MUSHROOM: TagKey = tag("is_mushroom") - - /** - * Biomes that spawn as a river. - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_RIVER]) - */ - val IS_RIVER: TagKey = tag("is_river") - - /** - * Biomes that spawn as part of the world's oceans. - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_OCEAN]) - */ - val IS_OCEAN: TagKey = tag("is_ocean") - - /** - * Biomes that spawn as part of the world's oceans that have low depth. - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_DEEP_OCEAN]) - */ - val IS_DEEP_OCEAN: TagKey = tag("is_deep_ocean") - val IS_SHALLOW_OCEAN: TagKey = tag("is_shallow_ocean") - - val IS_UNDERGROUND: TagKey = tag("is_underground") - val IS_CAVE: TagKey = tag("is_cave") - - val IS_LUSH: TagKey = tag("is_lush") - val IS_MAGICAL: TagKey = tag("is_magical") - val IS_RARE: TagKey = tag("is_rare") - val IS_PLATEAU: TagKey = tag("is_plateau") - val IS_MODIFIED: TagKey = tag("is_modified") - val IS_SPOOKY: TagKey = tag("is_spooky") - - /** - * Biomes that lack any natural life or vegetation. - * (Example, land destroyed and sterilized by nuclear weapons) - */ - val IS_WASTELAND: TagKey = tag("is_wasteland") - - /** - * Biomes whose flora primarily consists of dead or decaying vegetation. - */ - val IS_DEAD: TagKey = tag("is_dead") - - /** - * Biomes with a large amount of flowers. - */ - val IS_FLORAL: TagKey = tag("is_floral") - - /** - * Biomes that are able to spawn sand-based blocks on the surface. - */ - val IS_SANDY: TagKey = tag("is_sandy") - - /** - * For biomes that contains lots of naturally spawned snow. - * For biomes where lot of ice is present, see [IS_ICY]. - * Biome with lots of both snow and ice may be in both tags. - */ - val IS_SNOWY: TagKey = tag("is_snowy") - - /** - * For land biomes where ice naturally spawns. - * For biomes where snow alone spawns, see [IS_SNOWY]. - */ - val IS_ICY: TagKey = tag("is_icy") - - /** - * Biomes consisting primarily of water. - */ - val IS_AQUATIC: TagKey = tag("is_aquatic") - - /** - * For water biomes where ice naturally spawns. - * For biomes where snow alone spawns, see [IS_SNOWY]. - */ - val IS_AQUATIC_ICY: TagKey = tag("is_aquatic_icy") - - /** - * Biomes that spawn in the Nether. - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_NETHER]) - */ - val IS_NETHER: TagKey = tag("is_nether") - val IS_NETHER_FOREST: TagKey = tag("is_nether_forest") - - /** - * Biomes that spawn in the End. - * (This is for people who want to tag their biomes without getting - * side effects from [net.minecraft.tags.BiomeTags.IS_END]) - */ - val IS_END: TagKey = tag("is_end") - - /** - * Biomes that spawn as part of the large islands outside the center island in The End dimension. - */ - val IS_OUTER_END_ISLAND: TagKey = tag("is_outer_end_island") - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilder.kt deleted file mode 100644 index ba7e01c70..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilder.kt +++ /dev/null @@ -1,169 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.tags - -import net.minecraft.data.tags.TagsProvider -import net.minecraft.resources.ResourceKey -import net.minecraft.resources.ResourceLocation -import net.minecraft.tags.TagEntry -import net.minecraft.tags.TagKey -import java.util.function.Consumer -import java.util.function.Predicate -import java.util.stream.Stream - -class ATagBuilder(private val parent: TagsProvider.TagAppender, private val provider: ATagsProvider) : - TagsProvider.TagAppender(parent.builder), IATagBuilder -{ - override fun setReplace(replace: Boolean): ATagBuilder - { - ATagBuilderPlatform.setTagReplace(builder, replace) - return this - } - - override fun replace(): ATagBuilder - { - return setReplace(true) - } - - override fun add(element: T): ATagBuilder - { - add(provider.reverseLookup(element)) - return this - } - - @SafeVarargs - override fun add(vararg elements: T): ATagBuilder - { - Stream.of(*elements).map { element: T -> - provider.reverseLookup( - element - ) - }.forEach { registryKey: ResourceKey -> - this.add( - registryKey - ) - } - return this - } - - override fun add(registryKey: ResourceKey): ATagBuilder - { - parent.add(registryKey) - return this - } - - override fun add(id: ResourceLocation): ATagBuilder - { - builder.addElement(id) - return this - } - - override fun addOptional(id: ResourceLocation): ATagBuilder - { - parent.addOptional(id) - return this - } - - override fun addOptional(registryKey: ResourceKey): ATagBuilder - { - return addOptional(registryKey.location()) - } - - override fun addOptionals(vararg ids: ResourceLocation): ATagBuilder - { - ids.forEach(this::addOptional) - return this - } - - override fun addOptionals(vararg keys: ResourceKey): ATagBuilder - { - keys.forEach(this::addOptional) - return this - } - - override fun addTag(tag: TagKey): ATagBuilder - { - builder.add(ForcedTagEntry(TagEntry.tag(tag.location()))) - return this - } - - override fun addOptionalTag(id: ResourceLocation): ATagBuilder - { - parent.addOptionalTag(id) - return this - } - - override fun addOptionalTag(tag: TagKey): ATagBuilder - { - return addOptionalTag(tag.location()) - } - - override fun addOptionalTags(vararg ids: ResourceLocation): ATagBuilder - { - ids.forEach(this::addOptionalTag) - return this - } - - override fun addOptionalTags(vararg tags: TagKey): ATagBuilder - { - tags.forEach(this::addOptionalTag) - return this - } - - override fun add(vararg ids: ResourceLocation): ATagBuilder - { - for (id in ids) - { - add(id) - } - - return this - } - - @SafeVarargs - override fun add(vararg registryKeys: ResourceKey): ATagBuilder - { - for (registryKey in registryKeys) - { - add(registryKey) - } - - return this - } - - override fun addTags(vararg ids: ResourceLocation): ATagBuilder - { - for (id in ids) - { - builder.addTag(id) - } - - return this - } - - @SafeVarargs - override fun addTags(vararg tagKeys: TagKey): ATagBuilder - { - for (tagKey in tagKeys) - { - addTag(tagKey) - } - - return this - } - - class ForcedTagEntry(private val delegate: TagEntry) : - TagEntry(delegate.id, true, delegate.required) - { - override fun build(arg: Lookup, consumer: Consumer): Boolean - { - return delegate.build(arg, consumer) - } - - override fun verifyIfPresent( - objectExistsTest: Predicate, - tagExistsTest: Predicate - ): Boolean - { - return true - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.common.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.common.kt deleted file mode 100644 index 6847e897b..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.common.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.tags - -import net.minecraft.data.tags.TagsProvider.TagAppender -import net.minecraft.tags.TagBuilder - -/** Cross-loader hooks into vanilla/loader-specific tag builder internals not otherwise exposed uniformly. */ -expect object ATagBuilderPlatform -{ - /** Sets whether a tag file [replace]s (rather than merges with) tags from lower-priority datapacks. */ - fun setTagReplace(builder: TagBuilder, replace: Boolean) - - /** Creates an [IATagBuilder] wrapping [parent], the loader's native tag appender for [provider]. */ - fun createTagBuilder(parent: TagAppender, provider: ATagsProvider): IATagBuilder -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagsProvider.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagsProvider.kt deleted file mode 100644 index 649401016..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagsProvider.kt +++ /dev/null @@ -1,423 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.tags - -import dev.architectury.extensions.injected.InjectedRegistryEntryExtension -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.data.IADataProvider -import net.kernelpanicsoft.archie.registries.holder -import dev.architectury.platform.Mod -import net.minecraft.core.Holder -import net.minecraft.core.HolderLookup -import net.minecraft.core.Registry -import net.minecraft.core.RegistryAccess -import net.minecraft.core.registries.BuiltInRegistries -import net.minecraft.core.registries.Registries -import net.minecraft.data.PackOutput -import net.minecraft.data.tags.TagsProvider -import net.minecraft.resources.ResourceKey -import net.minecraft.resources.ResourceLocation -import net.minecraft.tags.* -import net.minecraft.world.entity.EntityType -import net.minecraft.world.item.Item -import net.minecraft.world.level.biome.Biome -import net.minecraft.world.level.block.Block -import net.minecraft.world.level.material.Fluid -import java.util.* -import java.util.concurrent.CompletableFuture -import java.util.function.Consumer -import java.util.function.Function -import kotlin.jvm.optionals.getOrNull -import kotlin.system.exitProcess - -abstract class ATagsProvider( - override val output: PackOutput, override val mod: Mod, registryKey: ResourceKey>, - registries: CompletableFuture, override val exitOnError: Boolean -) : TagsProvider(output, registryKey, registries), IADataProvider -{ - final override fun addTags(registries: HolderLookup.Provider) - { - runCatching { - generate(registries) - }.onFailure { - Archie.LOGGER.error( - "Data Provider $name failed with exception: ${it.message}\n" + - "Stacktrace: ${it.stackTraceToString()}" - ) - if (exitOnError) exitProcess(-1) - } - } - - /** - * Implement this method and then use [invoke] to get and register new tag builders. - */ - abstract fun generate(registries: HolderLookup.Provider) - - /** - * Looks up a registry entry for a specific [ResourceKey]. - * Only works if the resource key corresponds to the provider's [registryKey] - * @param registries The [HolderLookup.Provider] received from [addTags] - * @param key The [ResourceKey] to look up - * @return The looked up registry entry - * @throws IllegalStateException If either the registry or the entry cannot be looked up - */ - protected open fun lookup(registries: HolderLookup.Provider, key: ResourceKey): T - { - val registryLookup = registries.lookupOrThrow(registryKey) - return registryLookup.getOrThrow(key).value() - } - - /** - * Looks up a registry entry for a specific [ResourceLocation]. - * Uses the provider's [registryKey] to create a [ResourceKey] and delegates - * to the overload that takes a [ResourceKey] - * @param registries The [HolderLookup.Provider] received from [addTags] - * @param key The [ResourceKey] to look up - * @return The looked up registry entry - * @throws IllegalStateException If either the registry or the entry cannot be looked up - */ - protected open fun lookup(registries: HolderLookup.Provider, key: ResourceLocation): T - { - return lookup(registries, ResourceKey.create(registryKey, key)) - } - - /** - * Override to enable adding objects to the tag builder directly. - */ - open fun reverseLookup(element: T): ResourceKey - { - val registry = - RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY).registry(registryKey).getOrNull() - - if (registry != null) - { - val key: Optional> = registry.getResourceKey(element) - - if (key.isPresent) - { - return key.get() - } - } - - throw UnsupportedOperationException("Adding objects is not supported by $javaClass") - } - - @Suppress("UNCHECKED_CAST") - protected fun reverseLookupInjected(element: E): ResourceKey - { - return ((element as InjectedRegistryEntryExtension).holder as Holder.Reference).key() - } - - /** - * Creates a new instance of [IATagBuilder] for the given [TagKey]. - * - * @receiver The [TagKey] tag to create the builder for - * @return The [IATagBuilder] instance - */ - operator fun TagKey.invoke(): IATagBuilder - { - return ATagBuilderPlatform.createTagBuilder(super.tag(this), this@ATagsProvider) - } - - /** - * Creates a new instance of [IATagBuilder] for the given [TagKey] - * and applies a lambda over it. - * - * @receiver The [TagKey] tag to create the builder for - * @param block the lamda to apply over the [TagKey] - */ - operator fun TagKey.invoke(block: IATagBuilder.() -> Unit) - { - this().apply(block) - } - - /** - * Creates a new instance of [IATagBuilder] for the given [TagKey] - * and adds an element of type [T] to it. - * - * @receiver The [TagKey] tag to create the builder for - * @param tag the element to add to the [TagKey] - */ - operator fun TagKey.plusAssign(tag: T) - { - this().add(tag) - } - - /** - * Creates a new instance of [IATagBuilder] for the given [TagKey] - * and adds an element of type [ResourceKey] to it. - * - * @receiver The [TagKey] tag to create the builder for - * @param tag the element to add to the [TagKey] - */ - operator fun TagKey.plusAssign(tag: ResourceKey) - { - this().add(tag) - } - - /** - * Creates a new instance of [IATagBuilder] for the given [TagKey] - * and adds an element of type [ResourceLocation] to it. - * - * @receiver The [TagKey] tag to create the builder for - * @param tag the element to add to the [TagKey] - */ - operator fun TagKey.plusAssign(tag: ResourceLocation) - { - this().add(tag) - } - - /** - * Creates a new instance of [IATagBuilder] for the given [TagKey] - * and adds a tag of type [TagKey] to it. - * - * @receiver The [TagKey] tag to create the builder for - * @param tag the element to add to the [TagKey] - */ - operator fun TagKey.plusAssign(tag: TagKey) - { - this().addTag(tag) - } - - /** - * Creates a new instance of [IATagBuilder] for the given [TagKey] - * and adds a list of elements of type [T] to it. - * - * @receiver The [TagKey] tag to create the builder for - * @param tags the list of elements to add to the [TagKey] - */ - operator fun TagKey.plusAssign(tags: List) - { - this().apply { tags.forEach(this::add) } - } - - /** - * Creates a new instance of [IATagBuilder] for the given [TagKey] - * and adds a list of elements of type [ResourceKey] to it. - * - * @receiver The [TagKey] tag to create the builder for - * @param tags the list of elements to add to the [TagKey] - */ - @JvmName("plusAssignKeyList") - operator fun TagKey.plusAssign(tags: List>) - { - this().apply { tags.forEach(this::add) } - } - - /** - * Creates a new instance of [IATagBuilder] for the given [TagKey] - * and adds a list of elements of type [ResourceLocation] to it. - * - * @receiver The [TagKey] tag to create the builder for - * @param tags the list of elements to add to the [TagKey] - */ - @JvmName("plusAssignLocList") - operator fun TagKey.plusAssign(tags: List) - { - this().apply { tags.forEach(this::add) } - } - - /** - * Creates a new instance of [IATagBuilder] for the given [TagKey] - * and adds a list of tags of type [TagKey] to it. - * - * @receiver The [TagKey] tag to create the builder for - * @param tags the list of elements to add to the [TagKey] - */ - @JvmName("plusAssignTagList") - operator fun TagKey.plusAssign(tags: List>) - { - this().apply { tags.forEach(this::addTag) } - } - - operator fun TagKey.timesAssign(tag: ResourceKey) - { - this().addOptional(tag) - } - - operator fun TagKey.timesAssign(tag: ResourceLocation) - { - this().addOptional(tag) - } - - operator fun TagKey.timesAssign(tag: TagKey) - { - this().addOptionalTag(tag) - } - - @JvmName("timesAssignKeyList") - operator fun TagKey.timesAssign(tags: List>) - { - this().apply { tags.forEach(this::addOptional) } - } - - @JvmName("timesAssignLocList") - operator fun TagKey.timesAssign(tags: List) - { - this().apply { tags.forEach(this::addOptional) } - } - - @JvmName("timesAssignTagList") - operator fun TagKey.timesAssign(tags: List>) - { - this().apply { tags.forEach(this::addOptionalTag) } - - } - - @Deprecated("Don't use this, use the platform agnostic version", ReplaceWith("tag()")) - override fun tag(tag: TagKey): TagAppender - { - throw IllegalStateException("Usage of vanilla \"tag\" method in an ArchieTagsProvider is prohibited. use the platform agnostic \"invoke\" operator instead") - } - - override fun getName(): String = format("${ - registryKey.location().path.split("/").last().split("_") - .joinToString(" ") { it.replaceFirstChar(Char::uppercase) } - } Tags") - - /** - * Extend this class to create [Block] tags in the "/blocks" tag directory. - */ - abstract class BlockTagsProvider( - output: PackOutput, - mod: Mod, - registriesFuture: CompletableFuture, - exitOnError: Boolean - ) : - ATagsProvider(output, mod, Registries.BLOCK, registriesFuture, exitOnError) - { - override fun reverseLookup(element: Block): ResourceKey - { - return reverseLookupInjected(element) - } - } - - /** - * Extend this class to create [Item] tags in the "/items" tag directory. - */ - abstract class ItemTagsProvider : - ATagsProvider - { - /** - * Construct an [ItemTagsProvider] tag provider **with** an associated [BlockTagsProvider] tag provider. - * - * @param output The [PackOutput] instance - * @param mod The architectury [Mod] instance - * @param registriesFuture The [HolderLookup.Provider] future - * @param blockTagsProvider The parent [BlockTagsProvider] - */ - constructor( - output: PackOutput, - mod: Mod, - registriesFuture: CompletableFuture, - blockTagsProvider: BlockTagsProvider?, - exitOnError: Boolean - - ) : super(output, mod, Registries.ITEM, registriesFuture, exitOnError) - { - this.blockTagBuilderProvider = - if (blockTagsProvider == null) null else Function, TagBuilder> { tag: TagKey -> - blockTagsProvider.getOrCreateRawBuilder( - tag - ) - } - } - - /** - * Construct an [ItemTagsProvider] tag provider **without** an associated [BlockTagsProvider] tag provider. - * - * @param output The [PackOutput] instance - * @param mod The architectury [Mod] instance - * @param registriesFuture The [HolderLookup.Provider] future - */ - constructor( - output: PackOutput, - mod: Mod, - registriesFuture: CompletableFuture, - exitOnError: Boolean - - ) : this(output, mod, registriesFuture, null, exitOnError) - - - private val blockTagBuilderProvider: Function, TagBuilder>? - - /** - * Copy the entries from a tag with the [Block] type into this item tag. - * - * - * The [ItemTagsProvider] tag provider must be constructed with an associated [BlockTagsProvider] tag provider to use this method. - * - * @param blockTag The block tag to copy from. - * @param itemTag The item tag to copy to. - */ - fun copy(blockTag: TagKey, itemTag: TagKey) - { - val blockTagBuilder = Objects.requireNonNull( - this.blockTagBuilderProvider, - "Pass Block tag provider via constructor to use copy" - )!!.apply(blockTag) - val itemTagBuilder: TagBuilder = this.getOrCreateRawBuilder(itemTag) - blockTagBuilder.build().forEach(Consumer { entry: TagEntry -> - itemTagBuilder.add( - entry - ) - }) - } - - override fun reverseLookup(element: Item): ResourceKey - { - return reverseLookupInjected(element) - } - } - - /** - * Extend this class to create [Fluid] tags in the "/fluids" tag directory. - */ - abstract class FluidTagsProvider( - output: PackOutput, - mod: Mod, - registriesFuture: CompletableFuture, - exitOnError: Boolean - ) : - ATagsProvider(output, mod, Registries.FLUID, registriesFuture, exitOnError) - { - override fun reverseLookup(element: Fluid): ResourceKey - { - return reverseLookupInjected(element) - } - } - - /** - * Extend this class to create [EntityType] tags in the "/entity_types" tag directory. - */ - abstract class EntityTypeTagsProvider( - output: PackOutput, - mod: Mod, - registriesFuture: CompletableFuture, - exitOnError: Boolean - ) : - ATagsProvider>(output, mod, Registries.ENTITY_TYPE, registriesFuture, exitOnError) - { - override fun reverseLookup(element: EntityType<*>): ResourceKey> - { - return reverseLookupInjected(element) - } - } - - /** - * Extend this class to create [Biome] tags in the "/worldgen/biome" tag directory. - * - * **Note:** Minecraft does not have a biome registry, so only [ResourceKey] and [TagKey] are allowed as tag entries - */ - abstract class BiomeTagsProvider( - output: PackOutput, - mod: Mod, - registriesFuture: CompletableFuture, exitOnError: Boolean - ) : - ATagsProvider(output, mod, Registries.BIOME, registriesFuture, exitOnError) - { - override fun reverseLookup(element: Biome): ResourceKey - { - throw UnsupportedOperationException("You can't look up a biome in the registry since Minecraft doesn't have a biome registry") - } - } - -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/IATagBuilder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/IATagBuilder.kt deleted file mode 100644 index 264b75798..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/IATagBuilder.kt +++ /dev/null @@ -1,155 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.tags - -import net.minecraft.data.tags.TagsProvider -import net.minecraft.resources.ResourceKey -import net.minecraft.resources.ResourceLocation -import net.minecraft.tags.BiomeTags -import net.minecraft.tags.BlockTags -import net.minecraft.tags.EntityTypeTags -import net.minecraft.tags.FluidTags -import net.minecraft.tags.ItemTags -import net.minecraft.tags.TagKey - - - -/** - * An extension to [TagsProvider.TagAppender] that provides additional functionality. - */ -interface IATagBuilder -{ - /** - * Set the value of the `replace` flag in a Tag. - * - * - * When set to true the tag will replace any existing tag entries. - * - * @return the [IATagBuilder] instance - */ - fun setReplace(replace: Boolean): IATagBuilder - - /** - * Set the value of the `replace` flag to true in a Tag. - * - * - * The tag will replace any existing tag entries. - * - * @return the [IATagBuilder] instance - */ - fun replace(): IATagBuilder - - /** - * Add an element to the tag. - * - * @return the [IATagBuilder] instance - */ - fun add(element: T): IATagBuilder - - /** - * Add multiple elements to the tag. - * - * @return the [IATagBuilder] instance - */ - @SafeVarargs - fun add(vararg elements: T): IATagBuilder - - /** - * Add an element to the tag. - * - * @return the [IATagBuilder] instance - */ - fun add(registryKey: ResourceKey): IATagBuilder - - /** - * Add a single element to the tag. - * - * @return the [IATagBuilder] instance - */ - fun add(id: ResourceLocation): IATagBuilder - - /** - * Add an optional [ResourceLocation] to the tag. - * - * @return the [IATagBuilder] instance - */ - fun addOptional(id: ResourceLocation): IATagBuilder - - /** - * Add an optional [ResourceKey] to the tag. - * - * @return the [IATagBuilder] instance - */ - fun addOptional(registryKey: ResourceKey): IATagBuilder - - /** Add multiple optional [ResourceLocation]s to the tag. */ - fun addOptionals(vararg ids: ResourceLocation): IATagBuilder - - /** Add multiple optional [ResourceKey]s to the tag. */ - fun addOptionals(vararg keys: ResourceKey): IATagBuilder - - /** - * Add all elements of [tag] to this tag, unconditionally (unlike [addTags], this does not - * require [tag] to be defined by a known builder or vanilla tag). - * - * @return the [IATagBuilder] instance - * @see BlockTags - * - * @see EntityTypeTags - * - * @see FluidTags - * - * @see BiomeTags - * - * @see ItemTags - */ - fun addTag(tag: TagKey): IATagBuilder - - /** - * Add another optional tag to this tag. - * - * @return the [IATagBuilder] instance - */ - fun addOptionalTag(id: ResourceLocation): IATagBuilder - - /** - * Add another optional tag to this tag. - * - * @return the [IATagBuilder] instance - */ - fun addOptionalTag(tag: TagKey): IATagBuilder - - /** Add multiple optional tags, by id, to this tag. */ - fun addOptionalTags(vararg ids: ResourceLocation): IATagBuilder - - /** Add multiple optional tags to this tag. */ - fun addOptionalTags(vararg tags: TagKey): IATagBuilder - - /** - * Add multiple elements to this tag. - * - * @return the [IATagBuilder] instance - */ - fun add(vararg ids: ResourceLocation): IATagBuilder - - /** - * Add multiple elements to this tag. - * - * @return the [IATagBuilder] instance - */ - @SafeVarargs - fun add(vararg registryKeys: ResourceKey): IATagBuilder - - /** - * Add multiple tags to this tag. - * - * @return the [IATagBuilder] instance - */ - fun addTags(vararg ids: ResourceLocation): IATagBuilder - - /** - * Add multiple tags to this tag. - * - * @return the [IATagBuilder] instance - */ - @SafeVarargs - fun addTags(vararg tagKeys: TagKey): IATagBuilder -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt deleted file mode 100644 index 7bc877bc8..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt +++ /dev/null @@ -1,396 +0,0 @@ -package net.kernelpanicsoft.archie.data.util - -import com.google.gson.* -import com.mojang.math.Axis -import com.mojang.math.Transformation -import net.minecraft.util.Mth -import net.minecraft.util.StringRepresentable -import org.joml.* -import java.lang.Math -import java.lang.reflect.Type -import kotlin.math.acos - -/** - * Math and JSON-parsing helpers for [Transformation] (translate/rotate/scale/rotate model - * transforms), including [Deserializer] for the "TRSR" JSON format used by custom item/block - * model transforms. - */ -object TransformationHelper -{ - /** Builds a quaternion from Euler [xyz] angles (in [degrees] if `true`, else radians). */ - fun quatFromXYZ(xyz: Vector3f, degrees: Boolean): Quaternionf - { - return quatFromXYZ(xyz.x, xyz.y, xyz.z, degrees) - } - - fun quatFromXYZ(xyz: FloatArray, degrees: Boolean): Quaternionf - { - return quatFromXYZ(xyz[0], xyz[1], xyz[2], degrees) - } - - fun quatFromXYZ(x: Float, y: Float, z: Float, degrees: Boolean): Quaternionf - { - val conversionFactor = if (degrees) Math.PI.toFloat() / 180 else 1f - return Quaternionf().rotationXYZ(x * conversionFactor, y * conversionFactor, z * conversionFactor) - } - - /** Builds a quaternion directly from `[x, y, z, w]` components in [values]. */ - fun makeQuaternion(values: FloatArray): Quaternionf - { - return Quaternionf(values[0], values[1], values[2], values[3]) - } - - /** Linearly interpolates from [from] to [to] by [progress] (0..1). */ - fun lerp(from: Vector3f?, to: Vector3f?, progress: Float): Vector3f - { - val res = Vector3f(from) - res.lerp(to, progress) - return res - } - - private const val THRESHOLD = 0.9995 - - /** Spherically interpolates from [v0] to [v1] by [t] (0..1), falling back to lerp when the inputs are nearly parallel. */ - fun slerp(v0: Quaternionfc, v1: Quaternionfc, t: Float): Quaternionf - { - // From https://en.wikipedia.org/w/index.php?title=Slerp&oldid=928959428 - // License: CC BY-SA 3.0 https://creativecommons.org/licenses/by-sa/3.0/ - - // Compute the cosine of the angle between the two vectors. - // If the dot product is negative, slerp won't take - // the shorter path. Note that v1 and -v1 are equivalent when - // the negation is applied to all four components. Fix by - // reversing one quaternion. - - var v1 = v1 - var dot = v0.x() * v1.x() + v0.y() * v1.y() + v0.z() * v1.z() + v0.w() * v1.w() - if (dot < 0.0f) - { - v1 = Quaternionf(-v1.x(), -v1.y(), -v1.z(), -v1.w()) - dot = -dot - } - - // If the inputs are too close for comfort, linearly interpolate - // and normalize the result. - if (dot > THRESHOLD) - { - val x = Mth.lerp(t, v0.x(), v1.x()) - val y = Mth.lerp(t, v0.y(), v1.y()) - val z = Mth.lerp(t, v0.z(), v1.z()) - val w = Mth.lerp(t, v0.w(), v1.w()) - return Quaternionf(x, y, z, w) - } - - // Since dot is in range [0, DOT_THRESHOLD], acos is safe - val angle01 = acos(dot.toDouble()).toFloat() - val angle0t = angle01 * t - val sin0t = Mth.sin(angle0t) - val sin01 = Mth.sin(angle01) - val sin1t = Mth.sin(angle01 - angle0t) - - val s1 = sin0t / sin01 - val s0 = sin1t / sin01 - - return Quaternionf( - s0 * v0.x() + s1 * v1.x(), - s0 * v0.y() + s1 * v1.y(), - s0 * v0.z() + s1 * v1.z(), - s0 * v0.w() + s1 * v1.w() - ) - } - - /** Interpolates every component of a [Transformation] (translation/scale linearly, rotations spherically) from [one] to [that] by [progress]. */ - fun slerp(one: Transformation, that: Transformation, progress: Float): Transformation - { - return Transformation( - lerp(one.translation, that.translation, progress), - slerp(one.leftRotation, that.leftRotation, progress), - lerp(one.scale, that.scale, progress), - slerp(one.rightRotation, that.rightRotation, progress) - ) - } - - /** Whether [v1] and [v2] are componentwise equal within [epsilon]. */ - fun epsilonEquals(v1: Vector4f, v2: Vector4f, epsilon: Float): Boolean - { - return Mth.abs(v1.x() - v2.x()) < epsilon && Mth.abs(v1.y() - v2.y()) < epsilon && Mth.abs( - v1.z() - v2.z() - ) < epsilon && Mth.abs(v1.w() - v2.w()) < epsilon - } - - /** Gson deserializer for the "TRSR" [Transformation] JSON format: `"identity"`, a raw 3x4 matrix, or an object with `translation`/`rotation`(`left_rotation`)/`scale`/`right_rotation`(`post-rotation`)/`origin`. */ - class Deserializer : JsonDeserializer - { - @Throws(JsonParseException::class) - override fun deserialize(json: JsonElement, typeOfT: Type, context: JsonDeserializationContext): Transformation - { - if (json.isJsonPrimitive && json.asJsonPrimitive.isString) - { - val transform = json.asString - if (transform == "identity") - { - return Transformation.identity() - } else - { - throw JsonParseException("TRSR: unknown default string: $transform") - } - } - if (json.isJsonArray) - { - // direct matrix array - return Transformation(parseMatrix(json)) - } - if (!json.isJsonObject) throw JsonParseException("TRSR: expected array or object, got: $json") - val obj = json.asJsonObject - val ret: Transformation - if (obj.has("matrix")) - { - // matrix as a sole key - ret = Transformation(parseMatrix(obj["matrix"])) - if (obj.entrySet().size > 1) - { - throw JsonParseException("TRSR: can't combine matrix and other keys") - } - return ret - } - var translation: Vector3f? = null - var leftRot: Quaternionf? = null - var scale: Vector3f? = null - var rightRot: Quaternionf? = null - // TODO: Default origin is opposing corner, due to a mistake. - // This should probably be replaced with center in future versions. - var origin: Vector3f? = - TransformOrigin.OPPOSING_CORNER.vector // TODO: Changing this to ORIGIN_CENTER breaks models, function content needs changing too -C - val elements: MutableSet = HashSet(obj.keySet()) - if (obj.has("translation")) - { - translation = Vector3f(parseFloatArray(obj["translation"], 3, "Translation")) - elements.remove("translation") - } - if (obj.has("rotation")) - { - leftRot = parseRotation(obj["rotation"]) - elements.remove("rotation") - } else if (obj.has("left_rotation")) - { - leftRot = parseRotation(obj["left_rotation"]) - elements.remove("left_rotation") - } - if (obj.has("scale")) - { - if (!obj["scale"].isJsonArray) - { - try - { - val s = obj["scale"].asNumber.toFloat() - scale = Vector3f(s, s, s) - } catch (ex: ClassCastException) - { - throw JsonParseException("TRSR scale: expected number or array, got: " + obj["scale"]) - } - } else - { - scale = Vector3f(parseFloatArray(obj["scale"], 3, "Scale")) - } - elements.remove("scale") - } - if (obj.has("right_rotation")) - { - rightRot = parseRotation(obj["right_rotation"]) - elements.remove("right_rotation") - } else if (obj.has("post-rotation")) - { - rightRot = parseRotation(obj["post-rotation"]) - elements.remove("post-rotation") - } - if (obj.has("origin")) - { - origin = parseOrigin(obj) - elements.remove("origin") - } - if (!elements.isEmpty()) throw JsonParseException( - "TRSR: can either have single 'matrix' key, or a combination of 'translation', 'rotation' OR 'left_rotation', 'scale', 'post-rotation' (legacy) OR 'right_rotation', 'origin'. Found: " + java.lang.String.join( - ", ", - elements - ) - ) - - val matrix = Transformation(translation, leftRot, scale, rightRot) - return matrix.applyOriginLocal(Vector3f(origin)) - } - - fun Transformation.isIdentityLocal(): Boolean - { - return this@isIdentityLocal == Transformation.identity() - } - - fun Transformation.applyOriginLocal(origin: Vector3f): Transformation - { - val transform: Transformation = this@applyOriginLocal - if (transform.isIdentityLocal()) return Transformation.identity() - - val ret = transform.matrix - val tmp = Matrix4f().translation(origin.x(), origin.y(), origin.z()) - tmp.mul(ret, ret) - tmp.translation(-origin.x(), -origin.y(), -origin.z()) - ret.mul(tmp) - return Transformation(ret) - } - - companion object - { - private fun parseOrigin(obj: JsonObject): Vector3f? - { - var origin: Vector3f? = null - - // Two types supported: string ("center", "corner", "opposing-corner") and array ([x, y, z]) - val originElement = obj["origin"] - if (originElement.isJsonArray) - { - origin = Vector3f(parseFloatArray(originElement, 3, "Origin")) - } else if (originElement.isJsonPrimitive) - { - val originString = originElement.asString - val originEnum = TransformOrigin.fromString(originString) - ?: throw JsonParseException("Origin: expected one of 'center', 'corner', 'opposing-corner'") - origin = originEnum.vector - } else - { - throw JsonParseException("Origin: expected an array or one of 'center', 'corner', 'opposing-corner'") - } - return origin - } - - fun parseMatrix(e: JsonElement): Matrix4f - { - if (!e.isJsonArray) throw JsonParseException("Matrix: expected an array, got: $e") - val m = e.asJsonArray - if (m.size() != 3) throw JsonParseException("Matrix: expected an array of length 3, got: " + m.size()) - val matrix = Matrix4f() - for (rowIdx in 0..2) - { - if (!m[rowIdx].isJsonArray) throw JsonParseException("Matrix row: expected an array, got: " + m[rowIdx]) - val r = m[rowIdx].asJsonArray - if (r.size() != 4) throw JsonParseException("Matrix row: expected an array of length 4, got: " + r.size()) - for (columnIdx in 0..3) - { - try - { - matrix[columnIdx, rowIdx] = r[columnIdx].asNumber.toFloat() - } catch (ex: ClassCastException) - { - throw JsonParseException("Matrix element: expected number, got: " + r[columnIdx]) - } - } - } - // JOML's unsafe matrix component setter does not recalculate these properties, so the matrix would stay marked as identity - matrix.determineProperties() - return matrix - } - - fun parseFloatArray(e: JsonElement, length: Int, prefix: String): FloatArray - { - if (!e.isJsonArray) throw JsonParseException("$prefix: expected an array, got: $e") - val t = e.asJsonArray - if (t.size() != length) throw JsonParseException(prefix + ": expected an array of length " + length + ", got: " + t.size()) - val ret = FloatArray(length) - for (i in 0 until length) - { - try - { - ret[i] = t[i].asNumber.toFloat() - } catch (ex: ClassCastException) - { - throw JsonParseException(prefix + " element: expected number, got: " + t[i]) - } - } - return ret - } - - fun parseAxisRotation(e: JsonElement): Quaternionf - { - if (!e.isJsonObject) throw JsonParseException("Axis rotation: object expected, got: $e") - val obj = e.asJsonObject - if (obj.entrySet().size != 1) throw JsonParseException("Axis rotation: expected single axis object, got: $e") - val entry = obj.entrySet().iterator().next() - val ret: Quaternionf - try - { - ret = if (entry.key == "x") - { - Axis.XP.rotationDegrees(entry.value.asNumber.toFloat()) - } else if (entry.key == "y") - { - Axis.YP.rotationDegrees(entry.value.asNumber.toFloat()) - } else if (entry.key == "z") - { - Axis.ZP.rotationDegrees(entry.value.asNumber.toFloat()) - } else throw JsonParseException("Axis rotation: expected single axis key, got: " + entry.key) - } catch (ex: ClassCastException) - { - throw JsonParseException("Axis rotation value: expected number, got: " + entry.value) - } - return ret - } - - fun parseRotation(e: JsonElement): Quaternionf - { - if (e.isJsonArray) - { - if (e.asJsonArray[0].isJsonObject) - { - val ret = Quaternionf() - for (a in e.asJsonArray) - { - ret.mul(parseAxisRotation(a)) - } - return ret - } else if (e.isJsonArray) - { - val array = e.asJsonArray - return if (array.size() == 3) //Vanilla rotation - quatFromXYZ(parseFloatArray(e, 3, "Rotation"), true) - else // quaternion - makeQuaternion(parseFloatArray(e, 4, "Rotation")) - } else throw JsonParseException("Rotation: expected array or object, got: $e") - } else if (e.isJsonObject) - { - return parseAxisRotation(e) - } else throw JsonParseException("Rotation: expected array or object, got: $e") - } - } - } - - /** Named reference points a [Deserializer]-parsed transform's rotation/scale can pivot around. */ - enum class TransformOrigin(val vector: Vector3f, private val serialName: String) : StringRepresentable - { - CENTER(Vector3f(.5f, .5f, .5f), "center"), - CORNER(Vector3f(), "corner"), - OPPOSING_CORNER(Vector3f(1f, 1f, 1f), "opposing-corner"); - - override fun getSerializedName(): String - { - return serialName - } - - companion object - { - fun fromString(originName: String): TransformOrigin? - { - if (CENTER.serializedName == originName) - { - return CENTER - } - if (CORNER.serializedName == originName) - { - return CORNER - } - if (OPPOSING_CORNER.serializedName == originName) - { - return OPPOSING_CORNER - } - return null - } - } - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt deleted file mode 100644 index 6556c99e8..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.kernelpanicsoft.archie.events - -import dev.architectury.event.Event - -/** - * A simpler alternative to [AEventObject] for wrapping an Architectury [event] that isn't - * scoped to a particular [dev.architectury.platform.Mod] and doesn't need a - * [AEvents.HandlerConstructor]. - * - * @param T The Architectury handler/listener type expected by [event]. - */ -abstract class ABasicEventObject() -{ - /** The underlying Architectury event this wrapper registers [handler] with. */ - abstract val event: Event - - /** The listener registered with [event] by [init]. */ - abstract val handler: T - - /** Registers [handler] with [event]. Not idempotent; call once during initialization. */ - fun init() - { - event.register(handler) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt deleted file mode 100644 index 5ae408e7f..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt +++ /dev/null @@ -1,47 +0,0 @@ -package net.kernelpanicsoft.archie.events - -import dev.architectury.event.Event -import dev.architectury.platform.Mod - -/** - * Base class for Archie's mod-scoped Architectury event wrappers, such as - * [AEvents.GatherDataHandler] and [AEvents.RegisterGameTestHandler]. - * - * Subclasses wire together an Architectury [event], the [handlerConstructor] that builds a - * [mod]-scoped [H] from a [T] callback, and the [handler] logic itself, then call [init] once - * (idempotently, thread-safely) to register with the underlying event. - * - * @param T The event payload/receiver type passed to [handler]. - * @param H The [AEvents.Handler] type produced for [mod]. - * @param C The [AEvents.HandlerConstructor] that builds an [H]. - * @param mod The [Mod] this event object is scoped to. - */ -abstract class AEventObject, C : AEvents.HandlerConstructor>(val mod: Mod) -{ - /** The underlying Architectury event this wrapper registers a handler with. */ - abstract val event: Event - - @Volatile - private var initialized: Boolean = false - - /** Builds a [mod]-scoped [H] from the [handler] callback. */ - abstract val handlerConstructor: C - - /** The callback invoked when [event] fires for [mod]. */ - abstract fun T.handler() - - /** - * Registers [handler] with [event] via [handlerConstructor]. Safe to call multiple times; - * only the first call has any effect. - */ - fun init() - { - if (initialized) return - synchronized(this) - { - if (initialized) return - event.register(handlerConstructor.create(mod) { handler() }) - initialized = true - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEvents.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEvents.kt deleted file mode 100644 index 2ca7be7da..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEvents.kt +++ /dev/null @@ -1,185 +0,0 @@ -package net.kernelpanicsoft.archie.events - -import net.kernelpanicsoft.archie.data.ADataGenerator -import net.kernelpanicsoft.archie.gametest.AGameTestPlatform -import dev.architectury.event.Event -import dev.architectury.event.EventFactory -import dev.architectury.event.EventResult -import dev.architectury.platform.Mod -import dev.architectury.platform.Platform -import dev.architectury.utils.Env -import net.kernelpanicsoft.archie.gametest.AGameTestSide - -/** - * Archie's central, mod-scoped event registry, built on top of Architectury's event system. - * - * A downstream mod opts in with `AEvents += MOD` (its own [Mod] descriptor), then listens for - * [GATHER_DATA] and/or [REGISTER_GAME_TEST] via the corresponding handler's - * [HandlerConstructor.create]. Handlers are mod-scoped: [GatherDataHandler] and - * [RegisterGameTestHandler] both check the invoking [Mod] and no-op (via - * [EventResult.pass]) for any mod other than the one they were created for. - */ -object AEvents -{ - /** Fired during datagen runs; handlers should gate by owning [Mod]. */ - val GATHER_DATA: Event = EventFactory.createEventResult() - - /** Fired during gametest registration runs; handlers should register test classes per [Mod]. */ - val REGISTER_GAME_TEST: Event = EventFactory.createEventResult() - - private val mods: MutableList = mutableListOf() - - /** Mods that opted into Archie event plumbing via `AEvents += MOD`. */ - val MODS: List - get() = mods - - fun register(mod: Mod) - { - mods.add(mod) - } - - operator fun plusAssign(mod: Mod) = register(mod) - - /** Marker for a mod-scoped Architectury event listener created by a [HandlerConstructor]. */ - interface Handler - - /** Builds a mod-scoped [H] whose body invokes `block` on the event's [T] payload. */ - fun interface HandlerConstructor> - { - /** Creates an [H] for [mod] that runs [block] against the [T] payload when invoked. */ - fun create(mod: Mod, block: T.() -> Unit): H - } - - /** - * Handler for [GATHER_DATA]. Implementations are produced via [HandlerConstructor.create] - * and forward to the registered `block` only when the firing [ADataGenerator.mod] matches - * the [Mod] the handler was created for. - */ - interface GatherDataHandler : Handler - { - operator fun invoke(dataGenerator: ADataGenerator): EventResult - - companion object : HandlerConstructor - { - override fun create(mod: Mod, block: ADataGenerator.() -> Unit): GatherDataHandler - { - return GatherDataHandlerImpl(mod, block) - } - - class GatherDataHandlerImpl internal constructor( - private val mod: Mod, - private val gatherData: ADataGenerator.() -> Unit - ) : - GatherDataHandler - { - override operator fun invoke(dataGenerator: ADataGenerator): EventResult - { - if (this.mod != dataGenerator.mod) - return EventResult.pass() - dataGenerator.gatherData() - return EventResult.interruptDefault() - } - } - } - } - - /** - * DSL receiver passed to [REGISTER_GAME_TEST] listeners for declaring gametest classes. - * - * Classes registered via [server]/[client] are only collected on the matching - * [AGameTestPlatform] [AGameTestSide], unless [all] is `true`; classes registered via - * [common] are always collected. Collected classes are handed off to - * [AGameTestPlatform.register]. - * - * @param all When `true`, [server] and [client] blocks are collected on both sides. - */ - class ArchieGameTestBuilder(private val all: Boolean = false) - { - /** All classes collected so far across [server], [client], and [common] blocks. */ - val classes: MutableList> = mutableListOf() - - /** Declares gametest classes that should only be registered on the server side. */ - fun server(block: Environment.Server.() -> Unit) - { - Environment.Server(all).apply(block).also { classes.addAll(it.classes) } - } - - /** Declares gametest classes that should only be registered on the client side. */ - fun client(block: Environment.Client.() -> Unit) - { - Environment.Client(all).apply(block).also { classes.addAll(it.classes) } - } - - /** Declares gametest classes that should always be registered, regardless of side. */ - fun common(block: Environment.Common.() -> Unit) - { - Environment.Common().apply(block).also { classes.addAll(it.classes) } - } - - /** Scopes [register] calls to classes that should be collected only when [predicate] holds. */ - sealed class Environment(private val predicate: () -> Boolean) - { - /** Classes registered in this environment scope. */ - val classes: MutableList> = mutableListOf() - class Server(all: Boolean) : Environment({ all || AGameTestPlatform.side == AGameTestSide.SERVER }) - class Client(all: Boolean) : Environment({ all || AGameTestPlatform.side == AGameTestSide.CLIENT }) - class Common : Environment({ true }) - - /** Adds [clazz] to [classes] if this environment's [predicate] currently holds. */ - fun register(clazz: Class) - { - if (!predicate()) return - classes.add(clazz) - } - - /** Reified convenience for [register] using [T]'s [Class]. */ - inline fun register() - { - register(T::class.java) - } - } - } - - /** - * Handler for [REGISTER_GAME_TEST]. Implementations are produced via - * [HandlerConstructor.create] and forward to the registered `block` only when the firing - * [mod] matches the [Mod] the handler was created for, collecting declared test classes via - * an [ArchieGameTestBuilder] and registering each with [AGameTestPlatform.register]. - */ - interface RegisterGameTestHandler : Handler - { - operator fun invoke(mod: Mod): EventResult - - companion object : HandlerConstructor - { - override fun create( - mod: Mod, - block: ArchieGameTestBuilder.() -> Unit - ): RegisterGameTestHandler - { - return RegisterGameTestHandlerImpl(mod, block) - } - - class RegisterGameTestHandlerImpl internal constructor( - private val mod: Mod, - private val registerGameTests: ArchieGameTestBuilder.() -> Unit - ) : RegisterGameTestHandler - { - override operator fun invoke(mod: Mod): EventResult - { - if (this.mod != mod) - return EventResult.pass() - - ArchieGameTestBuilder().apply(registerGameTests).classes.forEach { clazz -> - AGameTestPlatform.register(clazz, mod) - } - return EventResult.interruptDefault() - } - - - } - } - } - - -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt deleted file mode 100644 index 494dfebe5..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt +++ /dev/null @@ -1,1653 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import com.mojang.realmsclient.RealmsMainScreen -import dev.architectury.platform.Mod -import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.test.StandardTestDispatcher -import kotlinx.coroutines.test.TestCoroutineScheduler -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.gui.ComposeIdleAware -import net.kernelpanicsoft.archie.gui.ComposeTestClockOverride -import net.kernelpanicsoft.archie.gui.LayerManagerProvider -import net.kernelpanicsoft.archie.util.setReflection -import net.minecraft.SharedConstants -import net.minecraft.client.Minecraft -import net.minecraft.client.Screenshot -import net.minecraft.client.gui.components.AbstractWidget -import net.minecraft.client.gui.screens.GenericMessageScreen -import net.minecraft.client.gui.screens.Screen -import net.minecraft.client.gui.screens.TitleScreen -import net.minecraft.client.gui.screens.multiplayer.JoinMultiplayerScreen -import net.minecraft.client.gui.screens.worldselection.CreateWorldScreen -import net.minecraft.client.gui.screens.worldselection.WorldCreationUiState -import net.minecraft.core.BlockPos -import net.minecraft.core.Holder -import net.minecraft.core.registries.Registries -import net.minecraft.network.chat.Component -import net.minecraft.network.chat.contents.TranslatableContents -import net.minecraft.server.MinecraftServer -import net.minecraft.world.level.block.entity.BlockEntity -import net.minecraft.world.level.levelgen.presets.WorldPreset -import net.minecraft.world.level.levelgen.presets.WorldPresets -import org.apache.commons.lang3.function.FailableConsumer -import org.apache.commons.lang3.function.FailableFunction -import org.joml.Vector2i -import org.lwjgl.glfw.GLFW -import java.nio.file.Path -import java.util.* -import java.util.concurrent.CountDownLatch -import java.util.concurrent.TimeUnit -import java.util.concurrent.ConcurrentHashMap -import java.util.function.Consumer -import java.util.function.Predicate -import java.util.function.Supplier -import java.lang.reflect.Modifier -import java.nio.file.Files -import kotlin.reflect.full.primaryConstructor - -private const val DEFAULT_TICK_MILLIS = 50L -private const val CLIENT_EXEC_TIMEOUT_SECONDS = 10L -private const val WORLD_BUILDER_EXEC_TIMEOUT_SECONDS = 300L -private const val SCREEN_SET_TIMEOUT_TICKS = 40 -private const val COMPOSE_IDLE_TIMEOUT_TICKS = 20 -private const val COMPOSE_IDLE_CONSECUTIVE_CHECKS = 4 - -private object DedicatedServerLifecycleTracker { - private val activeServers = ConcurrentHashMap.newKeySet() - - fun register(server: Any) { - activeServers += server - } - - fun unregister(server: Any) { - activeServers -= server - } - - fun stopAllLeakedServers() { - activeServers.toList().forEach { server -> - runCatching { - ADedicatedServerPlatform.stop(server) - }.onFailure { error -> - Archie.LOGGER.warn("Failed stopping leaked dedicated server: ${error.message}") - } - } - - val deadlineNanos = System.nanoTime() + TimeUnit.SECONDS.toNanos(10) - while (System.nanoTime() < deadlineNanos) { - val stillAlive = activeServers.filter { server -> - runCatching { ADedicatedServerPlatform.isAlive(server) }.getOrDefault(false) - } - if (stillAlive.isEmpty()) break - Thread.sleep(50L) - } - - activeServers.removeIf { server -> - runCatching { !ADedicatedServerPlatform.isAlive(server) }.getOrDefault(true) - } - } -} - -/** - * Vanilla's dedicated-server bootstrap (`Main.main`) always reads `server.properties`/`eula.txt` - * from the process's current working directory, regardless of the `--universe` argument - so - * per-test isolation there isn't possible without spawning a separate process. This captures - * whatever was there before the first override (once) and restores it once the harness run - * finishes, so the repo's own working directory isn't left permanently polluted with test - * artifacts. Deliberately not a blocking lock: a leaked dedicated server (one whose context is - * never explicitly closed) is already recovered via [DedicatedServerLifecycleTracker], and a - * blocking acquire here that's never released on that path would deadlock every later - * dedicated-server test instead of just risking a rare overlapping-write race. - */ -private object CwdBootstrapFileGuard { - private var captured = false - private var originalServerProperties: ByteArray? = null - private var originalEula: ByteArray? = null - - @Synchronized - fun writeOverride(properties: Properties) { - val cwd = Path.of(".") - val propertiesPath = cwd.resolve("server.properties") - val eulaPath = cwd.resolve("eula.txt") - - if (!captured) { - originalServerProperties = if (Files.exists(propertiesPath)) Files.readAllBytes(propertiesPath) else null - originalEula = if (Files.exists(eulaPath)) Files.readAllBytes(eulaPath) else null - captured = true - } - - Files.newBufferedWriter(propertiesPath).use { writer -> - properties.store(writer, "Archie GameTest dedicated server properties") - } - Files.newBufferedWriter(eulaPath).use { writer -> - writer.write("eula=true") - writer.newLine() - } - } - - @Synchronized - fun restoreIfCaptured() { - if (!captured) return - runCatching { - val cwd = Path.of(".") - val propertiesPath = cwd.resolve("server.properties") - val eulaPath = cwd.resolve("eula.txt") - originalServerProperties?.let { Files.write(propertiesPath, it) } ?: Files.deleteIfExists(propertiesPath) - originalEula?.let { Files.write(eulaPath, it) } ?: Files.deleteIfExists(eulaPath) - }.onFailure { error -> - Archie.LOGGER.warn("Failed to restore original server.properties/eula.txt in working directory: ${error.message}") - } - captured = false - originalServerProperties = null - originalEula = null - } -} - -/** Marks a test method to be executed by the Archie client GameTest backport harness. */ -@Target(AnnotationTarget.FUNCTION) -@Retention(AnnotationRetention.RUNTIME) -annotation class ClientGameTest(val name: String = "") - -data class TestScreenshotOptions( - val name: String, -) - -data class TestScreenshotComparisonOptions( - val templateImage: String, - val screenshot: TestScreenshotOptions = TestScreenshotOptions(name = "client-gametest"), -) - -data class AClientGameTestFailure( - val testId: String, - val rootCause: String, -) - -/** - * Synthetic input dispatch for a client GameTest's active [Screen], bypassing GLFW entirely - * (each call feeds the corresponding `mouseClicked`/`keyPressed`/... callback directly). Reached - * via [ClientGameTestContext.getInput] - most tests should prefer the higher-level, node-scoped - * [TestNodeScope] helpers (`click()`, `hover()`, `type()`, `scroll()`) instead of calling this - * directly, since those also resolve the target node's on-screen position first. - */ -interface TestInput { - fun click(x: Double, y: Double, button: Int = 0) - fun keyPress(keyCode: Int, scanCode: Int = 0, modifiers: Int = 0) - fun holdKey(keyCode: Int, scanCode: Int = 0, modifiers: Int = 0) - fun releaseKey(keyCode: Int, scanCode: Int = 0, modifiers: Int = 0) - fun pressKey(keyCode: Int, scanCode: Int = 0, modifiers: Int = 0) - fun holdMouse(button: Int = 0) - fun releaseMouse(button: Int = 0) - fun pressMouse(button: Int = 0) - fun holdControl() - fun releaseControl() - fun holdShift() - fun releaseShift() - fun holdAlt() - fun releaseAlt() - fun charTyped(char: Char, modifiers: Int = 0) - fun typeChars(value: String) - fun scroll(x: Double = 0.0, y: Double = 1.0) - fun setCursor(x: Double, y: Double) - fun moveCursor(deltaX: Double, deltaY: Double) - fun clearInputs() -} - -/** - * Builds a test world from [ClientGameTestContext.worldBuilder]: either a singleplayer world via - * [create]/[withSingleplayer], or a same-JVM dedicated server via [createServer]/[withServer] for - * tests that need a real client↔server boundary (e.g. exercising [ConfigSpec.Server] sync or - * other multiplayer-only codepaths) rather than the integrated server a singleplayer world uses. - */ -@Suppress("unused") -interface TestWorldBuilder { - fun setUseConsistentSettings(useConsistentSettings: Boolean): TestWorldBuilder - - fun adjustSettings(settingsAdjuster: Consumer): TestWorldBuilder - - fun create(): TestSingleplayerContext - - fun withSingleplayer(callback: TestSingleplayerContext.() -> Unit) { - val context = create() - try { - context.callback() - } finally { - context.close() - } - } - - fun createServer(serverProperties: Properties): TestDedicatedServerContext - - fun withServer(serverProperties: Properties, callback: TestDedicatedServerContext.() -> Unit) { - val context = createServer(serverProperties) - try { - context.callback() - } finally { - context.close() - } - } -} - -/** A running singleplayer test world created by [TestWorldBuilder.create], with server-side access via [server]. Closing (see [TestWorldBuilder.withSingleplayer]) disconnects and waits for the world to unload. */ -@Suppress("unused") -interface TestSingleplayerContext { - val clientContext: ClientGameTestContext - val saveDirectory: Path - val clientWorld: TestClientWorldContext - val server: TestServerContext - - fun close() -} - -/** A running same-JVM dedicated server created by [TestWorldBuilder.createServer]. [connect] joins the client to it; closing (see [TestWorldBuilder.withServer]) stops the server process. */ -@Suppress("unused") -interface TestDedicatedServerContext { - val clientContext: ClientGameTestContext - - val serverDirectory: Path - - fun connect(): TestServerConnection - - fun withConnection(callback: TestServerConnection.() -> Unit) { - val connection = connect() - try { - connection.callback() - } finally { - runCatching { connection.disconnect() } - .onFailure { error -> - Archie.LOGGER.warn("Failed to disconnect server connection cleanly: ${error.message}") - } - } - } - - fun close() -} - -/** The client's connection to a [TestDedicatedServerContext], returned by [TestDedicatedServerContext.connect]. */ -@Suppress("unused") -interface TestServerConnection { - val clientContext: ClientGameTestContext - val clientWorld: TestClientWorldContext - - fun disconnect() -} - -/** Client-side world-loading waits (chunk download/render), independent of world type - available on both [TestSingleplayerContext.clientWorld] and [TestServerConnection.clientWorld]. */ -@Suppress("unused") -interface TestClientWorldContext { - fun waitForChunksDownload(timeout: Int = ClientGameTestContext.DEFAULT_TIMEOUT): Int - - fun waitForChunksRender(waitForDownload: Boolean = true, timeout: Int = ClientGameTestContext.DEFAULT_TIMEOUT): Int -} - -/** Server-side access for a [TestSingleplayerContext]'s integrated server: run commands or arbitrary code on the server thread. */ -@Suppress("unused") -interface TestServerContext { - fun runCommand(command: String) - - fun runOnServer(action: FailableConsumer) - - fun computeOnServer(function: FailableFunction): T - - fun runOnServer(action: (MinecraftServer) -> Unit) - - fun computeOnServer(function: (MinecraftServer) -> T): T -} - -/** Minimal assertion/report API passed to client harness test methods. */ -@Suppress("unused") -interface ClientGameTestContext { - val testId: String - - companion object { - const val NO_TIMEOUT: Int = -1 - const val DEFAULT_TIMEOUT: Int = 10 * SharedConstants.TICKS_PER_SECOND - } - - fun assertTrue(condition: Boolean, message: () -> String) - - fun assertEquals(expected: Any?, actual: Any?, message: () -> String = { "Expected <$expected>, got <$actual>" }) - - fun fail(message: String): Nothing - - fun assertScreenshotContains(templateImage: String): Vector2i = - assertScreenshotContains(TestScreenshotComparisonOptions(templateImage = templateImage)) - - fun assertScreenshotContains(options: TestScreenshotComparisonOptions): Vector2i - - fun assertScreenshotEquals(templateImage: String) = - assertScreenshotEquals(TestScreenshotComparisonOptions(templateImage = templateImage)) - - fun assertScreenshotEquals(options: TestScreenshotComparisonOptions) - - fun clickScreenButton(translationKey: String) - - fun computeOnClient(function: (Minecraft) -> T): T - - fun computeOnClient(function: FailableFunction): T - - fun getInput(): TestInput - - fun restoreDefaultGameOptions() - - fun runOnClient(action: (Minecraft) -> Unit) - - fun runOnClient(action: FailableConsumer) - - fun setScreen(screen: Supplier) - - fun takeScreenshot(name: String): Path = takeScreenshot(TestScreenshotOptions(name)) - - fun takeScreenshot(options: TestScreenshotOptions): Path - - fun tryClickScreenButton(translationKey: String): Boolean - - fun waitFor(predicate: Predicate): Int = - waitFor(predicate, DEFAULT_TIMEOUT) - - fun waitFor(predicate: Predicate, timeout: Int): Int - - fun waitForScreen(screenClass: Class?): Int - fun waitForScreen(screenClass: Class, block: ComposeScreenTestContext.() -> Unit): Int - - fun waitTick() - - fun waitTicks(ticks: Int) - - /** - * Waits for asynchronous Compose recomposition triggered by a prior state mutation (e.g. a - * click, hover, keypress, or typed character) to settle, so a following assertion doesn't - * race the still-in-flight visual/layout update. No-ops for screens that aren't - * Compose-driven. Already used internally by [takeScreenshot]; call this explicitly after - * [TestInput]/[TestNodeScope] actions that aren't immediately followed by a screenshot. - */ - fun waitForComposeIdle() - - fun worldBuilder(): TestWorldBuilder - - fun withWorld(callback: TestWorldBuilder.() -> Unit) { - worldBuilder().apply(callback) - } -} - -internal class DefaultClientGameTestContext( - override val testId: String, -) : ClientGameTestContext { - companion object { - private val optionSnapshotLock = Any() - - @Volatile - private var defaultOptionValues: Map? = null - - @Volatile - private var deterministicOptionsInitialized: Boolean = false - } - - internal fun describeScreen(screen: Screen?): String = - screen?.let { "${it::class.java.name}@${System.identityHashCode(it)}" } ?: "null" - - private fun timeoutMessage(action: String, timeoutSeconds: Long, client: Minecraft): String { - val caller = Thread.currentThread() - return "Timed out waiting for client thread execution " + - "(testId=$testId, action=$action, timeoutSeconds=$timeoutSeconds, " + - "callerThread=${caller.name}, callerState=${caller.state}, " + - "clientScreen=${describeScreen(client.screen)}, levelLoaded=${client.level != null})" - } - - private val input: TestInput = object : TestInput { - private val keysDown = mutableSetOf>() - private val mouseButtonsDown = mutableSetOf() - private var cursorX: Double? = null - private var cursorY: Double? = null - - private fun currentCursorX(screen: Screen): Double = cursorX ?: (screen.width / 2.0) - - private fun currentCursorY(screen: Screen): Double = cursorY ?: (screen.height / 2.0) - - override fun click(x: Double, y: Double, button: Int) { - runOnClient { client -> - val screen = client.screen ?: return@runOnClient - cursorX = x - cursorY = y - warpRealCursor(client, x, y) - screen.mouseClicked(x, y, button) - screen.mouseReleased(x, y, button) - } - } - - override fun keyPress(keyCode: Int, scanCode: Int, modifiers: Int) { - pressKey(keyCode, scanCode, modifiers) - } - - override fun holdKey(keyCode: Int, scanCode: Int, modifiers: Int) { - val key = keyCode to scanCode - if (!keysDown.add(key)) return - runOnClient { client -> - client.screen?.keyPressed(keyCode, scanCode, modifiers) - } - } - - override fun releaseKey(keyCode: Int, scanCode: Int, modifiers: Int) { - val key = keyCode to scanCode - if (!keysDown.remove(key)) return - runOnClient { client -> - client.screen?.keyReleased(keyCode, scanCode, modifiers) - } - } - - override fun pressKey(keyCode: Int, scanCode: Int, modifiers: Int) { - holdKey(keyCode, scanCode, modifiers) - waitTick() - releaseKey(keyCode, scanCode, modifiers) - } - - override fun holdMouse(button: Int) { - if (!mouseButtonsDown.add(button)) return - runOnClient { client -> - val screen = client.screen ?: return@runOnClient - val x = currentCursorX(screen) - val y = currentCursorY(screen) - screen.mouseClicked(x, y, button) - } - } - - override fun releaseMouse(button: Int) { - if (!mouseButtonsDown.remove(button)) return - runOnClient { client -> - val screen = client.screen ?: return@runOnClient - val x = currentCursorX(screen) - val y = currentCursorY(screen) - screen.mouseReleased(x, y, button) - } - } - - override fun pressMouse(button: Int) { - holdMouse(button) - waitTick() - releaseMouse(button) - } - - override fun holdControl() { - holdKey(GLFW.GLFW_KEY_LEFT_CONTROL) - } - - override fun releaseControl() { - releaseKey(GLFW.GLFW_KEY_LEFT_CONTROL) - } - - override fun holdShift() { - holdKey(GLFW.GLFW_KEY_LEFT_SHIFT) - } - - override fun releaseShift() { - releaseKey(GLFW.GLFW_KEY_LEFT_SHIFT) - } - - override fun holdAlt() { - holdKey(GLFW.GLFW_KEY_LEFT_ALT) - } - - override fun releaseAlt() { - releaseKey(GLFW.GLFW_KEY_LEFT_ALT) - } - - override fun charTyped(char: Char, modifiers: Int) { - runOnClient { client -> - client.screen?.charTyped(char, modifiers) - } - } - - override fun typeChars(value: String) { - value.forEach { - charTyped(it) - waitForComposeIdle() - } - } - - override fun scroll(x: Double, y: Double) { - runOnClient { client -> - val screen = client.screen ?: return@runOnClient - val sx = currentCursorX(screen) - val sy = currentCursorY(screen) - screen.mouseScrolled(sx, sy, x, y) - } - } - - override fun setCursor(x: Double, y: Double) { - cursorX = x - cursorY = y - runOnClient { client -> - warpRealCursor(client, x, y) - client.screen?.mouseMoved(x, y) - } - } - - override fun moveCursor(deltaX: Double, deltaY: Double) { - runOnClient { client -> - val screen = client.screen ?: return@runOnClient - val nextX = currentCursorX(screen) + deltaX - val nextY = currentCursorY(screen) + deltaY - cursorX = nextX - cursorY = nextY - warpRealCursor(client, nextX, nextY) - screen.mouseMoved(nextX, nextY) - } - } - - override fun clearInputs() { - keysDown.toList().forEach { (keyCode, scanCode) -> - releaseKey(keyCode, scanCode) - } - mouseButtonsDown.toList().forEach { button -> - releaseMouse(button) - } - cursorX = null - cursorY = null - } - } - - override fun assertTrue(condition: Boolean, message: () -> String) { - if (!condition) fail(message()) - } - - override fun assertEquals(expected: Any?, actual: Any?, message: () -> String) { - if (expected != actual) fail(message()) - } - - override fun fail(message: String): Nothing = throw IllegalStateException(message) - - override fun assertScreenshotContains(options: TestScreenshotComparisonOptions): Vector2i { - val templatePath = ScreenshotManager.resolveTemplate(options.templateImage) - if (!templatePath.toFile().exists()) { - fail("Screenshot template not found: ${options.templateImage} (resolved to: $templatePath)") - } - - val capturePath = takeScreenshot(options.screenshot) - val matchPos = ScreenshotComparer.findInImage(templatePath.toFile(), capturePath.toFile()) - return matchPos ?: fail("Template image '${options.templateImage}' not found in screenshot") - } - - override fun assertScreenshotEquals(options: TestScreenshotComparisonOptions) { - val templatePath = ScreenshotManager.resolveTemplate(options.templateImage) - if (!templatePath.toFile().exists()) { - fail("Screenshot template not found: ${options.templateImage} (resolved to: $templatePath)") - } - - val capturePath = takeScreenshot(options.screenshot) - val equal = ScreenshotComparer.imagesEqual(templatePath.toFile(), capturePath.toFile()) - if (!equal) { - fail("Screenshot does not match template '${options.templateImage}' (expected: $templatePath, actual: $capturePath)") - } - } - - override fun clickScreenButton(translationKey: String) { - if (!tryClickScreenButton(translationKey)) { - fail("No screen button found for translation key '$translationKey'") - } - } - - override fun computeOnClient(function: (Minecraft) -> T): T { - return computeOnClient("anonymous-client-action", CLIENT_EXEC_TIMEOUT_SECONDS, function) - } - - internal fun computeOnClient(action: String, timeoutSeconds: Long, function: (Minecraft) -> T): T { - val client = Minecraft.getInstance() - return if (client.isSameThread) { - ensureDeterministicGameOptionsInitialized(client) - function(client) - } else { - var value: T? = null - var throwable: Throwable? = null - val latch = CountDownLatch(1) - client.execute { - runCatching { - ensureDeterministicGameOptionsInitialized(client) - function(client) - } - .onSuccess { value = it } - .onFailure { throwable = it } - latch.countDown() - } - - if (!latch.await(timeoutSeconds, TimeUnit.SECONDS)) { - fail(timeoutMessage(action, timeoutSeconds, client)) - } - - throwable?.let { throw it } - @Suppress("UNCHECKED_CAST") - value as T - } - } - - override fun computeOnClient(function: FailableFunction): T { - return computeOnClient("failable-client-function", CLIENT_EXEC_TIMEOUT_SECONDS) { client -> function.apply(client) } - } - - override fun getInput(): TestInput = input - - override fun restoreDefaultGameOptions() { - runOnClient { client -> - restoreCapturedGameOptions(client) - client.options.save() - } - } - - private fun initializeDeterministicGameOptions(client: Minecraft) { - val options = client.options - if (defaultOptionValues == null) { - synchronized(optionSnapshotLock) { - if (defaultOptionValues == null) { - defaultOptionValues = captureOptionValues(options) - } - } - } - - applyDeterministicTweaks(options) - disablePauseOnLostFocus(options) - options.save() - } - - /** - * `pauseOnLostFocus` is a raw boolean field on [net.minecraft.client.Options], not an - * `OptionInstance`/`SimpleOption` wrapper, so it's invisible to [applyDeterministicTweaks]'s - * [isOptionLike]-filtered reflection loop. Client GameTest windows routinely run without OS - * focus (headless CI, parallel loader:side invocations, a terminal/IDE stealing focus), and - * vanilla pauses world ticking whenever the window isn't focused - silently stalling every - * waitTick()/waitTicks() call - so it needs disabling separately. - */ - private fun disablePauseOnLostFocus(options: Any) { - runCatching { - options.setReflection("pauseOnLostFocus", false) - } - } - - private fun ensureDeterministicGameOptionsInitialized(client: Minecraft) { - if (deterministicOptionsInitialized) return - - synchronized(optionSnapshotLock) { - if (deterministicOptionsInitialized) return - initializeDeterministicGameOptions(client) - deterministicOptionsInitialized = true - } - } - - private fun restoreCapturedGameOptions(client: Minecraft) { - val options = client.options - val snapshot = defaultOptionValues ?: synchronized(optionSnapshotLock) { - defaultOptionValues ?: captureOptionValues(options).also { defaultOptionValues = it } - } - restoreOptionValues(options, snapshot) - } - - private fun captureOptionValues(options: Any): Map { - val captured = mutableMapOf() - for (field in options.javaClass.declaredFields) { - if (Modifier.isStatic(field.modifiers)) continue - field.isAccessible = true - val option = runCatching { field.get(options) }.getOrNull() ?: continue - if (!isOptionLike(option)) continue - captured[field.name] = readOptionValue(option) - } - return captured - } - - private fun restoreOptionValues(options: Any, snapshot: Map) { - for ((fieldName, value) in snapshot) { - val field = runCatching { options.javaClass.getDeclaredField(fieldName) }.getOrNull() ?: continue - field.isAccessible = true - val option = runCatching { field.get(options) }.getOrNull() ?: continue - if (!isOptionLike(option)) continue - writeOptionValue(option, value) - } - } - - private fun applyDeterministicTweaks(options: Any) { - for (field in options.javaClass.declaredFields) { - if (Modifier.isStatic(field.modifiers)) continue - field.isAccessible = true - val option = runCatching { field.get(options) }.getOrNull() ?: continue - if (!isOptionLike(option)) continue - - when { - field.name.contains("tutorial", ignoreCase = true) -> { - writeOptionEnumByName(option, "NONE") - } - field.name.contains("cloud", ignoreCase = true) -> { - writeOptionEnumByName(option, "OFF") - } - field.name.contains("renderDistance", ignoreCase = true) || field.name.contains("viewDistance", ignoreCase = true) -> { - writeOptionValue(option, 5) - } - field.name.contains("music", ignoreCase = true) -> { - writeOptionValue(option, 0.0) - } - } - } - } - - private fun isOptionLike(option: Any): Boolean { - val n = option.javaClass.simpleName - return n == "OptionInstance" || n == "SimpleOption" - } - - private fun readOptionValue(option: Any): Any? { - val getter = option.javaClass.methods.firstOrNull { - (it.name == "get" || it.name == "getValue") && it.parameterCount == 0 - } ?: return null - return runCatching { getter.invoke(option) }.getOrNull() - } - - private fun writeOptionValue(option: Any, value: Any?) { - val setter = option.javaClass.methods.firstOrNull { method -> - (method.name == "set" || method.name == "setValue") && method.parameterCount == 1 - } ?: return - - runCatching { - setter.invoke(option, value) - } - } - - private fun writeOptionEnumByName(option: Any, enumName: String) { - val current = readOptionValue(option) ?: return - if (!current.javaClass.isEnum) return - - val constant = current.javaClass.enumConstants - ?.firstOrNull { (it as? Enum<*>)?.name == enumName } ?: return - writeOptionValue(option, constant) - } - - override fun runOnClient(action: (Minecraft) -> Unit) { - computeOnClient("run-on-client", CLIENT_EXEC_TIMEOUT_SECONDS) { client -> - action(client) - } - } - - internal fun postToClient(action: (Minecraft) -> Unit) { - Minecraft.getInstance().execute { action(Minecraft.getInstance()) } - } - - override fun runOnClient(action: FailableConsumer) { - runOnClient { client -> action.accept(client) } - } - - override fun setScreen(screen: Supplier) { - var expected: Screen? = null - runOnClient { client -> - expected = screen.get() - client.setScreen(expected) - } - - runCatching { - waitFor( - { client -> - val current = client.screen - val target = expected - when { - target == null -> current == null - current === target -> true - current != null && current::class.java == target::class.java -> true - else -> false - } - }, - SCREEN_SET_TIMEOUT_TICKS, - ) - }.getOrElse { - fail( - "Screen transition did not complete in time " + - "(testId=$testId, expected=${describeScreen(expected)}, " + - "actual=${computeOnClient { describeScreen(it.screen) }}, cause=${it.message})" - ) - } - } - - /** - * Waits for asynchronous Compose recomposition (state write -> apply notification -> - * frame request -> recompose job -> next-frame join, see [ComposeIdleAware]) to settle - * before capturing a screenshot, so a `click()` (or similar) immediately followed by a - * screenshot assertion doesn't race the still-in-flight visual update. - * - * Requires two consecutive idle reads, since a single idle read can still land in the - * narrow window between a state mutation and the snapshot write observer's callback firing. - * Not a hard guarantee under extreme scheduler starvation, but turns an always-racy check - * into one that's reliable in practice. No-ops for screens that aren't Compose-driven. - */ - override fun waitForComposeIdle() { - var consecutiveIdle = 0 - repeat(COMPOSE_IDLE_TIMEOUT_TICKS) { - val idle = computeOnClient("compose-idle-check", CLIENT_EXEC_TIMEOUT_SECONDS) { client -> - (client.screen as? ComposeIdleAware)?.isComposeIdle() ?: true - } - if (idle) { - consecutiveIdle++ - if (consecutiveIdle >= COMPOSE_IDLE_CONSECUTIVE_CHECKS) return - } else { - consecutiveIdle = 0 - } - waitTick() - } - } - - override fun takeScreenshot(options: TestScreenshotOptions): Path { - waitForComposeIdle() - val capturePath = ScreenshotManager.generateCapturePath(testId, options.name) - computeOnClient("take-screenshot", CLIENT_EXEC_TIMEOUT_SECONDS) { client -> - try { - capturePath.parent?.let { Files.createDirectories(it) } - - // 1.21.1 API: capture from the main render target and write with NativeImage. - Screenshot.takeScreenshot(client.mainRenderTarget).use { screenshot -> - screenshot.writeToFile(capturePath) - } - } catch (e: Exception) { - fail("Failed to take screenshot: ${e.message}") - } - } - return capturePath - } - - override fun tryClickScreenButton(translationKey: String): Boolean { - return tryClickScreenButton(translationKey, CLIENT_EXEC_TIMEOUT_SECONDS) - } - - internal fun tryClickScreenButton(translationKey: String, timeoutSeconds: Long): Boolean { - return computeOnClient("try-click-screen-button", timeoutSeconds) { client: Minecraft -> - val screen = client.screen ?: return@computeOnClient false - val widget = screen.children() - .filterIsInstance() - .firstOrNull { - val contents = it.message.contents - contents is TranslatableContents && contents.key == translationKey - } ?: return@computeOnClient false - - val cx = widget.x + (widget.width / 2.0) - val cy = widget.y + (widget.height / 2.0) - screen.mouseClicked(cx, cy, 0) - screen.mouseReleased(cx, cy, 0) - true - } - } - - override fun waitFor(predicate: Predicate, timeout: Int): Int { - if (timeout == ClientGameTestContext.NO_TIMEOUT) { - var ticksWaited = 0 - while (!computeOnClient("wait-for", CLIENT_EXEC_TIMEOUT_SECONDS) { client: Minecraft -> predicate.test(client) }) { - ticksWaited++ - waitTick() - } - return ticksWaited - } - - require(timeout > 0) { "timeout must be positive or NO_TIMEOUT" } - for (tick in 0 until timeout) { - val ready = computeOnClient("wait-for", CLIENT_EXEC_TIMEOUT_SECONDS) { client: Minecraft -> predicate.test(client) } - if (ready) return tick - waitTick() - } - - if (!computeOnClient("wait-for-final-check", CLIENT_EXEC_TIMEOUT_SECONDS) { client: Minecraft -> predicate.test(client) }) { - fail("Predicate did not become true within $timeout ticks") - } - - return timeout - } - - override fun waitForScreen(screenClass: Class?): Int - { - return waitFor { client -> - val current = client.screen - if (screenClass == null) current == null else current != null && screenClass.isInstance(current) - } - } - - override fun waitForScreen( - screenClass: Class, - block: ComposeScreenTestContext.() -> Unit - ): Int - { - return waitForScreen(screenClass).also { - computeOnClient { screenClass.cast(it.screen) }.also { - ComposeScreenTestContext(this, it).block() - } - } - } - - override fun waitTick() { - waitTicks(1) - } - - override fun waitTicks(ticks: Int) { - val remainingTicks = ticks.coerceAtLeast(0) - if (remainingTicks == 0) return - - val timeoutMillis = (remainingTicks.toLong() * DEFAULT_TICK_MILLIS * 20L).coerceAtLeast(DEFAULT_TICK_MILLIS) - if (!ThreadingImpl.awaitTicks(remainingTicks, timeoutMillis)) { - fail("Timed out waiting for $remainingTicks client tick(s)") - } - } - - override fun worldBuilder(): TestWorldBuilder = DefaultTestWorldBuilder(this) -} - -private class DefaultTestWorldBuilder( - private val context: DefaultClientGameTestContext, -) : TestWorldBuilder { - private var useConsistentSettings = true - private var settingsAdjustor: Consumer = Consumer { } - - override fun setUseConsistentSettings(useConsistentSettings: Boolean): TestWorldBuilder = apply { - this.useConsistentSettings = useConsistentSettings - } - - override fun adjustSettings(settingsAdjuster: Consumer): TestWorldBuilder = apply { - this.settingsAdjustor = settingsAdjuster - } - - override fun create(): TestSingleplayerContext { - val saveDirectory = context.computeOnClient("world-builder-open-create-screen", WORLD_BUILDER_EXEC_TIMEOUT_SECONDS) { client -> - val oldScreen = client.screen - CreateWorldScreen.openFresh(client, oldScreen) - - val createWorldScreen = client.screen as? CreateWorldScreen - ?: context.fail("CreateWorldScreen.openFresh did not open a world-creation screen") - - val creator = createWorldScreen.uiState - - if (useConsistentSettings) { - setConsistentSettings(creator) - } - - settingsAdjustor.accept(creator) - - client.levelSource.baseDir.resolve(creator.targetFolder) - } - - context.postToClient { client -> - val screen = client.screen ?: return@postToClient - val widget = screen.children() - .filterIsInstance() - .firstOrNull { - val contents = it.message.contents - contents is TranslatableContents && contents.key == "selectWorld.create" - } ?: return@postToClient - - val cx = widget.x + (widget.width / 2.0) - val cy = widget.y + (widget.height / 2.0) - screen.mouseClicked(cx, cy, 0) - screen.mouseReleased(cx, cy, 0) - } - - // World creation transitions can momentarily starve the harness tick gate. - // Treat this first post-click tick as best-effort and rely on world-load checks below. - runCatching { context.waitTick() } - waitForWorldLoad() - - return DefaultTestSingleplayerContext( - clientContext = context, - saveDirectory = saveDirectory, - ) - } - - override fun createServer(serverProperties: Properties): TestDedicatedServerContext { - return DefaultServerWorldBuilder(context).create(serverProperties) - } - - private fun waitForWorldLoad() { - val worldLoadTimeoutTicks = (WORLD_BUILDER_EXEC_TIMEOUT_SECONDS * 1000 / DEFAULT_TICK_MILLIS).toInt() - runCatching { - context.waitTick() - context.waitFor( - { client -> client.level != null || client.singleplayerServer != null }, - worldLoadTimeoutTicks, - ) - }.getOrElse { - val client = Minecraft.getInstance() - Archie.LOGGER.warn( - "Timed out waiting for world load start (testId=${context.testId}, timeoutSeconds=$WORLD_BUILDER_EXEC_TIMEOUT_SECONDS, screen=${context.describeScreen(client.screen)}, levelLoaded=${client.level != null}, serverStarted=${client.singleplayerServer != null})" - ) - } - } - - private fun setConsistentSettings(creator: WorldCreationUiState) { - val flatPreset: Holder = creator.settings - .worldgenLoadContext() - .lookupOrThrow(Registries.WORLD_PRESET) - .getOrThrow(WorldPresets.FLAT) - - creator.worldType = WorldCreationUiState.WorldTypeEntry(flatPreset) - creator.seed = "1" - } -} - -private class DefaultTestSingleplayerContext( - override val clientContext: ClientGameTestContext, - override val saveDirectory: Path, -) : TestSingleplayerContext { - override val clientWorld: TestClientWorldContext = DefaultTestClientWorldContext(clientContext) - override val server: TestServerContext = DefaultTestServerContext(clientContext) - - override fun close() { - ThreadingImpl.checkOnGametestThread("close") - - clientContext.runOnClient { client -> - val hasLevel = client.level != null - val hasLocalServer = client.singleplayerServer != null || client.isLocalServer - if (!hasLevel && !hasLocalServer) return@runOnClient - - client.level?.disconnect() - if (hasLocalServer) { - client.disconnect(GenericMessageScreen(Component.translatable("menu.savingLevel"))) - } else { - client.disconnect() - } - } - - runCatching { - clientContext.waitFor( - { client -> client.level == null && client.singleplayerServer == null }, - SharedConstants.TICKS_PER_MINUTE, - ) - }.getOrElse { error -> - Archie.LOGGER.warn( - "Singleplayer world did not close cleanly (testId=${clientContext.testId}, saveDir=$saveDirectory, cause=${error.message})" - ) - } - - // Final recovery pass: always try to land on title and clear any lingering local session state. - clientContext.runOnClient { client -> - if (client.level != null || client.singleplayerServer != null || client.isLocalServer) { - client.level?.disconnect() - if (client.singleplayerServer != null || client.isLocalServer) { - client.disconnect(GenericMessageScreen(Component.translatable("menu.savingLevel"))) - } else { - client.disconnect() - } - } - if (client.screen !is TitleScreen) { - client.setScreen(TitleScreen()) - } - } - - runCatching { - clientContext.waitFor( - { client -> client.level == null && client.singleplayerServer == null }, - SharedConstants.TICKS_PER_SECOND * 10, - ) - }.getOrElse { error -> - Archie.LOGGER.warn( - "Singleplayer session still present after forced close (testId=${clientContext.testId}, saveDir=$saveDirectory, cause=${error.message})" - ) - } - } -} - -private class DefaultTestServerContext( - private val clientContext: ClientGameTestContext, -) : TestServerContext { - override fun runCommand(command: String) { - ThreadingImpl.checkOnGametestThread("runCommand") - require(command.isNotBlank()) { "command cannot be blank" } - - runOnServer(FailableConsumer { server -> - runCommandReflective(server, command) - }) - } - - override fun runOnServer(action: FailableConsumer) { - ThreadingImpl.checkOnGametestThread("runOnServer") - val server = requireSingleplayerServer() - ThreadingImpl.runOnServer { - action.accept(server) - } - } - - override fun computeOnServer(function: FailableFunction): T { - ThreadingImpl.checkOnGametestThread("computeOnServer") - val server = requireSingleplayerServer() - var result: T? = null - ThreadingImpl.runOnServer { - result = function.apply(server) - } - - @Suppress("UNCHECKED_CAST") - return result as T - } - - override fun runOnServer(action: (MinecraftServer) -> Unit) = runOnServer(FailableConsumer(action)) - - override fun computeOnServer(function: (MinecraftServer) -> T): T = computeOnServer(FailableFunction(function)) - - private fun requireSingleplayerServer(): MinecraftServer { - return clientContext.computeOnClient { client -> - client.singleplayerServer ?: throw IllegalStateException("No integrated server is running") - } - } - - private fun runCommandReflective(server: MinecraftServer, command: String) { - val source = runCatching { - server.javaClass.methods.firstOrNull { it.name == "createCommandSourceStack" && it.parameterCount == 0 } - ?.invoke(server) - }.getOrNull() - - val commandsObj = runCatching { - server.javaClass.methods.firstOrNull { it.name == "getCommands" && it.parameterCount == 0 }?.invoke(server) - ?: server.javaClass.methods.firstOrNull { it.name == "getCommandManager" && it.parameterCount == 0 }?.invoke(server) - }.getOrNull() ?: throw IllegalStateException("Could not resolve command manager from server") - - val executeMethod = commandsObj.javaClass.methods.firstOrNull { method -> - method.parameterCount == 2 && - (method.name == "performPrefixedCommand" || method.name == "executeWithPrefix" || method.name == "performCommand") - } ?: throw IllegalStateException("Could not find command execution method on ${commandsObj.javaClass.name}") - - executeMethod.invoke(commandsObj, source, command) - } -} - -private class DefaultServerWorldBuilder( - private val context: DefaultClientGameTestContext, -) { - fun create(serverProperties: Properties): TestDedicatedServerContext { - val serverStartTimeout = WORLD_BUILDER_EXEC_TIMEOUT_SECONDS - - lateinit var serverInstance: Any - lateinit var serverDirectory: Path - - try { - // Prepare server directory on client thread - context.computeOnClient("setup-dedicated-server-dir", serverStartTimeout) { mc -> - try { - val gameDir = mc.gameDirectory.toPath() - val dir = gameDir.resolve("test_server_${System.nanoTime()}") - Files.createDirectories(dir) - serverDirectory = dir - - // Apply server properties - writeServerBootstrapFiles(dir, serverProperties) - } catch (e: Exception) { - context.fail("Failed to set up server directory: ${e.message}") - } - } - - // Start server asynchronously (blocks gametest thread, not client thread) - try { - serverInstance = ADedicatedServerPlatform.start( - serverDirectory, - serverProperties, - serverStartTimeout - ) - DedicatedServerLifecycleTracker.register(serverInstance) - } catch (e: Exception) { - context.fail("Failed to start dedicated server: ${e.message}") - } - - return DefaultTestDedicatedServerContext( - clientContext = context, - serverInstance = serverInstance, - serverDirectory = serverDirectory, - ) - } catch (e: Exception) { - context.fail("Error creating dedicated server: ${e.message}") - } - } - - private fun writeServerBootstrapFiles(serverDirectory: Path, serverProperties: Properties) { - val merged = Properties() - merged.putAll(serverProperties) - merged.putIfAbsent("online-mode", "false") - merged.putIfAbsent("spawn-protection", "0") - merged.putIfAbsent("max-players", "1") - // This dedicated server shares the JVM with the client under test (no subprocess - // isolation). ServerWatchdog calls System.exit(1) if a single tick exceeds - // max-tick-time, which would kill the whole test JVM - disable it by default. - merged.putIfAbsent("max-tick-time", "0") - - // The dedicated-server launcher reads these files from the process working directory, - // while the harness also keeps an isolated copy under the generated per-test server dir. - writeBootstrapFiles(serverDirectory, merged) - CwdBootstrapFileGuard.writeOverride(merged) - } - - private fun writeBootstrapFiles(targetDirectory: Path, properties: Properties) { - writeTextFile(targetDirectory, "server.properties") { writer -> - properties.store(writer, "Archie GameTest dedicated server properties") - } - writeTextFile(targetDirectory, "eula.txt") { writer -> - writer.write("eula=true") - writer.newLine() - } - } - - private fun writeTextFile(targetDirectory: Path, fileName: String, writerAction: (java.io.BufferedWriter) -> Unit) { - Files.newBufferedWriter(targetDirectory.resolve(fileName)).use(writerAction) - } -} - -private data class DefaultTestDedicatedServerContext( - override val clientContext: ClientGameTestContext, - val serverInstance: Any, - override val serverDirectory: Path, -) : TestDedicatedServerContext { - override fun connect(): TestServerConnection { - ThreadingImpl.checkOnGametestThread("connect") - - val port = ADedicatedServerPlatform.port(serverInstance) - clientContext.runOnClient { client -> - connectToLocalhost(client, port) - } - - clientContext.waitFor( - { client -> client.level != null }, - ClientGameTestContext.DEFAULT_TIMEOUT, - ) - - return DefaultTestServerConnection( - clientContext = clientContext, - clientWorld = DefaultTestClientWorldContext(clientContext), - ) - } - - override fun close() { - ThreadingImpl.checkOnGametestThread("close") - - try { - val stopRequested = requestStopWithTimeout(timeoutMillis = TimeUnit.SECONDS.toMillis(10)) - if (!stopRequested) { - Archie.LOGGER.warn( - "Timed out requesting dedicated server stop; forcing halt (testId=${clientContext.testId}, serverDir=$serverDirectory)" - ) - forceHaltServer() - } - - runCatching { - clientContext.waitFor( - { _ -> !ADedicatedServerPlatform.isAlive(serverInstance) }, - SharedConstants.TICKS_PER_MINUTE, - ) - }.getOrElse { error -> - Archie.LOGGER.warn( - "Dedicated server did not stop cleanly (testId=${clientContext.testId}, serverDir=$serverDirectory, alive=${ADedicatedServerPlatform.isAlive(serverInstance)}, cause=${error.message})" - ) - } - } finally { - DedicatedServerLifecycleTracker.unregister(serverInstance) - } - } - - private fun requestStopWithTimeout(timeoutMillis: Long): Boolean { - var stopFailure: Throwable? = null - val stopThread = Thread({ - runCatching { - // Stopping from outside the server tick thread avoids self-stop deadlocks. - ADedicatedServerPlatform.stop(serverInstance) - }.recoverCatching { - ThreadingImpl.runOnServer { - ADedicatedServerPlatform.stop(serverInstance) - } - }.onFailure { - stopFailure = it - } - }, "Archie Dedicated GameTest Server Stop") - - stopThread.isDaemon = true - stopThread.start() - stopThread.join(timeoutMillis) - - stopFailure?.let { - throw IllegalStateException("Failed to stop dedicated server context", it) - } - - return !stopThread.isAlive - } - - private fun forceHaltServer() { - runCatching { - val haltMethod = serverInstance.javaClass.methods.firstOrNull { method -> - (method.name == "stopServer") && method.parameterCount <= 1 - } ?: return - - when (haltMethod.parameterCount) { - 0 -> haltMethod.invoke(serverInstance) - 1 -> { - val paramType = haltMethod.parameterTypes[0] - when (paramType) { - Boolean::class.javaPrimitiveType, Boolean::class.java -> haltMethod.invoke(serverInstance, true) - else -> haltMethod.invoke(serverInstance, null) - } - } - } - } - } - - private fun connectToLocalhost(client: Minecraft, port: Int) { - val connectScreenClass = runCatching { - Class.forName("net.minecraft.client.gui.screens.ConnectScreen") - }.getOrElse { - throw IllegalStateException("ConnectScreen class not found") - } - - val addressClass = runCatching { - Class.forName("net.minecraft.client.multiplayer.resolver.ServerAddress") - }.getOrNull() ?: runCatching { - Class.forName("net.minecraft.client.multiplayer.ServerAddress") - }.getOrElse { - throw IllegalStateException("ServerAddress class not found") - } - - val serverDataClass = runCatching { - Class.forName("net.minecraft.client.multiplayer.ServerData") - }.getOrElse { - throw IllegalStateException("ServerData class not found") - } - - val parseMethod = addressClass.methods.firstOrNull { - it.name == "parseString" && it.parameterCount == 1 - } ?: addressClass.methods.firstOrNull { - it.name == "parse" && it.parameterCount == 1 - } ?: throw IllegalStateException("Could not find ServerAddress parse method") - - val address = parseMethod.invoke(null, "localhost:$port") - - val serverTypeClass = serverDataClass.declaredClasses.firstOrNull { - it.simpleName == "Type" && it.isEnum - } - val serverTypeValue = serverTypeClass?.enumConstants?.firstOrNull() - val serverData = serverDataClass.constructors.firstOrNull { it.parameterCount >= 3 } - ?.newInstance("localhost", "localhost:$port", serverTypeValue) - - val connectMethod = connectScreenClass.methods.firstOrNull { - it.name == "startConnecting" || it.name == "connect" - } ?: throw IllegalStateException("Could not find ConnectScreen connect method") - - val args = connectMethod.parameterTypes.map { param -> - when { - Screen::class.java.isAssignableFrom(param) -> client.screen - Minecraft::class.java.isAssignableFrom(param) -> client - param.isAssignableFrom(addressClass) -> address - serverData != null && param.isAssignableFrom(serverDataClass) -> serverData - param == Boolean::class.javaPrimitiveType || param == Boolean::class.java -> false - else -> null - } - }.toTypedArray() - - connectMethod.invoke(null, *args) - } - -} - -private data class DefaultTestServerConnection( - override val clientContext: ClientGameTestContext, - override val clientWorld: TestClientWorldContext, -) : TestServerConnection { - override fun disconnect() { - ThreadingImpl.checkOnGametestThread("close") - - clientContext.runOnClient { client -> - if (client.level == null) { - if (client.screen !is TitleScreen) { - client.setScreen(TitleScreen()) - } - return@runOnClient - } - - client.level?.disconnect() - client.disconnect() - } - - runCatching { - clientContext.waitFor({ client -> client.level == null }, ClientGameTestContext.DEFAULT_TIMEOUT) - }.getOrElse { error -> - Archie.LOGGER.warn( - "Timed out waiting for dedicated-server client disconnect (testId=${clientContext.testId}, cause=${error.message})" - ) - } - clientContext.setScreen(Supplier { TitleScreen() }) - } -} - -private class DefaultTestClientWorldContext( - private val clientContext: ClientGameTestContext, -) : TestClientWorldContext { - override fun waitForChunksDownload(timeout: Int): Int { - ThreadingImpl.checkOnGametestThread("waitForChunksDownload") - return clientContext.waitFor({ client -> areChunksLoaded(client) }, timeout) - } - - override fun waitForChunksRender(waitForDownload: Boolean, timeout: Int): Int { - ThreadingImpl.checkOnGametestThread("waitForChunksRender") - return clientContext.waitFor( - { client -> - (!waitForDownload || areChunksLoaded(client)) && areChunksRendered(client) - }, - timeout, - ) - } - - private fun areChunksLoaded(client: Minecraft): Boolean { - val level = client.level ?: return false - val player = client.player ?: return false - - val viewDistance = resolveClientViewDistance(client).coerceAtLeast(2) - val centerChunkX = player.blockX shr 4 - val centerChunkZ = player.blockZ shr 4 - val chunkSource = level.chunkSource - - val hasChunkMethod = chunkSource.javaClass.methods.firstOrNull { - (it.name == "hasChunk" || it.name == "isChunkLoaded") && - it.parameterCount == 2 && - it.parameterTypes[0] == Int::class.javaPrimitiveType && - it.parameterTypes[1] == Int::class.javaPrimitiveType - } - - if (hasChunkMethod != null) { - for (dz in -viewDistance..viewDistance) { - for (dx in -viewDistance..viewDistance) { - val loaded = runCatching { - hasChunkMethod.invoke(chunkSource, centerChunkX + dx, centerChunkZ + dz) as? Boolean - }.getOrNull() ?: false - if (!loaded) return false - } - } - return true - } - - // Fallback when chunk-source internals differ across mappings. - return true - } - - private fun areChunksRendered(client: Minecraft): Boolean { - val levelRenderer = client.levelRenderer ?: return false - val renderCompleteMethod = levelRenderer.javaClass.methods.firstOrNull { - (it.name == "isTerrainRenderComplete" || it.name == "isRenderComplete") && - it.parameterCount == 0 - } - - if (renderCompleteMethod != null) { - return runCatching { - renderCompleteMethod.invoke(levelRenderer) as? Boolean - }.getOrNull() ?: false - } - - return true - } - - private fun resolveClientViewDistance(client: Minecraft): Int { - val options = client.options - val method = options.javaClass.methods.firstOrNull { - (it.name == "getEffectiveRenderDistance" || it.name == "getClampedViewDistance") && it.parameterCount == 0 - } - - return runCatching { - (method?.invoke(options) as? Int) ?: 5 - }.getOrDefault(5) - } -} - -/** Aggregate result of an [AClientGameTestHarness.run] invocation. */ -data class AClientGameTestSummary( - val passed: Int, - val failed: Int, - val skipped: Int, - val failedTests: List = emptyList(), - val failedDetails: List = emptyList(), -) - -/** - * Runs every [ClientGameTest]-annotated method across [modToClasses] (as collected by - * [AGameTestPlatform.register] via [AGameTestEventObject]/[AEvents.ArchieGameTestBuilder]'s - * `client { }` block) sequentially on the client thread, then returns to the title screen. - * No-ops (returning an all-zero summary) unless [side] is [AGameTestSide.CLIENT]. - */ -object AClientGameTestHarness { - @OptIn(ExperimentalCoroutinesApi::class) - fun run(modToClasses: Map>>, side: AGameTestSide?): AClientGameTestSummary { - if (side != AGameTestSide.CLIENT) return AClientGameTestSummary(passed = 0, failed = 0, skipped = 0) - - val selectedMods = selectModsToRun(modToClasses) - - var passed = 0 - var failed = 0 - var skipped = 0 - val failedTests = mutableListOf() - val failedDetails = mutableListOf() - - selectedMods.forEach { (mod, classes) -> - classes.forEach { clazz -> - val methods = clazz.declaredMethods.filter { - it.isAnnotationPresent(ClientGameTest::class.java) - } - - if (methods.isEmpty()) return@forEach - - val instance = clazz.kotlin.objectInstance ?: clazz.kotlin.primaryConstructor?.call() - - methods.forEach { method -> - val clientTest = method.getAnnotation(ClientGameTest::class.java) - val explicitName = clientTest?.name?.takeIf { it.isNotBlank() } - val testId = explicitName ?: "${mod.modId}:${clazz.simpleName.lowercase()}.${method.name.lowercase()}" - val context = DefaultClientGameTestContext(testId) - - val params = method.parameterTypes - val supported = when { - params.isEmpty() -> true - params.size == 1 && ClientGameTestContext::class.java.isAssignableFrom(params[0]) -> true - else -> false - } - - if (!supported) { - skipped++ - Archie.LOGGER.warn("[ClientGameTest] Skipping {} (unsupported signature: {} params)", testId, params.size) - return@forEach - } - - // Give every ComposeScreen this test opens a virtual clock/dispatcher (see - // ComposeTestClockOverride's KDoc) instead of real threads/wall-clock time - - // installed here (not the render thread) since setScreen{} dispatches actual - // screen construction there, but reads the override via a plain shared - // static, not a ThreadLocal. Always cleared, even on failure, so it can never - // leak into the next test or (in principle) the running game. - // - // advanceTimeBy(bounded), NOT advanceUntilIdle() - composables can run - // legitimately infinite delay() loops (e.g. TextFieldCore's blinking-cursor - // LaunchedEffect), and advanceUntilIdle() only returns once truly nothing is - // scheduled, which for an unboundedly-recurring loop is never: it hangs the - // render thread forever the first time such a composable is on screen. A fixed - // per-pump increment (~1 tick) makes every delay() progress a little on every - // real frame instead, bounded and hang-proof either way. - val scheduler = TestCoroutineScheduler() - ComposeTestClockOverride.dispatcher = StandardTestDispatcher(scheduler) - ComposeTestClockOverride.pump = { scheduler.advanceTimeBy(50) } - try { - runCatching { - context.getInput().clearInputs() - method.isAccessible = true - if (params.isEmpty()) method.invoke(instance) - else method.invoke(instance, context) - }.onSuccess { - context.getInput().clearInputs() - passed++ - Archie.LOGGER.info("[ClientGameTest] PASS {}", testId) - }.onFailure { error -> - runCatching { context.getInput().clearInputs() } - failed++ - failedTests += testId - failedDetails += AClientGameTestFailure(testId, rootCauseSummary(error)) - Archie.LOGGER.error("[ClientGameTest] FAIL {}", testId, error) - } - } finally { - ComposeTestClockOverride.dispatcher = null - ComposeTestClockOverride.pump = null - } - } - } - } - - Archie.LOGGER.info("[ClientGameTest] Completed: passed={}, failed={}, skipped={}", passed, failed, skipped) - // Safety net: if a test aborted before context.close(), force-stop leaked dedicated servers - // before the client begins shutdown to avoid dedicated tick crashes against torn-down GLFW. - DedicatedServerLifecycleTracker.stopAllLeakedServers() - // Always runs, even if some test leaked its server context - see CwdBootstrapFileGuard. - CwdBootstrapFileGuard.restoreIfCaptured() - - val minecraft = Minecraft.getInstance() - minecraft.execute { - val flag = minecraft.isLocalServer - val serverdata = minecraft.currentServer - minecraft.level?.disconnect() - if (flag) { - minecraft.disconnect(GenericMessageScreen(Component.translatable("menu.savingLevel"))) - } else { - minecraft.disconnect() - } - - val titlescreen = TitleScreen() - if (flag) { - minecraft.setScreen(titlescreen) - } else if (serverdata != null && serverdata.isRealm) { - minecraft.setScreen(RealmsMainScreen(titlescreen)) - } else { - minecraft.setScreen(JoinMultiplayerScreen(titlescreen)) - } - } - return AClientGameTestSummary( - passed = passed, - failed = failed, - skipped = skipped, - failedTests = failedTests, - failedDetails = failedDetails, - ) - } -} - -/** - * Warps the real GLFW cursor to the same position a synthetic [TestInput] call just fed to - * [net.minecraft.client.gui.screens.Screen.mouseMoved]/`mouseClicked` directly. - * - * That synthetic dispatch bypasses GLFW entirely, so vanilla's own `MouseHandler` never learns - * about it - its own cursor-position callback still fires from whatever the OS/window's real - * cursor is doing, completely independent of the test's intended position. If that callback - * later reports a position outside the node the test just hovered/clicked, it fires its own - * `mouseMoved` with the stale real coordinates, silently overwriting `hovered` state right back - * to false - a real cursor twitch (or, under Xvfb, a window-manager cursor warp on focus) racing - * a test assertion and failing it. Keeping the real cursor in sync removes that race outright: - * any later real callback reports the same position the test already set, so no spurious - * enter/exit transition can happen. - */ -private fun warpRealCursor(client: Minecraft, guiX: Double, guiY: Double) { - val window = client.window - val realX = guiX * window.screenWidth / window.guiScaledWidth - val realY = guiY * window.screenHeight / window.guiScaledHeight - GLFW.glfwSetCursorPos(window.window, realX, realY) -} - -private fun selectModsToRun(modToClasses: Map>>): Map>> { - val selected = AGameTestModFilter.selectMods(modToClasses.keys).toSet() - return modToClasses.filterKeys { it in selected } -} - -private fun rootCauseSummary(error: Throwable): String { - val root = generateSequence(error) { it.cause }.last() - val message = root.message?.takeIf { it.isNotBlank() } - return if (message != null) { - "${root::class.java.name}: $message" - } else { - root::class.java.name - } -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt deleted file mode 100644 index 83c95fae7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import java.nio.file.Path -import java.util.Properties - -/** Cross-loader bridge for dedicated server bootstrap used by client GameTests. */ -expect object ADedicatedServerPlatform { - /** - * Boots a loader-specific dedicated server rooted at [serverDirectory] using - * [serverProperties], waiting up to [timeoutSeconds] for it to finish starting. - * - * @return An opaque, loader-specific handle to pass to [stop]/[port]/[isAlive]. - */ - fun start(serverDirectory: Path, serverProperties: Properties, timeoutSeconds: Long): Any - - /** Shuts down the dedicated server identified by [serverInstance] (as returned by [start]). */ - fun stop(serverInstance: Any) - - /** The port the dedicated server identified by [serverInstance] is listening on. */ - fun port(serverInstance: Any): Int - - /** Whether the dedicated server identified by [serverInstance] is still running. */ - fun isAlive(serverInstance: Any): Boolean -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt deleted file mode 100644 index bdb7786e1..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt +++ /dev/null @@ -1,20 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import net.kernelpanicsoft.archie.events.AEvents -import net.kernelpanicsoft.archie.events.AEventObject -import dev.architectury.event.Event -import dev.architectury.platform.Mod - -/** - * Convenience [AEventObject] base for listening to [AEvents.REGISTER_GAME_TEST] for [mod]. - * Subclass and override [handler] (an [AEvents.ArchieGameTestBuilder] receiver) to declare - * gametest classes via `server { register<...>() }` / `client { ... }` / `common { ... }`. - */ -abstract class AGameTestEventObject(mod: Mod) : - AEventObject( - mod - ) -{ - override val event: Event = AEvents.REGISTER_GAME_TEST - override val handlerConstructor: AEvents.RegisterGameTestHandler.Companion = AEvents.RegisterGameTestHandler.Companion -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt deleted file mode 100644 index e86cc366f..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt +++ /dev/null @@ -1,78 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import dev.architectury.platform.Mod -import dev.architectury.utils.Env - -/** Logical side used while collecting/running GameTests. */ -enum class AGameTestSide { - SERVER, - CLIENT, -} - -internal fun AGameTestSide.toEnv(): Env = when (this) { - AGameTestSide.SERVER -> Env.SERVER - AGameTestSide.CLIENT -> Env.CLIENT -} - -/** - * Cross-loader GameTest integration point. - * - * Implementations detect whether GameTest mode is active and collect test classes per mod. - */ -expect object AGameTestPlatform -{ - /** `true` when the current process is running under a GameTest task (`runGametest`/`runGametestClient`). */ - val isGameTest: Boolean - - /** Active logical side for this GameTest run (supports launcher/property overrides). */ - val side: AGameTestSide? - - /** Register a test class for [mod] when GameTest bootstrapping occurs. */ - fun register(clazz: Class<*>, mod: Mod) -} - -/** - * Restricts which [AEvents.REGISTER_GAME_TEST]-registered mods a single `runGametest`/ - * `runGametestClient` invocation actually runs, via the [GAMETEST_MOD_ID_FILTER_PROPERTY] - * system property (a comma-separated list of mod ids). - * - * Every mod that has called `AEvents += MOD` shares one JVM-wide [AEvents.MODS] list - which - * matters because a composite build's included builds can *both* end up in that list within - * the same process. Archie-Test's Loom `runs{}` blocks `includeBuild("../Archie")`, and both - * `Archie` and `ArchieTest`'s mod init call `AEvents += MOD`, so launching Archie-Test's own - * `runGametestClient`/`runGametest` previously ran Archie's *entire* GameTest suite a second - * time in the same process, without Archie-Test's own suite being any bigger - only - * distinguishable by the test count not matching the log's actual line count. Every loader's - * `AGameTestPlatformInternal`/`AClientGameTestHarness` server- and client-side test collection - * should call [selectMods] on [AEvents.MODS] before iterating, instead of iterating it directly. - */ -object AGameTestModFilter { - private const val GAMETEST_MOD_ID_FILTER_PROPERTY = "archie.gametest.modid" - - /** - * Filters [mods] down to just the ones named in [GAMETEST_MOD_ID_FILTER_PROPERTY], or returns - * [mods] unchanged if that property isn't set. Each project's own Loom `runs{}` block should - * set this to its own `mod_id` gradle property on its `gametest`/`gametestClient` runs. - * - * @throws IllegalStateException if the property is set but names no mod present in [mods]. - */ - fun selectMods(mods: Collection): List { - val filter = System.getProperty(GAMETEST_MOD_ID_FILTER_PROPERTY)?.trim().orEmpty() - if (filter.isEmpty()) return mods.toList() - - val requestedIds = filter.split(',') - .map { it.trim() } - .filter { it.isNotEmpty() } - .toSet() - - check(requestedIds.isNotEmpty()) { - "No valid mod IDs specified in gametest filter '$GAMETEST_MOD_ID_FILTER_PROPERTY'" - } - - val selected = mods.filter { it.modId in requestedIds } - check(selected.isNotEmpty()) { - "No gametests found for requested mod IDs: ${requestedIds.joinToString(",")} (available: ${mods.joinToString(",") { it.modId }})" - } - return selected - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ComposeScreenTestContext.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ComposeScreenTestContext.kt deleted file mode 100644 index 55312b708..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ComposeScreenTestContext.kt +++ /dev/null @@ -1,286 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import dev.architectury.registry.menu.ExtendedMenuProvider -import dev.architectury.registry.menu.MenuRegistry -import net.kernelpanicsoft.archie.gui.ComposeContainerScreen -import net.kernelpanicsoft.archie.gui.LayerManagerProvider -import net.kernelpanicsoft.archie.gui.layer.Layer -import net.kernelpanicsoft.archie.gui.layout.LayoutNode -import net.minecraft.client.gui.screens.Screen -import net.minecraft.client.player.LocalPlayer -import net.minecraft.core.BlockPos -import net.minecraft.world.level.block.entity.BlockEntity -import net.minecraft.world.level.block.state.BlockState -import org.apache.commons.lang3.function.FailableConsumer -import org.apache.commons.lang3.function.FailableFunction - -/** Selects which [Layer] a [ComposeScreenTestContext] node lookup searches. */ -enum class LayerSelector { - /** The frontmost layer (a modal/dialog if one is open, otherwise the base screen). */ - Top, - - /** The screen's original base layer, regardless of any modals stacked on top of it. */ - Base, -} - -/** A resolved [LayoutNode] handle, scoped to a single [ComposeScreenTestContext.node] block. */ -@Suppress("unused") -class TestNodeScope( - val context: ClientGameTestContext, - val node: LayoutNode, -) { - /** - * Waits for compose to settle before reading [node]'s on-screen bounds - without this, a - * node's very first interaction (right after [ComposeScreenTestContext.waitForScreen]/ - * [ComposeScreenTestContext.node] finds it) can read a transient pre-layout-settle position - * (e.g. before a wrapping Scrollable's initial measure has stabilized), computing a click/ - * hover target that no longer matches the node's real bounds one frame later - silently - * missing the node (no ENTER/PRESS ever dispatches) rather than failing loudly. - */ - private fun centerCoords(): Pair { - context.waitForComposeIdle() - return context.computeOnClient { - val (nx, ny) = node.absoluteCoords - (nx + node.width / 2.0) to (ny + node.height / 2.0) - } - } - - /** Clicks the center of this node's on-screen bounds. */ - fun click(button: Int = 0) { - val (x, y) = centerCoords() - context.getInput().click(x, y, button) - } - - /** Moves the cursor to the center of this node's on-screen bounds, without clicking - e.g. to assert a [TextureStates.HOVERED] visual state. */ - fun hover() { - val (x, y) = centerCoords() - context.getInput().setCursor(x, y) - } - - /** - * Presses and releases [keyCode]. Key input in this framework targets the active screen as - * a whole, not a specific node - [click] (or [hover], for a text field that focuses on - * hover) the target first if it needs focus. - */ - fun pressKey(keyCode: Int, scanCode: Int = 0, modifiers: Int = 0) { - context.getInput().pressKey(keyCode, scanCode, modifiers) - } - - /** Types each character of [value] as if typed at the keyboard. See [pressKey] re: focus. */ - fun type(value: String) { - context.getInput().typeChars(value) - } - - /** Moves the cursor to this node's center, then scrolls there. See [TestInput.scroll]. */ - fun scroll(x: Double = 0.0, y: Double = 1.0) { - val (cx, cy) = centerCoords() - context.getInput().setCursor(cx, cy) - context.getInput().scroll(x, y) - } - - /** - * The [TextureStates] key this node's [net.kernelpanicsoft.archie.gui.layout.Renderer] most - * recently selected to draw (e.g. `"hovered"`), or `null` if this node doesn't render a - * theme-state-driven visual. See [net.kernelpanicsoft.archie.gui.nodes.UINode.renderState]. - */ - val renderState: String? get() = context.computeOnClient { node.renderState } - - /** - * Fails unless this node's [renderState] equals [expected]. - * - * Reads [renderState] exactly once - the same value is used both to decide pass/fail and - * (on failure) in the default message. Re-reading it live inside the message lambda instead - * would race further recomposition between the comparison and the (lazily-evaluated, only - * on failure) message being built, showing a misleading "got " that no longer - * matches whatever value the comparison actually failed on. - */ - fun assertRenderState( - expected: String, - message: (() -> String)? = null, - ) { - val actual = renderState - context.assertEquals(expected, actual, message ?: { - "Expected node '${node.name}' render state <$expected>, got <$actual> (testId=${context.testId})" - }) - } - - /** This node's direct children's names, in composition order. */ - fun childNames(): List = context.computeOnClient { node.children.map { it.name } } - - /** Fails unless this node's direct children's names, in order, equal [expected]. */ - fun assertChildNames(vararg expected: String) { - val actual = childNames() - context.assertEquals(expected.toList(), actual) { - "Expected node '${node.name}' children <${expected.toList()}>, got <$actual> (testId=${context.testId})\n${describeTree()}" - } - } - - /** Whether a descendant named [name] exists anywhere in this node's subtree, without failing. */ - fun hasDescendant(name: String): Boolean = context.computeOnClient { node.findNode(name) != null } - - /** Fails unless a descendant named [name] exists anywhere in this node's subtree. */ - fun assertHasDescendant(name: String) { - context.assertTrue(hasDescendant(name)) { - "Expected node '${node.name}' to have a descendant named '$name' (testId=${context.testId})\n${describeTree()}" - } - } - - /** - * Fails if this node or any descendant has a non-positive width or height - the "zero-size - * widget" class of layout bug, catchable without any pixel comparison. - */ - fun assertAllDescendantsSized() { - val unsized = context.computeOnClient { node.flatten().filter { it.width <= 0 || it.height <= 0 } } - context.assertTrue(unsized.isEmpty()) { - "Expected every node under '${node.name}' to have a positive size, but found zero-sized: " + - unsized.joinToString { "${it.name}(${it.width}x${it.height})" } + - " (testId=${context.testId})\n${describeTree()}" - } - } - - /** A recursive dump of this node's subtree (name, nested per child), for failure messages. */ - fun describeTree(): String = context.computeOnClient { node.toString() } - - /** - * All descendants of this node named [name], in depth-first order - the escape hatch for - * [node] (which requires exactly one match) when a subtree legitimately has several, e.g. - * every "Button" in a dialog's action row. - */ - fun nodes(name: String): List = context.computeOnClient { node.findAllNodes(name) } - - /** - * Waits for a descendant named [name] within this node's subtree (not the whole layer) to - * appear, then runs [block] against it. Fails if [name] doesn't appear within [timeout] ticks. - */ - fun node( - name: String, - timeout: Int = ClientGameTestContext.DEFAULT_TIMEOUT, - block: TestNodeScope.() -> R, - ): R { - context.waitFor({ _ -> node.findNode(name) != null }, timeout) - val resolved = context.computeOnClient { node.findNode(name) } - ?: context.fail("Node '$name' not found under '${node.name}' (testId=${context.testId})") - return TestNodeScope(context, resolved).block() - } - - operator fun LayoutNode.invoke(block: TestNodeScope.() -> R): R - { - return TestNodeScope(context, this).block() - } -} - -/** - * Kotlin-idiomatic access to a [ComposeContainerScreen]'s layer/node tree from a client game - * test, replacing manual `layerManager.layers...findNode(...)` bookkeeping with a small - * receiver-block DSL. - */ -@Suppress("unused") -class ComposeScreenTestContext internal constructor( - val context: ClientGameTestContext, - val screen: S, -) { - /** The number of layers currently on the stack (1 = no modal open). */ - val layerCount: Int get() = context.computeOnClient { screen.layerManager.layers.size } - - /** The frontmost [Layer] (a modal/dialog if one is open, otherwise the base screen). */ - val topLayer: Layer get() = layer(layer = LayerSelector.Top) - /** The screen's original base [Layer], regardless of any modals stacked on top of it. */ - val baseLayer: Layer get() = layer(layer = LayerSelector.Base) - - private fun resolveLayer(selector: LayerSelector): Layer? = when (selector) { - LayerSelector.Top -> screen.layerManager.top - LayerSelector.Base -> screen.layerManager.layers.firstOrNull() - } - - private fun resolveNode(name: String, layer: LayerSelector): LayoutNode? = - resolveLayer(layer)?.findNode(name) - - /** Checks whether a node named [name] currently exists, without waiting for it. */ - fun hasNode(name: String, layer: LayerSelector = LayerSelector.Top): Boolean = - context.computeOnClient { resolveNode(name, layer) != null } - - /** Resolves [layer] to a [Layer] immediately, without waiting. Fails if it doesn't currently exist. */ - fun layer(layer: LayerSelector = LayerSelector.Top): Layer = context.computeOnClient { resolveLayer(layer) ?: error("Layer not found") } - - /** Resolves the layer at stack position [index] immediately, without waiting. Fails if it doesn't currently exist. */ - fun layer(index: Int): Layer = context.computeOnClient { screen.layerManager.layers.getOrNull(index) ?: error("Layer not found") } - - /** Waits for a layer to appear at stack position [index], then runs [block] against it. */ - fun waitForLayer(index: Int, block: Layer.() -> Unit = {}) { - context.waitFor { screen.layerManager.layers.getOrNull(index) != null } - val resolved = context.computeOnClient { screen.layerManager.layers.getOrNull(index) } - ?: context.fail("Layer '$index' not found (testId=${context.testId})") - return resolved.block() - } - - /** Waits for a node named [name] to appear on this specific [Layer], then runs [block] against it. */ - fun Layer.node( - name: String, - timeout: Int = ClientGameTestContext.DEFAULT_TIMEOUT, - block: TestNodeScope.() -> R, - ): R { - context.waitFor { findNode(name) != null } - val resolved = context.computeOnClient { findNode(name) } - ?: context.fail("Node '$name' not found (testId=${context.testId})") - return TestNodeScope(context, resolved).block() - } - - /** - * Waits for a node named [name] to appear on [layer] (default: the topmost layer), then - * runs [block] against it. Fails the test if the node doesn't appear within [timeout] ticks. - */ - fun node( - name: String, - layer: LayerSelector = LayerSelector.Top, - timeout: Int = ClientGameTestContext.DEFAULT_TIMEOUT, - block: TestNodeScope.() -> R, - ): R { - context.waitFor({ _ -> resolveNode(name, layer) != null }, timeout) - val resolved = context.computeOnClient { resolveNode(name, layer) } - ?: context.fail("Node '$name' not found on ${layer.name.lowercase()} layer (testId=${context.testId})") - return TestNodeScope(context, resolved).block() - } - - /** Wraps an already-resolved [LayoutNode] (e.g. one indexed out of [TestNodeScope.nodes]) for interaction, without constructing a [TestNodeScope] by hand. */ - operator fun LayoutNode.invoke(block: TestNodeScope.() -> R): R = TestNodeScope(context, this).block() -} - -/** Reified convenience for [ClientGameTestContext.waitForScreen] that resolves [S]'s [Class] automatically. */ -inline fun ClientGameTestContext.waitForScreen(noinline block: ComposeScreenTestContext.() -> Unit = {}) where S : Screen, S : LayerManagerProvider = waitForScreen(S::class.java, block) - -/** Waits until the client-side player entity exists, then returns it. */ -fun ClientGameTestContext.waitForPlayer(): LocalPlayer = waitFor { client -> client.player != null }.let { computeOnClient { client -> client.player!! } } - -/** Waits until a block entity of type [T] exists at [pos] on the client, then returns the server-side instance. */ -inline fun TestSingleplayerContext.waitForTile(pos: BlockPos): T -{ - clientContext.waitFor { client -> client.level?.getBlockEntity(pos) is T } - return server.computeOnServer { minecraftServer -> - val player = minecraftServer.playerList.players.first() - val level = player.level() - val tile = level.getBlockEntity(pos) as? T - tile ?: error("Tile not found at $pos") - } -} - -/** Places [state] at [pos], waits for its [BlockEntity] of type [T] to exist, then opens its menu for the test's player. */ -inline fun TestSingleplayerContext.placeTileAndOpenMenu(pos: BlockPos, state: BlockState) where T : BlockEntity, T : ExtendedMenuProvider -{ - server.runOnServer { minecraftServer -> - val player = minecraftServer.playerList.players.first() - val level = player.level() - level.setBlockAndUpdate(pos, state) - } - val tile = waitForTile(pos) - server.runOnServer { minecraftServer -> - val player = minecraftServer.playerList.players.first() - MenuRegistry.openExtendedMenu(player, tile) - } -} - -/** Combines [placeTileAndOpenMenu] and [waitForScreen]: places [state], opens its menu, then waits for [S] and runs [block]. */ -inline fun TestSingleplayerContext.placeTileAndWaitForScreen(pos: BlockPos, state: BlockState, noinline block: ComposeScreenTestContext.() -> Unit = {}) where T : BlockEntity, T : ExtendedMenuProvider, S : Screen, S : LayerManagerProvider -{ - placeTileAndOpenMenu(pos, state) - clientContext.waitForScreen(block) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt deleted file mode 100644 index be5a0238a..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt +++ /dev/null @@ -1,50 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import net.minecraft.gametest.framework.GameTestHelper - -/** - * Fails this GameTest via [GameTestHelper.fail] with [message] if [condition] is `false`. - */ -fun GameTestHelper.assertTrue(condition: Boolean, message: () -> String) -{ - if (!condition) { - fail(message()) - } -} - -/** - * Fails this GameTest via [GameTestHelper.fail] if [expected] and [actual] are not equal. - * - * @param message Failure message builder; defaults to reporting both values. - */ -fun GameTestHelper.assertEquals( - expected: T, - actual: T, - message: () -> String = { "Expected <$expected>, got <$actual>" }, -) -{ - if (expected != actual) { - fail(message()) - } -} - -/** - * Runs [block] and asserts it throws a [T], failing this GameTest via [GameTestHelper.fail] - * if [block] completes without throwing or throws a different exception type. - * - * @return The caught exception of type [T]. - */ -inline fun GameTestHelper.expectThrows(noinline block: () -> Unit): T -{ - return try { - block() - fail("Expected exception ${T::class.simpleName} to be thrown") - throw IllegalStateException("Unreachable") - } catch (t: Throwable) { - if (t is T) t - else { - fail("Expected ${T::class.simpleName}, got ${t::class.simpleName}") - throw IllegalStateException("Unreachable", t) - } - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt deleted file mode 100644 index 6745acc6a..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt +++ /dev/null @@ -1,21 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import net.minecraft.gametest.framework.GameTest -import net.minecraft.gametest.framework.GameTestHelper - -/** - * A trivially-succeeding placeholder, registered by [AGameTestPlatformInternal] on each loader - * when a mod's [AGameTestModFilter]-selected suite has no real test functions for the current - * [AGameTestSide] - e.g. a mod with only client-side coverage (like Archie-Test, whose own suite - * registers just [net.kernelpanicsoft.archie.test.gametest.TestScreenGameTest]) running its - * server invocation. Vanilla's `GameTestServer` refuses to boot with zero registered test - * functions at all (`IllegalArgumentException: No test functions were given!`); this keeps that - * boot trivially satisfied instead of crashing the whole invocation. - */ -@Suppress("unused") -class NoOpGameTest { - @GameTest(template = "archie:gametest/empty") - fun GameTestHelper.testNoOpPlaceholder() { - succeed() - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparer.kt deleted file mode 100644 index 8bb02e113..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparer.kt +++ /dev/null @@ -1,101 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import org.joml.Vector2i -import java.awt.Color -import java.awt.image.BufferedImage -import java.io.File -import javax.imageio.ImageIO -import kotlin.math.abs - -/** - * Screenshot comparison utility with support for both exact and fuzzy matching. - * Based on Fabric's TestScreenshotComparisonAlgorithms implementation. - */ -object ScreenshotComparer { - /** - * Compare two images for exact equality (all pixels must match). - * @return true if images are identical, false otherwise - */ - fun imagesEqual(templateFile: File, captureFile: File): Boolean { - val template = readImage(templateFile) ?: return false - val capture = readImage(captureFile) ?: return false - - if (template.width != capture.width || template.height != capture.height) { - return false - } - - val templateRgb = template.getRGB(0, 0, template.width, template.height, null, 0, template.width) - val captureRgb = capture.getRGB(0, 0, capture.width, capture.height, null, 0, capture.width) - - return templateRgb.contentEquals(captureRgb) - } - - /** - * Find a template image within a larger capture image (allowing sub-image matching). - * Uses exact pixel-by-pixel matching by default. - * @return top-left corner of the matched region, or null if not found - */ - fun findInImage(templateFile: File, captureFile: File, tolerance: Int = 0): Vector2i? { - val template = readImage(templateFile) ?: return null - val capture = readImage(captureFile) ?: return null - - if (template.width > capture.width || template.height > capture.height) { - return null - } - - val algorithm = if (tolerance > 0) { - MeanSquaredDifferenceAlgorithm(tolerance / 255.0f) - } else { - ExactScreenshotComparisonAlgorithm - } - - val templateRgb = template.getRGB(0, 0, template.width, template.height, null, 0, template.width) - val captureRgb = capture.getRGB(0, 0, capture.width, capture.height, null, 0, capture.width) - - val templateRawImage = RawImageImpl(template.width, template.height, templateRgb) - val captureRawImage = RawImageImpl(capture.width, capture.height, captureRgb) - - return algorithm.findColor(captureRawImage, templateRawImage) - } - - /** - * Find a template image using exact pixel matching. - */ - fun findInImageExact(templateFile: File, captureFile: File): Vector2i? { - return findInImage(templateFile, captureFile, tolerance = 0) - } - - /** - * Find a template image using fuzzy matching with configurable threshold. - * @param maxMeanSquaredDifference tolerance threshold (0.0-1.0) - */ - fun findInImageFuzzy(templateFile: File, captureFile: File, maxMeanSquaredDifference: Float = 0.005f): Vector2i? { - val template = readImage(templateFile) ?: return null - val capture = readImage(captureFile) ?: return null - - if (template.width > capture.width || template.height > capture.height) { - return null - } - - val algorithm = MeanSquaredDifferenceAlgorithm(maxMeanSquaredDifference) - - val templateRgb = template.getRGB(0, 0, template.width, template.height, null, 0, template.width) - val captureRgb = capture.getRGB(0, 0, capture.width, capture.height, null, 0, capture.width) - - val templateRawImage = RawImageImpl(template.width, template.height, templateRgb) - val captureRawImage = RawImageImpl(capture.width, capture.height, captureRgb) - - return algorithm.findColor(captureRawImage, templateRawImage) - } - - private fun readImage(file: File): BufferedImage? { - return try { - if (file.exists()) ImageIO.read(file) else null - } catch (e: Exception) { - null - } - } -} - - - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparisonAlgorithm.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparisonAlgorithm.kt deleted file mode 100644 index a3d53e291..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparisonAlgorithm.kt +++ /dev/null @@ -1,211 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -/** - * Comparison algorithm interface for screenshot matching. - * Supports both exact and fuzzy matching with configurable thresholds. - */ -interface ScreenshotComparisonAlgorithm { - /** - * Find a template pattern in a larger capture image using color data. - * @return top-left corner of matched region, or null if not found - */ - fun findColor(haystack: RawImage, needle: RawImage): org.joml.Vector2i? - - /** - * Find a template pattern in a larger capture image using grayscale data. - * @return top-left corner of matched region, or null if not found - */ - fun findGrayscale(haystack: RawImage, needle: RawImage): org.joml.Vector2i? - - /** - * Raw image data holder for comparison operations. - */ - interface RawImage { - fun width(): Int - fun height(): Int - fun data(): DATA - } -} - -/** - * Exact pixel matching algorithm - all pixels must match exactly. - */ -object ExactScreenshotComparisonAlgorithm : ScreenshotComparisonAlgorithm { - override fun findColor(haystack: ScreenshotComparisonAlgorithm.RawImage, needle: ScreenshotComparisonAlgorithm.RawImage): org.joml.Vector2i? { - val haystackData = haystack.data() - val needleData = needle.data() - val haystackWidth = haystack.width() - val needleWidth = needle.width() - val needleHeight = needle.height() - - if (needleWidth > haystackWidth || needleHeight > haystack.height()) { - return null - } - - for (needleY in 0..(haystack.height() - needleHeight)) { - for (needleX in 0..(haystackWidth - needleWidth)) { - var match = true - for (y in 0 until needleHeight) { - for (x in 0 until needleWidth) { - val haystackColor = haystackData[(needleY + y) * haystackWidth + needleX + x] - val needleColor = needleData[y * needleWidth + x] - if (haystackColor != needleColor) { - match = false - break - } - } - if (!match) break - } - if (match) { - return org.joml.Vector2i(needleX, needleY) - } - } - } - return null - } - - override fun findGrayscale(haystack: ScreenshotComparisonAlgorithm.RawImage, needle: ScreenshotComparisonAlgorithm.RawImage): org.joml.Vector2i? { - val haystackData = haystack.data() - val needleData = needle.data() - val haystackWidth = haystack.width() - val needleWidth = needle.width() - val needleHeight = needle.height() - - if (needleWidth > haystackWidth || needleHeight > haystack.height()) { - return null - } - - for (needleY in 0..(haystack.height() - needleHeight)) { - for (needleX in 0..(haystackWidth - needleWidth)) { - var match = true - for (y in 0 until needleHeight) { - for (x in 0 until needleWidth) { - val haystackLuminance = haystackData[(needleY + y) * haystackWidth + needleX + x] - val needleLuminance = needleData[y * needleWidth + x] - if (haystackLuminance != needleLuminance) { - match = false - break - } - } - if (!match) break - } - if (match) { - return org.joml.Vector2i(needleX, needleY) - } - } - } - return null - } -} - -/** - * Mean squared difference algorithm - allows fuzzy matching within a tolerance threshold. - * Based on Fabric's TestScreenshotComparisonAlgorithms implementation. - */ -data class MeanSquaredDifferenceAlgorithm(val maxMeanSquaredDifference: Float = 0.005f) : ScreenshotComparisonAlgorithm { - override fun findColor(haystack: ScreenshotComparisonAlgorithm.RawImage, needle: ScreenshotComparisonAlgorithm.RawImage): org.joml.Vector2i? { - val haystackData = haystack.data() - val needleData = needle.data() - val haystackWidth = haystack.width() - val needleWidth = needle.width() - val needleHeight = needle.height() - - if (needleWidth > haystackWidth || needleHeight > haystack.height()) { - return null - } - - // Threshold calculation to avoid floating point in inner loop - val threshold = (maxMeanSquaredDifference * needleWidth * needleHeight * 3 * 255 * 255).toLong() - - for (needleY in 0..(haystack.height() - needleHeight)) { - for (needleX in 0..(haystackWidth - needleWidth)) { - var sumSquaredDifference = 0L - var match = true - - for (y in 0 until needleHeight) { - for (x in 0 until needleWidth) { - val haystackColor = haystackData[(needleY + y) * haystackWidth + needleX + x] - val haystackRed = (haystackColor shr 16) and 0xFF - val haystackGreen = (haystackColor shr 8) and 0xFF - val haystackBlue = haystackColor and 0xFF - - val needleColor = needleData[y * needleWidth + x] - val needleRed = (needleColor shr 16) and 0xFF - val needleGreen = (needleColor shr 8) and 0xFF - val needleBlue = needleColor and 0xFF - - val diffRed = haystackRed - needleRed - val diffGreen = haystackGreen - needleGreen - val diffBlue = haystackBlue - needleBlue - - sumSquaredDifference += (diffRed * diffRed + diffGreen * diffGreen + diffBlue * diffBlue).toLong() - - if (sumSquaredDifference >= threshold) { - match = false - break - } - } - if (!match) break - } - - if (match) { - return org.joml.Vector2i(needleX, needleY) - } - } - } - return null - } - - override fun findGrayscale(haystack: ScreenshotComparisonAlgorithm.RawImage, needle: ScreenshotComparisonAlgorithm.RawImage): org.joml.Vector2i? { - val haystackData = haystack.data() - val needleData = needle.data() - val haystackWidth = haystack.width() - val needleWidth = needle.width() - val needleHeight = needle.height() - - if (needleWidth > haystackWidth || needleHeight > haystack.height()) { - return null - } - - val threshold = (maxMeanSquaredDifference * needleWidth * needleHeight * 255 * 255).toLong() - - for (needleY in 0..(haystack.height() - needleHeight)) { - for (needleX in 0..(haystackWidth - needleWidth)) { - var sumSquaredDifference = 0L - var match = true - - for (y in 0 until needleHeight) { - for (x in 0 until needleWidth) { - val haystackLuminance = haystackData[(needleY + y) * haystackWidth + needleX + x].toInt() and 0xFF - val needleLuminance = needleData[y * needleWidth + x].toInt() and 0xFF - val diff = haystackLuminance - needleLuminance - - sumSquaredDifference += (diff * diff).toLong() - - if (sumSquaredDifference >= threshold) { - match = false - break - } - } - if (!match) break - } - - if (match) { - return org.joml.Vector2i(needleX, needleY) - } - } - } - return null - } -} - -/** - * Raw image data implementation for comparison operations. - */ -data class RawImageImpl(val width: Int, val height: Int, val data: DATA) : ScreenshotComparisonAlgorithm.RawImage { - override fun width() = width - override fun height() = height - override fun data() = data -} - - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotManager.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotManager.kt deleted file mode 100644 index 24c92301a..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotManager.kt +++ /dev/null @@ -1,54 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import java.nio.file.Files -import java.nio.file.Path -import java.nio.file.Paths - -/** - * Manages screenshot file I/O, template storage, and directory organization. - */ -object ScreenshotManager { - private fun getScreenshotBaseDir(): Path { - val baseDir = Paths.get("build", "gametests", "screenshots") - Files.createDirectories(baseDir) - return baseDir - } - - /** - * Get directory for captured test screenshots. - */ - fun getCaptureDir(): Path { - val dir = getScreenshotBaseDir().resolve("captures") - Files.createDirectories(dir) - return dir - } - - /** - * Get directory for screenshot templates (baselines). - */ - fun getTemplateDir(): Path { - val dir = getScreenshotBaseDir().resolve("templates") - Files.createDirectories(dir) - return dir - } - - /** - * Resolve a template image file by name. - * Searches in template directory with .png extension. - */ - fun resolveTemplate(templateName: String): Path { - val filename = if (templateName.endsWith(".png")) templateName else "$templateName.png" - return getTemplateDir().resolve(filename) - } - - /** - * Generate a unique capture filename for a test screenshot. - */ - fun generateCapturePath(testId: String, screenshotName: String): Path { - val sanitized = (testId + "_" + screenshotName) - .replace(Regex("[^a-zA-Z0-9_\\-.]"), "_") - val filename = if (sanitized.endsWith(".png")) sanitized else "$sanitized.png" - return getCaptureDir().resolve(filename) - } -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt deleted file mode 100644 index 08367c1bb..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt +++ /dev/null @@ -1,457 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import net.minecraft.client.Minecraft -import java.util.concurrent.Phaser -import java.util.concurrent.Semaphore -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException -import java.util.concurrent.atomic.AtomicReference - -/** - * Tracks which server instance's tick thread currently owns the shared "server" phaser slot. - * - * The `ServerMixin` (per-loader, in `src/main/mixin`) this bridge backs is applied to every - * [net.minecraft.server.MinecraftServer] instance - both the integrated singleplayer server and - * an in-process dedicated GameTest server can exist back-to-back (or briefly overlap during - * teardown/startup). Since only one - * "server" participant can safely register with [ThreadingImpl.phaser] at a time, this - * identifies the owning thread so a late call from an already-superseded instance can't - * deregister or arrive on behalf of a different, currently-active instance. - */ -private val serverRegisteredThread = AtomicReference(null) - -/** - * Shared client gametest threading bridge inspired by Fabric's ThreadingImpl. - * - * Uses a Phaser for tick-phase barriers and semaphores for task handoff from - * the gametest thread to client/server threads. - */ -object ThreadingImpl { - private const val THREAD_IMPL_CLASS_NAME = "net.kernelpanicsoft.archie.gametest.ThreadingImpl" - private const val TASK_ON_THIS_THREAD_METHOD_NAME = "runTaskOnThisThread" - private const val TASK_ON_OTHER_THREAD_METHOD_NAME = "runTaskOnOtherThread" - private const val PHASE_MASK = 3 - private const val PHASE_TICK = 0 - private const val PHASE_CLIENT_TASKS = 1 - private const val PHASE_SERVER_TASKS = 2 - private const val PHASE_TEST = 3 - - private val clientSemaphore = Semaphore(0) - private val serverSemaphore = Semaphore(0) - private val testSemaphore = Semaphore(0) - private val phaser = Phaser(0) - - @Volatile - private var clientCanAcceptTasks: Boolean = false - - @Volatile - private var serverCanAcceptTasks: Boolean = false - - @Volatile - private var clientRegistered: Boolean = false - - @Volatile - private var testRegistered: Boolean = false - - @Volatile - private var taskToRun: Runnable? = null - - @Volatile - private var testThread: Thread? = null - - @Volatile - var testFailureException: Throwable? = null - private set - - @Volatile - private var gameCrashed: Boolean = false - - // The phase each tick source last called phaser.arrive() for. onClientTick()/onServerTick() - // fire every real tick regardless of whether the test thread has caught up and arrived for - // the current phase yet - Phaser requires each registered party to arrive at most once per - // phase, so without this guard, two ticks landing before the test thread's next arrival (more - // likely under CI's slower/more contended scheduling - never reproduced on a fast local - // machine) throws "Attempted arrival of unregistered party" on the second one. Reading the - // phase this call actually arrived for straight off arrive()'s return value (rather than a - // separate phaser.phase read beforehand) avoids a TOCTOU gap between checking and arriving. - // Reset to -1 on (re-)registration in onClientRunStart()/onServerRunStart() - the phase - // counter doesn't reset when a party deregisters, so a stale value surviving into a new - // registration could wrongly skip that new party's first required arrival and stall forever. - @Volatile - private var clientLastArrivedPhase: Int = -1 - - @Volatile - private var serverLastArrivedPhase: Int = -1 - - @JvmStatic - fun runTestThread(testRunner: () -> Unit) { - check(testThread == null) { "There is already a test thread running" } - testFailureException = null - clientCanAcceptTasks = false - serverCanAcceptTasks = false - - val thread = Thread { - if (!testRegistered) { - synchronized(this) { - if (!testRegistered) { - phaser.register() - testRegistered = true - } - } - } - - try { - testRunner() - } catch (failure: Throwable) { - testFailureException = failure - } finally { - synchronized(this) { - if (testRegistered) { - testRegistered = false - phaser.arriveAndDeregister() - } - } - val capturedFailure = testFailureException - testThread = null - if (capturedFailure != null) { - Minecraft.getInstance().execute { - throw capturedFailure - } - } - } - } - thread.name = "Archie Client GameTest Thread" - thread.isDaemon = true - testThread = thread - thread.start() - } - - @JvmStatic - fun checkOnGametestThread(methodName: String) { - check(isOnGametestThread()) { - "$methodName can only be called from the client gametest thread" - } - } - - @JvmStatic - fun isOnGametestThread(): Boolean = Thread.currentThread() === testThread - - @JvmStatic - fun onClientRunStart() { - gameCrashed = false - if (!clientRegistered) { - synchronized(this) { - if (!clientRegistered) { - phaser.register() - clientRegistered = true - // A fresh registration must not inherit a previous instance's arrival - // history - the phaser's phase counter doesn't reset just because the - // prior party deregistered, so a stale value here could make this new - // party's first tick wrongly believe it already arrived for the current - // phase, permanently stalling that phase (see clientLastArrivedPhase kdoc). - clientLastArrivedPhase = -1 - } - } - } - } - - @JvmStatic - fun onClientRunStop() { - clientCanAcceptTasks = false - serverCanAcceptTasks = false - - synchronized(this) { - if (clientRegistered) { - clientRegistered = false - phaser.arriveAndDeregister() - } - } - - // Force-release the server slot regardless of which instance holds it - the client is - // shutting down entirely, so nothing should be left registered afterward. - if (serverRegisteredThread.getAndSet(null) != null) { - synchronized(this) { - phaser.arriveAndDeregister() - } - } - } - - @JvmStatic - fun onServerRunStart() { - val current = Thread.currentThread() - if (serverRegisteredThread.compareAndSet(null, current)) { - synchronized(this) { - phaser.register() - // See the matching comment in onClientRunStart() - a new server instance - // (e.g. an integrated singleplayer server starting after an earlier dedicated - // GameTest server already registered, arrived, and deregistered) must not - // inherit the previous instance's last-arrived phase. - serverLastArrivedPhase = -1 - } - } - // If another server instance's thread already holds the slot (e.g. an integrated - // server that hasn't finished tearing down yet), this instance simply won't - // participate in tick-phase sync until that one releases it - see onServerTick(). - } - - @JvmStatic - fun onServerRunStop() { - serverCanAcceptTasks = false - - val current = Thread.currentThread() - if (serverRegisteredThread.compareAndSet(current, null)) { - synchronized(this) { - phaser.arriveAndDeregister() - } - } - // If this thread never held the slot, it never registered either - nothing to release. - } - - @JvmStatic - fun setGameCrashed() { - gameCrashed = true - onClientRunStop() - } - - @JvmStatic - fun onClientTick() { - if (testThread == null && !testRegistered) return - - if (!clientRegistered) { - synchronized(this) { - if (!clientRegistered) { - phaser.register() - clientRegistered = true - } - } - } - - clientCanAcceptTasks = true - - if (clientSemaphore.tryAcquire()) { - taskToRun?.run() - } - - if (clientRegistered && phaser.phase != clientLastArrivedPhase) { - clientLastArrivedPhase = phaser.arrive() - } - } - - @JvmStatic - fun preRunTasks() { - if (!isThreadingActive()) return - } - - @JvmStatic - fun postRunTasks() { - if (!isThreadingActive()) return - - clientCanAcceptTasks = true - - while (clientSemaphore.tryAcquire()) { - val task = taskToRun ?: break - task.run() - } - } - - @JvmStatic - fun onServerTick() { - if (testThread == null && !testRegistered) return - - val current = Thread.currentThread() - if (serverRegisteredThread.compareAndSet(null, current)) { - synchronized(this) { - phaser.register() - } - } - - if (serverRegisteredThread.get() !== current) { - // Another server instance already owns the shared slot (e.g. this is a dedicated - // GameTest server ticking while the integrated server hasn't finished tearing - // down yet). Don't touch the semaphore/phaser on its behalf. - return - } - - serverCanAcceptTasks = true - - if (serverSemaphore.tryAcquire()) { - taskToRun?.run() - } - - if (phaser.phase != serverLastArrivedPhase) { - serverLastArrivedPhase = phaser.arrive() - } - } - - @Suppress("unused") - @JvmStatic - fun runOnClient(action: () -> Unit) { - checkOnGametestThread("runOnClient") - ensureDispatchPhase() - check(clientCanAcceptTasks) { "runOnClient called when no client is running" } - runTaskOnOtherThread(action, clientSemaphore) - } - - @Suppress("unused") - @JvmStatic - fun runOnServer(action: () -> Unit) { - checkOnGametestThread("runOnServer") - ensureDispatchPhase() - check(serverCanAcceptTasks) { - "runOnServer called when no server is running " + - "(serverRegisteredThread=${serverRegisteredThread.get()?.name}, " + - "testRegistered=$testRegistered, testThread=${testThread?.name}, phase=${getCurrentPhase()})" - } - runTaskOnOtherThread(action, serverSemaphore) - } - - private fun ensureDispatchPhase() { - // Intentionally no-op for the current client harness bridge. - // Dispatch relies on non-blocking client/server loop integration. - } - - private fun runTaskOnOtherThread(action: () -> Unit, targetSemaphore: Semaphore) { - val thrown = AtomicReference(null) - taskToRun = Runnable { runTaskOnThisThread(action, thrown) } - - targetSemaphore.release() - - try { - val acquired = testSemaphore.tryAcquire(10, TimeUnit.SECONDS) - check(acquired) { - "Timed out waiting for cross-thread task completion " + - "(phase=${getCurrentPhase()}, nextPhase=${getNextPhase()}, " + - "clientCanAcceptTasks=$clientCanAcceptTasks, serverCanAcceptTasks=$serverCanAcceptTasks, " + - "target=${if (targetSemaphore === clientSemaphore) "client" else "server"}, " + - "taskPending=${taskToRun != null}, testThreadAlive=${testThread?.isAlive == true})" - } - } catch (e: InterruptedException) { - throw RuntimeException(e) - } - - val error = thrown.get() - if (error != null) { - joinAsyncStackTrace(error) - throw error - } - } - - private fun runTaskOnThisThread(action: () -> Unit, thrown: AtomicReference) { - try { - action() - } catch (e: Throwable) { - thrown.set(e) - } finally { - taskToRun = null - testSemaphore.release() - } - } - - private fun joinAsyncStackTrace(error: Throwable) { - if (System.getProperty("fabric.client.gametest.disableJoinAsyncStackTraces") != null) { - return - } - - val otherThreadStackTrace = error.stackTrace ?: return - var otherThreadIndex = otherThreadStackTrace.size - 1 - while (otherThreadIndex >= 0) { - val element = otherThreadStackTrace[otherThreadIndex] - if (THREAD_IMPL_CLASS_NAME == element.className && TASK_ON_THIS_THREAD_METHOD_NAME == element.methodName) { - break - } - otherThreadIndex-- - } - - if (otherThreadIndex == -1) { - return - } - - val thisThreadStackTrace = Thread.currentThread().stackTrace - var thisThreadIndex = 0 - while (thisThreadIndex < thisThreadStackTrace.size) { - val element = thisThreadStackTrace[thisThreadIndex] - if (THREAD_IMPL_CLASS_NAME == element.className && TASK_ON_OTHER_THREAD_METHOD_NAME == element.methodName) { - break - } - thisThreadIndex++ - } - - if (thisThreadIndex == thisThreadStackTrace.size) { - return - } - - val joinedStackTrace = arrayOfNulls( - (otherThreadIndex + 1) + 1 + (thisThreadStackTrace.size - thisThreadIndex), - ) - System.arraycopy(otherThreadStackTrace, 0, joinedStackTrace, 0, otherThreadIndex + 1) - joinedStackTrace[otherThreadIndex + 1] = StackTraceElement("Async Stack Trace", ".", null, 1) - System.arraycopy( - thisThreadStackTrace, - thisThreadIndex, - joinedStackTrace, - otherThreadIndex + 2, - thisThreadStackTrace.size - thisThreadIndex, - ) - @Suppress("UNCHECKED_CAST") - error.stackTrace = joinedStackTrace as Array - } - - @JvmStatic - fun awaitTicks(ticks: Int, timeoutMillis: Long): Boolean { - if (gameCrashed) return false - if (ticks <= 0) return true - - if (!testRegistered) { - synchronized(this) { - if (!testRegistered) { - phaser.register() - testRegistered = true - } - } - } - - val deadline = System.nanoTime() + TimeUnit.MILLISECONDS.toNanos(timeoutMillis.coerceAtLeast(1L)) - repeat(ticks) { - val phase = advanceToNextTickPhase() - val remainingNanos = deadline - System.nanoTime() - if (remainingNanos <= 0L) return false - - try { - phaser.awaitAdvanceInterruptibly(phase, remainingNanos, TimeUnit.NANOSECONDS) - } catch (_: TimeoutException) { - return false - } catch (_: InterruptedException) { - Thread.currentThread().interrupt() - return false - } - } - - return true - } - - @Suppress("unused") - private fun getCurrentPhase(): Int = (phaser.phase - 1) and PHASE_MASK - - @Suppress("unused") - private fun getNextPhase(): Int = phaser.phase and PHASE_MASK - - @Suppress("unused") - private fun enterPhase(phase: Int) { - while (getNextPhase() != phase) { - phaser.arriveAndAwaitAdvance() - } - - // After aligning to the requested next phase, participate in that - // phase barrier as well. Without this, callers can observe the phase - // but not synchronize with peer threads at the same boundary. - phaser.arriveAndAwaitAdvance() - } - - private fun advanceToNextTickPhase(): Int { - check(PHASE_TICK == 0 && PHASE_CLIENT_TASKS == 1 && PHASE_SERVER_TASKS == 2 && PHASE_TEST == 3) - return phaser.arrive() - } - - private fun isThreadingActive(): Boolean = testThread != null || testRegistered -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/VerboseTestReporter.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/VerboseTestReporter.kt deleted file mode 100644 index bb06bfd63..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/VerboseTestReporter.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import dev.architectury.platform.Platform -import net.kernelpanicsoft.archie.Archie -import net.minecraft.gametest.framework.GameTestInfo -import net.minecraft.gametest.framework.TestReporter -import net.minecraft.resources.ResourceLocation - -/** Logs each GameTest's pass/fail result through [Archie.LOGGER] as it completes. */ -object VerboseTestReporter : TestReporter -{ - override fun onTestFailed(testInfo: GameTestInfo) - { - Archie.LOGGER.error("[GameTest] FAIL {}", testId(testInfo), testInfo.error) - } - - override fun onTestSuccess(testInfo: GameTestInfo) - { - - Archie.LOGGER.info("[GameTest] PASS {}", testId(testInfo)) - } - - /** A human-readable id for [testInfo]: `":"`, or just the test name if the owning mod isn't loaded. */ - fun testId(testInfo: GameTestInfo): String - { - val testModId = ResourceLocation.parse(testInfo.structureName).namespace - if (!Platform.isModLoaded(testModId)) return testInfo.testName - return "${testModId}:${testInfo.testName}" - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt deleted file mode 100644 index 63dc0d8d8..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt +++ /dev/null @@ -1,327 +0,0 @@ -package net.kernelpanicsoft.archie.gametest.junit - -import java.nio.channels.FileChannel -import java.nio.file.Path -import java.nio.file.StandardOpenOption -import java.time.Duration -import java.util.concurrent.CompletableFuture -import java.util.concurrent.CompletionException -import java.util.concurrent.ConcurrentHashMap -import java.util.concurrent.Executors -import java.util.concurrent.TimeUnit -import java.util.concurrent.atomic.AtomicBoolean -import java.util.concurrent.locks.ReentrantLock -import kotlin.io.path.appendText -import kotlin.io.path.createDirectories -import kotlin.io.path.exists -import kotlin.io.path.outputStream -import kotlin.io.path.readLines - -/** One test's pass/fail outcome, parsed from a GameTest invocation's log output. */ -internal data class TestResult( - val testId: String, - val passed: Boolean, -) - -/** The outcome of one [GameTestGradleExecutor.start] invocation, including per-test results parsed from its log. */ -internal data class GameTestGradleResult( - val success: Boolean, - val command: List, - val exitCode: Int, - val logFile: Path, - val logTail: String, - val testResults: Map = emptyMap(), // testId -> TestResult -) - -/** - * A [GameTestGradleInvocation]'s Gradle process, already running by the time this is returned. - * `liveTestResults` is populated live as PASS/FAIL lines are parsed from the process's output, so - * a caller can report an individual test as soon as it's known instead of waiting for [result] - * (the invocation's overall outcome) to complete. - */ -internal class GameTestGradleHandle( - val invocation: GameTestGradleInvocation, - private val liveTestResults: ConcurrentHashMap, - val result: CompletableFuture, -) { - /** - * Blocks until [testId] has been observed in the live log output, or [result] completes - - * whichever happens first. Returns `null` if the invocation finished without ever reporting - * a result for [testId] (e.g. it wasn't reached before a crash/timeout). - */ - fun awaitTestResult(testId: String, pollInterval: Duration = Duration.ofMillis(200)): TestResult? { - while (!result.isDone) { - liveTestResults[testId]?.let { return it } - Thread.sleep(pollInterval.toMillis()) - } - return liveTestResults[testId] - } -} - -/** Shells out to the Gradle wrapper to run one [GameTestGradleInvocation], capturing and parsing its log output. */ -internal object GameTestGradleExecutor { - /** Backs every in-flight invocation's blocking wait-for-exit; sized generously since these are I/O-bound, not CPU-bound. */ - private val ioExecutor = Executors.newCachedThreadPool { runnable -> - Thread(runnable, "archie-gametest-runner").apply { isDaemon = true } - } - - /** - * Every invocation is a separate --no-daemon Gradle process, and (at least for Archie-Test) - * they all depend on the same upstream composite-build artifact (e.g. Archie:common's - * remapped jar) - starting all of them at once races multiple processes rebuilding/rewriting - * that shared output concurrently, corrupting it (observed: `:Archie:common:remapJar FAILED - * ... ZipException: invalid stored block lengths`). Whichever invocation calls [start] first - * claims [primingClaimed] and runs a single, fast `assemble` build to force those shared - * outputs to exist once; everyone else blocks on [primingComplete] until that finishes. Once - * it's done, Gradle's up-to-date checks mean every invocation's own process only reads the - * already-built artifact, so all of them - including the priming one - start their real - * (long-running) invocation concurrently right after, instead of one invocation blocking - * every other one on its entire run. - * - * This alone only serializes invocations launched from the same JVM. `Archie`'s and - * `Archie-Test`'s own `:*:test` tasks each run in a *separate* Gradle test JVM, but - * Archie-Test composite-includes `../Archie` (see its settings.gradle.kts), so both JVMs' - * priming runs `assemble` against the very same `Archie:common` build output concurrently - - * this in-process guard does nothing across that boundary (observed: - * `:common:remapJar FAILED ... NoSuchFileException: archie-common-1.0.0.jar.tmp`, one - * process's remap temp file vanishing out from under the other). [withCrossProcessPrimingLock] - * closes that gap with an OS-level file lock shared by both JVMs. - */ - private val primingClaimed = AtomicBoolean(false) - private val primingComplete = CompletableFuture() - - /** - * Starts [invocation]'s Gradle task via `ProcessBuilder` and returns immediately with a - * [GameTestGradleHandle] tracking it - the process itself, and the background threads - * streaming its output, are already running. This lets a caller [start] every matrix entry - * up front so independent invocations run concurrently, instead of only starting the next - * one once a prior invocation's JUnit node happens to execute. - */ - fun start( - invocation: GameTestGradleInvocation, - timeout: Duration, - workspaceRoot: Path, - ): GameTestGradleHandle { - if (primingClaimed.compareAndSet(false, true)) { - runCatching { primeSharedBuildOutputs(workspaceRoot) } - .onSuccess { primingComplete.complete(null) } - .onFailure { primingComplete.completeExceptionally(it) } - .getOrThrow() - } else { - try { - primingComplete.join() - } catch (e: CompletionException) { - throw IllegalStateException("Shared build output priming failed; see cause", e.cause ?: e) - } - } - - val liveTestResults = ConcurrentHashMap() - val result = CompletableFuture.supplyAsync( - { withLoaderRunLock(workspaceRoot, invocation.loader) { runProcess(invocation, timeout, workspaceRoot, liveTestResults) } }, - ioExecutor, - ) - - return GameTestGradleHandle(invocation, liveTestResults, result) - } - - /** - * Runs a single, fast `assemble` (compiles and packages every subproject, including - * composite-included ones, without launching anything) so the shared upstream artifacts every - * matrix invocation depends on exist before any of them starts its own (much longer) process. - * Deliberately generic (not a hardcoded task path) so it works the same for both Archie's and - * Archie-Test's workspace roots. - * - * Wrapped in [withCrossProcessPrimingLock] since [primingClaimed] only guards against races - * within this JVM - see its doc comment. - */ - private fun primeSharedBuildOutputs(workspaceRoot: Path) { - withCrossProcessPrimingLock { - val logsDir = workspaceRoot.resolve("build/tmp/junit-gametest-runner").createDirectories() - val logFile = logsDir.resolve("priming.log") - - val wrapper = resolveGradleWrapper(workspaceRoot) - val command = listOf(wrapper.toString(), "assemble", "--console=plain", "--no-daemon") - - val process = ProcessBuilder(command) - .directory(workspaceRoot.toFile()) - .redirectErrorStream(true) - .start() - - logFile.outputStream().bufferedWriter().use { writer -> - process.inputStream.bufferedReader().useLines { lines -> - lines.forEach { line -> - println(line) - writer.appendLine(line) - } - } - } - - val exitCode = process.waitFor() - check(exitCode == 0) { - "Priming build ('${command.joinToString(" ")}') failed with exit code $exitCode. Log file: $logFile\n--- Log tail ---\n${tail(logFile)}" - } - } - } - - /** - * Runs [action] while holding an OS-level advisory lock on a fixed file under the system - * temp directory, blocking until it's acquired. `Archie`'s and `Archie-Test`'s `:*:test` - * tasks each spawn their own JVM (this object's in-process guards don't share state between - * them), but both machines' priming runs ultimately `assemble` the same physical - * `Archie:common` build output when run on the same machine (Archie-Test composite-includes - * `../Archie`) - this lock is what actually serializes them. Scoped to the whole machine - * rather than a specific workspace path since that's simpler and there's only ever one such - * priming race to guard against per machine (CI runner or dev box). - */ - private fun withCrossProcessPrimingLock(action: () -> T): T { - val lockFile = Path.of(System.getProperty("java.io.tmpdir"), "archie-gametest-priming.lock") - FileChannel.open(lockFile, StandardOpenOption.CREATE, StandardOpenOption.WRITE).use { channel -> - channel.lock().use { - return action() - } - } - } - - /** Per-(workspaceRoot, loader) in-JVM locks backing [withLoaderRunLock]. */ - private val loaderRunLocks = ConcurrentHashMap, ReentrantLock>() - - /** - * Runs [action] while holding an in-JVM lock scoped to [workspaceRoot] and [loader], blocking - * (whichever `ioExecutor` thread is running the invocation, never the caller of [start]) until - * it's acquired. - * - * Every invocation for one workspaceRoot is spawned as a child --no-daemon Gradle process from - * the same `:common:test` JVM, so a plain in-JVM lock is enough here - unlike - * [withCrossProcessPrimingLock], which guards a race between *separate* JVMs (Archie's and - * Archie-Test's own `:*:test` tasks) and needs an OS-level file lock. (A `FileChannel` lock - * would be wrong here for a different reason too: `java.nio.channels.FileLock` throws - * `OverlappingFileLockException` rather than blocking when a *second* lock on the same file - * is requested from within the same JVM - it's designed to guard against other processes, not - * queue other threads in this one.) - * - * Unlike [withCrossProcessPrimingLock]'s one-time shared-artifact priming, this serializes - * the *actual* invocation runs for the same loader (e.g. fabric:server and fabric:client), - * which both depend on and mutate that loader subproject's own build outputs - * (`:fabric:processResources` etc.) via their own separate, concurrently-launched - * --no-daemon Gradle processes. Without this, one invocation's spawned Minecraft process can - * read e.g. `fabric.mod.json` straight off disk at the exact moment the other invocation's - * own build is mid-rewrite of that same file - observed as a `ParseMetadataException: - * ... EOFException` from a momentarily-empty `fabric.mod.json`, cascading into every - * unrelated server test in that suite reporting a spurious failure. Different loaders (and - * different workspace roots) get different locks, so fabric and neoforge invocations - and - * Archie's vs Archie-Test's own invocations - still run fully in parallel. - */ - private fun withLoaderRunLock(workspaceRoot: Path, loader: Loader, action: () -> T): T { - val lock = loaderRunLocks.computeIfAbsent(workspaceRoot to loader) { ReentrantLock() } - lock.lock() - try { - return action() - } finally { - lock.unlock() - } - } - - private fun runProcess( - invocation: GameTestGradleInvocation, - timeout: Duration, - workspaceRoot: Path, - liveTestResults: ConcurrentHashMap, - ): GameTestGradleResult { - val logsDir = workspaceRoot.resolve("build/tmp/junit-gametest-runner").createDirectories() - val logFile = logsDir.resolve("${invocation.id.replace(':', '-')}.log") - - val wrapper = resolveGradleWrapper(workspaceRoot) - val command = mutableListOf(wrapper.toString()) - command += invocation.taskPath - command += "--console=plain" - command += "--no-daemon" - - val extraArgs = System.getProperty("archie.junit.gametest.extraArgs")?.trim().orEmpty() - if (extraArgs.isNotEmpty()) { - command.addAll(extraArgs.split(Regex("\\s+"))) - } - - val process = ProcessBuilder(command) - .directory(workspaceRoot.toFile()) - .redirectErrorStream(true) - .start() - - val testPattern = when (invocation.side) { - Side.SERVER -> Regex("""\[GameTest] (PASS|FAIL) (.+)""") - Side.CLIENT -> Regex("""\[ClientGameTest] (PASS|FAIL) (.+)""") - } - - val outputPump = Thread { - logFile.outputStream().bufferedWriter().use { writer -> - process.inputStream.bufferedReader().useLines { lines -> - lines.forEach { line -> - println(line) - writer.appendLine(line) - writer.flush() - - val match = testPattern.find(line) - if (match != null) { - val passed = match.groupValues[1] == "PASS" - val testId = match.groupValues[2].trim() - liveTestResults[testId] = TestResult(testId, passed) - } - } - } - } - }.apply { - name = "archie-gametest-output-${invocation.id}" - isDaemon = true - start() - } - - return awaitCompletion(process, outputPump, timeout, logFile, command, liveTestResults) - } - - private fun awaitCompletion( - process: Process, - outputPump: Thread, - timeout: Duration, - logFile: Path, - command: List, - liveTestResults: Map, - ): GameTestGradleResult { - val finished = process.waitFor(timeout.toMillis(), TimeUnit.MILLISECONDS) - val exitCode = if (finished) process.exitValue() else { - process.destroyForcibly() - process.waitFor() - -1 - } - - outputPump.join(5_000) - - if (!finished) { - logFile.appendText("\n[runner] Timed out after ${timeout.toMinutes()} minute(s).\n") - } - - val tail = tail(logFile) - return GameTestGradleResult( - success = finished && exitCode == 0, - command = command, - exitCode = exitCode, - logFile = logFile, - logTail = tail, - testResults = liveTestResults.toMap(), - ) - } - - private fun resolveGradleWrapper(workspaceRoot: Path): Path { - val unix = workspaceRoot.resolve("gradlew") - if (unix.exists()) return unix - - val windows = workspaceRoot.resolve("gradlew.bat") - if (windows.exists()) return windows - - error("Could not locate Gradle wrapper in $workspaceRoot") - } - - private fun tail(file: Path): String { - if (!file.exists()) return "" - val lines = file.readLines() - return lines.takeLast(120).joinToString("\n") - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt deleted file mode 100644 index aedb7de4f..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt +++ /dev/null @@ -1,60 +0,0 @@ -package net.kernelpanicsoft.archie.gametest.junit - -/** A mod loader [GameTestRunner] can launch a GameTest Gradle task for. */ -enum class Loader { - FABRIC, - NEOFORGE, -} - -/** The GameTest side (matches [net.kernelpanicsoft.archie.gametest.AGameTestSide]) to launch. */ -enum class Side { - SERVER, - CLIENT, -} - -/** One `loader:side` entry from [GameTestRunner]'s matrix, resolving to a single Gradle [taskPath] to run. */ -data class GameTestGradleInvocation( - val loader: Loader, - val side: Side, -) { - /** The fully-qualified Gradle task path that launches this invocation, e.g. `:fabric:runGametest`. */ - val taskPath: String - get() = when (loader) { - Loader.FABRIC -> when (side) { - Side.SERVER -> ":fabric:runGametest" - Side.CLIENT -> ":fabric:runGametestClient" - } - - Loader.NEOFORGE -> when (side) { - Side.SERVER -> ":neoforge:runGametest" - Side.CLIENT -> ":neoforge:runGametestClient" - } - } - - /** A short id for this invocation, e.g. `"fabric:server"`, used in test/container display names. */ - val id: String - get() = "${loader.name.lowercase()}:${side.name.lowercase()}" - - companion object { - /** - * Parses a comma-separated list of `loader:side` tokens (e.g. `"fabric:server,neoforge:client"`) - * into invocations, as used by [GameTestRunner.PROP_MATRIX]. - * - * @throws IllegalArgumentException if a token isn't in `loader:side` form or names an unknown [Loader]/[Side]. - */ - fun parseMatrix(value: String): List { - if (value.isBlank()) return emptyList() - return value.split(',').map { token -> - val parts = token.trim().split(':') - require(parts.size == 2) { - "Invalid matrix token '$token'. Expected format :, e.g. fabric:server" - } - GameTestGradleInvocation( - loader = Loader.valueOf(parts[0].trim().uppercase()), - side = Side.valueOf(parts[1].trim().uppercase()), - ) - } - } - } -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt deleted file mode 100644 index c50e60a01..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt +++ /dev/null @@ -1,159 +0,0 @@ -package net.kernelpanicsoft.archie.gametest.junit - -import net.kernelpanicsoft.archie.events.AEvents -import net.kernelpanicsoft.archie.gametest.ClientGameTest -import net.minecraft.gametest.framework.GameTest -import org.junit.jupiter.api.Assumptions.assumeTrue -import org.junit.jupiter.api.DynamicContainer -import org.junit.jupiter.api.DynamicTest -import java.net.URI -import java.nio.file.Path -import java.time.Duration -import kotlin.collections.forEach - -/** - * Bridges Archie's Loom-driven GameTests into a regular JUnit 5 run, so `./gradlew test` (or an - * IDE test runner) can launch `runGametest`/`runGametestClient` for a `loader:side` matrix and - * report each declared test as its own JUnit [DynamicTest], parsed from the launched process's - * log output. - * - * Disabled by default (opt-in via [PROP_ENABLED]) since it shells out to Gradle and boots a full - * Minecraft process per matrix entry. - */ -object GameTestRunner -{ - /** System property (`-D...=true`) that must be set to enable [tests]; otherwise it reports a single skipped test. */ - const val PROP_ENABLED = "archie.junit.gametest" - /** System property overriding [DEFAULT_MATRIX], a comma-separated list of `loader:side` pairs to launch. */ - const val PROP_MATRIX = "archie.junit.gametest.matrix" - /** System property overriding the per-invocation timeout, in minutes (default 20, minimum 1). */ - const val PROP_TIMEOUT_MINUTES = "archie.junit.gametest.timeoutMinutes" - /** System property overriding the auto-detected workspace root (the directory containing `settings.gradle.kts`). */ - const val PROP_WORKSPACE_ROOT = "archie.junit.gametest.root" - - /** The default `loader:side` matrix launched by [tests] when [PROP_MATRIX] isn't set. */ - const val DEFAULT_MATRIX = "fabric:server,fabric:client,neoforge:server,neoforge:client" - - /** - * Builds the JUnit dynamic test tree for [modID]: one container per `loader:side` invocation - * in the configured matrix, each running the loader's GameTest Gradle task once and then - * reporting one [DynamicTest] per test method declared via [tests] (an - * [AEvents.ArchieGameTestBuilder] receiver, same DSL as [AEvents.REGISTER_GAME_TEST]) whose - * side matches that invocation. - */ - fun tests(modID: String, tests: AEvents.ArchieGameTestBuilder.() -> Unit): Collection - { - val enabled = System.getProperty(PROP_ENABLED)?.toBooleanStrictOrNull() == true - if (!enabled) { - return listOf( - DynamicContainer.dynamicContainer("gametest:disabled", mutableListOf(DynamicTest.dynamicTest("GameTest runner is disabled") { - assumeTrue(false) { - "GameTest runner is disabled. Set -D$PROP_ENABLED=true to launch Loom gametest tasks from JUnit." - } - })) - ) - } - - val matrixValue = System.getProperty(PROP_MATRIX) ?: DEFAULT_MATRIX - val invocations = GameTestGradleInvocation.parseMatrix(matrixValue) - require(invocations.isNotEmpty()) { - "No GameTest invocations configured. Set -D$PROP_MATRIX with at least one loader:side pair." - } - - val timeout = Duration.ofMinutes((System.getProperty(PROP_TIMEOUT_MINUTES)?.toLongOrNull() ?: 20L).coerceAtLeast(1L)) - val root = resolveWorkspaceRoot() - - val handleLazies = invocations.associateWith { invocation -> - lazy(LazyThreadSafetyMode.SYNCHRONIZED) { GameTestGradleExecutor.start(invocation, timeout, root) } - } - - val containers = mutableListOf() - - invocations.forEach { invocation -> - val handleLazy = handleLazies.getValue(invocation) - containers.add(DynamicContainer.dynamicContainer(invocation.id, buildList { - val invocationTestName = "GameTest Invocation [${invocation.id}]" - val invocationTest = DynamicTest.dynamicTest(invocationTestName) { - val result = handleLazy.value.result.get() - - // Check if the invocation itself succeeded (exit code 0) - if (!result.success) { - val message = buildString { - append("GameTest invocation failed: $invocationTestName\n") - append("Exit code: ${result.exitCode}\n") - append("Command: ${result.command.joinToString(" ")}\n") - append("Log file: ${result.logFile}\n") - append("--- Log tail ---\n") - append(result.logTail) - } - throw AssertionError(message) - } - } - add(invocationTest) - AEvents.ArchieGameTestBuilder(true).apply(tests).classes.forEach { clazz -> - val classUri = URI.create("class:${clazz.name}") - add(DynamicContainer.dynamicContainer(clazz.simpleName, classUri, clazz.declaredMethods.flatMap { method -> - val hasGameTest = method.getAnnotationsByType(GameTest::class.java).isNotEmpty() - val hasClientGameTest = method.getAnnotationsByType(ClientGameTest::class.java).isNotEmpty() - val tests = mutableListOf() - if ((hasGameTest && invocation.side == Side.SERVER) || (hasClientGameTest && invocation.side == Side.CLIENT)) { - val id = "$modID:${clazz.simpleName.lowercase()}.${method.name.lowercase()}" - val displayName = "${method.name}" - val testName = "$displayName [${invocation.id}]" - val methodUri = URI.create("method:${clazz.name}#${method.name}") - val test = DynamicTest.dynamicTest(testName, methodUri) { - val testResult = handleLazy.value.awaitTestResult(id) - if (testResult != null && !testResult.passed) { - val result = handleLazy.value.result.get() - val message = buildString { - append("GameTest failed: $testName\n") - append("Exit code: ${result.exitCode}\n") - append("Command: ${result.command.joinToString(" ")}\n") - append("Log file: ${result.logFile}\n") - append("--- Log tail ---\n") - append(result.logTail) - } - throw AssertionError(message) - } else if (testResult == null) { - // Test wasn't found in the log output - val result = handleLazy.value.result.get() - if (!result.success) { - // Invocation failed entirely, report that - val message = buildString { - append("GameTest invocation failed: $testName\n") - append("Exit code: ${result.exitCode}\n") - append("Command: ${result.command.joinToString(" ")}\n") - append("Log file: ${result.logFile}\n") - append("--- Log tail ---\n") - append(result.logTail) - } - throw AssertionError(message) - } - // Otherwise treat as passed if test ID wasn't found (test might not have run) - } - } - tests.add(test) - } - tests - }.stream())) - } - })) - } - - return containers - } - - private fun resolveWorkspaceRoot(): Path { - val explicit = System.getProperty(PROP_WORKSPACE_ROOT)?.trim().orEmpty() - if (explicit.isNotEmpty()) return Path.of(explicit) - - var cursor = Path.of("").toAbsolutePath() - repeat(8) { - if (cursor.resolve("settings.gradle.kts").toFile().exists()) return cursor - cursor = cursor.parent ?: return@repeat - } - error("Unable to locate workspace root from ${Path.of("").toAbsolutePath()}. Set -D$PROP_WORKSPACE_ROOT=/path/to/Archie") - } - - -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/AUIScopeManager.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/AUIScopeManager.kt deleted file mode 100644 index 86122601f..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/AUIScopeManager.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.kernelpanicsoft.archie.gui - -import kotlinx.coroutines.CoroutineScope - -/** - * Global registry of active [CoroutineScope]s created by open GUI screens. - * - * Each [ComposeScreen] or [ComposeContainerScreen] registers its `composeScope` here on - * startup so that external systems (e.g. the event bus) can broadcast work to all live - * GUI coroutines without holding direct references to individual screens. - * - * Scopes are removed automatically when their screen closes. - */ -object AUIScopeManager { - /** The set of all currently active GUI coroutine scopes. */ - val scopes = mutableSetOf() -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeBlockContainerMenu.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeBlockContainerMenu.kt deleted file mode 100644 index 002d3e7cb..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeBlockContainerMenu.kt +++ /dev/null @@ -1,74 +0,0 @@ -package net.kernelpanicsoft.archie.gui - -import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager -import net.kernelpanicsoft.archie.gui.blockentity.ComposeBlockEntityState -import net.kernelpanicsoft.archie.gui.blockentity.getOrCreateBlockEntityState -import net.minecraft.server.level.ServerPlayer -import net.minecraft.world.entity.player.Inventory -import net.minecraft.world.entity.player.Player -import net.minecraft.world.inventory.MenuType -import net.minecraft.world.level.block.entity.BlockEntity - -/** - * Base class for [BlockEntity]-backed Compose container menus. - * - * See [ComposeContainerMenuBase] for slot pre-registration/positioning behavior, shared with - * [net.kernelpanicsoft.archie.gui.item.ComposeItemContainerMenu] - this class only adds the - * [BlockEntity]-specific pieces: holding [tile], deriving [blockEntityState] from its position, - * and registering it with [BlockEntityStateManager]. - * - * ### Subclassing - * ```kotlin - * class MyMenu(id: Int, inventory: Inventory, tile: MyTile) : - * ComposeBlockContainerMenu(MY_MENU_TYPE, id, inventory, tile) { - * - * override fun registerSlotHandlers() { - * handler("inventory", tile.items) // ties the "inventory" slot group to the storage - * } - * } - * ``` - * - * @param T The [BlockEntity] type that owns the storage. - * @param SELF The concrete menu subclass (self-referential for the [MenuType]). - * @param type The registered [MenuType] for this menu. - * @param id The container id assigned by the server. - * @param playerInventory The opening player's inventory. - * @param tile The block entity instance. - */ -abstract class ComposeBlockContainerMenu>( - type: MenuType, - id: Int, - playerInventory: Inventory, - protected val tile: T, -) : ComposeContainerMenuBase(type, id, playerInventory) { - - val blockEntityState: ComposeBlockEntityState = getOrCreateBlockEntityState(tile.blockPos) - - init - { - // Must run here, in this class's own init - not from ComposeContainerMenuBase's, which - // would dispatch into onMenuOpened() before `tile` (this class's own constructor - // property) is actually assigned. See onMenuOpened's KDoc. - onMenuOpened() - } - - override fun onMenuOpened() - { - BlockEntityStateManager.registerBlockEntity(tile) - if (!level.isClientSide) - { - BlockEntityStateManager.addTrackedPlayer(tile, player as ServerPlayer) - } - } - - override fun onMenuClosed(player: Player) - { - BlockEntityStateManager.unregisterBlockEntity(tile) - if (!level.isClientSide) - { - BlockEntityStateManager.removeTrackedPlayer(tile, player as ServerPlayer) - } - } - - override fun stillValid(player: Player): Boolean = true -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerMenuBase.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerMenuBase.kt deleted file mode 100644 index 9bc0e1068..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerMenuBase.kt +++ /dev/null @@ -1,483 +0,0 @@ -package net.kernelpanicsoft.archie.gui - -import earth.terrarium.common_storage_lib.item.impl.vanilla.AbstractVanillaContainer -import earth.terrarium.common_storage_lib.resources.item.ItemResource -import earth.terrarium.common_storage_lib.storage.base.CommonStorage -import net.kernelpanicsoft.archie.gui.layout.IntRect -import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel -import net.kernelpanicsoft.archie.transfer.ArchieItemMenuSlot -import net.kernelpanicsoft.archie.transfer.ArchieItemStorage -import net.kernelpanicsoft.archie.transfer.VanillaMenuSlot -import net.minecraft.world.Container -import net.minecraft.world.entity.player.Inventory -import net.minecraft.world.entity.player.Player -import net.minecraft.world.inventory.AbstractContainerMenu -import net.minecraft.world.inventory.ClickType -import net.minecraft.world.inventory.MenuType -import net.minecraft.world.inventory.Slot -import net.minecraft.world.item.ItemStack -import net.minecraft.world.level.Level -import java.util.function.Predicate - -/** - * Holder-agnostic base for Compose-backed container menus: everything about slot layout, - * registration, and vanilla-menu plumbing that doesn't care whether the menu is backed by a - * [net.minecraft.world.level.block.entity.BlockEntity] ([ComposeBlockContainerMenu]) or an - * [net.minecraft.world.item.ItemStack] ([net.kernelpanicsoft.archie.gui.item.ComposeItemContainerMenu]). - * - * Slots are pre-registered at construction time with placeholder pixel positions so that - * [AbstractContainerMenu.initializeContents] (triggered by the server's slot-sync packet) - * always finds the correct number of slots. When the Compose layout runs and reports actual - * on-screen positions via [updateSlotData], existing slot objects have their pixel coordinates - * updated in-place rather than the slot list being rebuilt from scratch. - * - * @param SELF The concrete menu subclass (self-referential for the [MenuType]). - * @param type The registered [MenuType] for this menu. - * @param id The container id assigned by the server. - * @param playerInventory The opening player's inventory. - */ -abstract class ComposeContainerMenuBase>( - type: MenuType, - id: Int, - protected val playerInventory: Inventory, -) : AbstractContainerMenu(type, id) { - - /** - * The most recently reported [SlotData] from the Compose layout. - * On the server this is the authoritative source of slot group sizes. - * On the client it is received from the server via a [net.kernelpanicsoft.archie.networking.NetworkChannel]. - */ - var slotData: SlotData = SlotData() - private set - - /** `true` once [repositionSlots] has run at least once and slot pixel positions are valid. */ - var ready: Boolean = false - private set - - /** Parallel to [slots], stores which compose layer produced each vanilla slot. */ - private var slotLayerDepthByIndex: IntArray = IntArray(0) - private var slotClipBoundsByIndex: Array = emptyArray() - - /** - * The enabled group ids [rebuildSlots] last ran with, in [SlotData.groups] iteration order. - * Used by [applySlotData] to tell a genuine shape change (e.g. switching tabs to a group - * backed by different storage) from a mere reposition (scrolling, resizing) that must not - * discard existing [Slot] identity. - */ - private var registeredGroupIds: List = emptyList() - - /** - * The screen's `leftPos` offset — set by [ComposeContainerScreen] so that absolute - * Compose coordinates can be converted to slot-relative coordinates that vanilla's - * item rendering expects (vanilla renders items at `leftPos + slot.x`). - */ - var screenLeftPos: Int = 0 - - /** - * The screen's `topPos` offset — set by [ComposeContainerScreen] so that absolute - * Compose coordinates can be converted to slot-relative coordinates that vanilla's - * item rendering expects (vanilla renders items at `topPos + slot.y`). - */ - var screenTopPos: Int = 0 - - /** Maps slot-group id → the storage that backs it. */ - private val slotHandlers: MutableMap> = mutableMapOf() - private val slotFilters: MutableMap> = mutableMapOf() - - protected val player: Player = playerInventory.player - protected val level: Level = player.level() - - /** - * Register whatever state-tracking this menu's holder needs here. **Not called - * automatically** - each concrete subclass must call this from its own `init {}` block, - * after its own constructor-parameter properties (e.g. `tile`) are assigned. Calling it from - * *this* class's own `init {}` instead would dispatch into the subclass's override before - * those properties exist yet (Kotlin/JVM run a subclass's own property initializers only - * after its superclass's constructor - including this class's `init {}` - has fully - * returned), silently observing them as null despite their non-null declared type. - */ - protected abstract fun onMenuOpened() - - /** Called from [removed] - unregister whatever [onMenuOpened] registered here. */ - protected abstract fun onMenuClosed(player: Player) - - /** - * Excludes the player-inventory slot at container-relative [index] (0-35, matching - * [Inventory]'s own numbering: hotbar 0-8, main 9-35) from placement/pickup - frozen in - * place rather than removed from the slot list, to avoid reworking [addPlayerSlots]'s - * hardcoded 36-slot/3x9+9 assumptions elsewhere. Used by - * [net.kernelpanicsoft.archie.gui.item.ComposeItemContainerMenu] to freeze the backpack's - * own slot in the player's inventory while its GUI is open (also special-cased in - * [quickMoveStack], so shift-clicking it doesn't duplicate its contents into itself). - */ - protected open fun isPlayerSlotExcluded(index: Int): Boolean = false - - // ── Slot registration ────────────────────────────────────────────────── - - /** - * Implement this to call [handler] for each slot group your menu exposes. - * - * This is called: - * 1. Once at construction time so the slots list is pre-populated with the correct count. - * 2. Again every time [updateSlotData] fires with updated positions. - */ - protected abstract fun registerSlotHandlers() - - /** - * Registers a generic [CommonStorage] handler for the named slot group. - */ - protected fun handler(group: String, storage: CommonStorage, filter: Predicate = Predicate { true }) { - slotHandlers[group] = storage - slotFilters[group] = filter - } - - // ── Called by the Compose layout ─────────────────────────────────────── - - /** - * Called by the [Slot] composable once its absolute screen position is known. - * - * On the **first** call (before any slots exist) the full slot list is built. - * On **subsequent** calls (re-layout / window resize) existing slot objects have their - * pixel coordinates updated in-place so [initializeContents] is never broken. - * - * @param data The updated [SlotData] from the Compose layout. - */ - fun updateSlotData(data: SlotData) { - slotData = data - applySlotData() - broadcastFullState() - // Notify server of the new layout so it can validate slot indices - ArchieNetworkChannel.toServer(data) - } - - fun slotLayerDepth(slotIndex: Int): Int = slotLayerDepthByIndex.getOrElse(slotIndex) { 0 } - fun slotClipBounds(slotIndex: Int): IntRect? = slotClipBoundsByIndex.getOrNull(slotIndex) - - /** - * Whether the slot at [slotIndex] currently overlaps its group's clip bounds (if it has - * any, i.e. it sits inside a [net.kernelpanicsoft.archie.gui.composables.containers.Scrollable]). - * - * Used as this slot's [Slot.isActive] - the same vanilla hook that hides the Donkey/Mule - * armor slot - so a slot scrolled out of view stops rendering its item icon and stops being - * hoverable/clickable, without needing to remove it from [slots] and break its identity. - * A slot with no clip bounds (not inside a scrollable) is always visible. - */ - fun isSlotVisible(slotIndex: Int): Boolean { - val clip = slotClipBoundsByIndex.getOrNull(slotIndex) ?: return true - val slot = slots.getOrNull(slotIndex) ?: return true - val absX = screenLeftPos + slot.x - val absY = screenTopPos + slot.y - return absX + 16 > clip.minX && absX < clip.maxX && absY + 16 > clip.minY && absY < clip.maxY - } - - /** - * Re-derives every [Slot]'s pixel position from the current [slotData] without touching - * slot identity or [slotHandlers]. Call after [screenLeftPos]/[screenTopPos] change - - * slot coordinates have the screen offset baked into their subtraction (see - * [repositionSlots]), so they go stale whenever the screen recenters, independent of - * whether Compose's own slot layout changed. - */ - fun refreshSlotPositions() { - if (slots.isEmpty()) return - repositionSlots() - rebuildSlotMetadataMaps() - } - - /** - * Applies the current [slotData]: rebuilds the vanilla [Slot] list from scratch only when - * the set of enabled groups actually changed (or on first layout); otherwise repositions - * the existing slots in place. - * - * Every layout pass - including a single frame of scrolling - re-reports the full - * [SlotData], so rebuilding unconditionally here would discard and recreate every [Slot] - * object on every scroll tick / resize, breaking anything holding a reference to one - * (drag-in-progress, vanilla's own hovered-slot tracking, quick-move). - */ - private fun applySlotData() { - val enabledGroupIds = slotData.groups.entries.filter { it.value.enabled }.map { it.key } - if (slots.isEmpty() || enabledGroupIds != registeredGroupIds) { - rebuildSlots() - registeredGroupIds = enabledGroupIds - } else { - repositionSlots() - } - rebuildSlotMetadataMaps() - } - - /** - * Full slot-list (re)construction: registers all menu slots and player slots. - * Only called by [applySlotData] when the enabled group set actually changed. - */ - private fun rebuildSlots() { - registerSlotHandlers() - this.slots.clear() - this.lastSlots.clear() - this.remoteSlots.clear() - addMenuSlots() - addPlayerSlots() - repositionSlots() - } - - /** - * Subsequent calls: update pixel coordinates of already-registered slots in-place. - * Converts absolute Compose screen coordinates to slot-relative coordinates by - * subtracting [screenLeftPos]/[screenTopPos], because vanilla renders items at - * `leftPos + slot.x` and `topPos + slot.y`. - * Slot *count* must not change between layouts. - */ - private fun repositionSlots() { - var slotIndex = 0 - - // Reposition menu slots - slotData.groups.forEach { (id, group) -> - if (group.enabled) - { - slotHandlers[id]?.let { _ -> - for (row in 0 until group.size.height) - { - for (col in 0 until group.size.width) - { - if (slotIndex < slots.size) - { - val mcSlot = slots[slotIndex] - mcSlot.x = group.pos.x + 1 + col * 18 - screenLeftPos - mcSlot.y = group.pos.y + 1 + row * 18 - screenTopPos - slotIndex++ - } - } - } - } - } - } - - // Reposition player slots (main inventory 3×9, then hotbar 1×9) - slotData.playerGroup.let { pg -> - for (row in 0 until 3) { - for (col in 0 until 9) { - if (slotIndex < slots.size) { - slots[slotIndex].x = pg.pos.x + 1 + col * 18 - screenLeftPos - slots[slotIndex].y = pg.pos.y + 1 + row * 18 - screenTopPos - slotIndex++ - } - } - } - for (col in 0 until 9) { - if (slotIndex < slots.size) { - slots[slotIndex].x = pg.pos.x + 1 + col * 18 - screenLeftPos - slots[slotIndex].y = pg.pos.y + 1 + 58 - screenTopPos - slotIndex++ - } - } - } - ready = true - } - - private fun rebuildSlotMetadataMaps() { - val depths = ArrayList(slots.size) - val clips = ArrayList(slots.size) - - slotData.groups.forEach { (id, group) -> - if (!group.enabled) return@forEach - if (slotHandlers[id] == null) return@forEach - val clip = group.clip - repeat(group.size.width * group.size.height) { - depths += group.layerDepth - clips += clip - } - } - - val playerClip = slotData.playerGroup.clip - repeat(36) { - depths += slotData.playerGroup.layerDepth - clips += playerClip - } - - while (depths.size < slots.size) { - depths += 0 - clips += null - } - slotLayerDepthByIndex = depths.toIntArray() - slotClipBoundsByIndex = clips.toTypedArray() - } - - // ── Internal slot helpers ────────────────────────────────────────────── - - private fun addMenuSlots() { - slotData.groups.forEach { (id, group) -> - if (!group.enabled) return@forEach - slotHandlers[id]?.let { handler -> - // Use slot-relative coords (subtract screen offset so vanilla adds it back correctly) - slotGrid(group.pos.x - screenLeftPos, group.pos.y - screenTopPos, group.size.width, group.size.height, handler, slotFilters[id] ?: Predicate { true }) - } - } - } - - private fun addPlayerSlots() { - slotData.playerGroup.let { pg -> - val relX = pg.pos.x - screenLeftPos - val relY = pg.pos.y - screenTopPos - // 3 rows of 9 (main inventory: playerInventory indices 9–35) - for (row in 0 until 3) { - for (col in 0 until 9) { - playerSlot(col + row * 9 + 9, relX + col * 18, relY + row * 18) - } - } - // Hotbar (playerInventory indices 0–8), 58px below main inventory - for (col in 0 until 9) { - playerSlot(col, relX + col * 18, relY + 58) - } - } - } - - /** - * Adds one player-inventory slot at container-relative [containerIndex], frozen against - * placement/pickup if [isPlayerSlotExcluded] says so - see its KDoc. - */ - private fun playerSlot(containerIndex: Int, x: Int, y: Int) { - val excluded = isPlayerSlotExcluded(containerIndex) - addSlot(object : Slot(playerInventory, containerIndex, x, y) - { - override fun mayPlace(itemStack: ItemStack): Boolean = !excluded - override fun mayPickup(player: Player): Boolean = !excluded - override fun isActive(): Boolean = isSlotVisible(index) - }) - } - - // ── Slot grid helper ─────────────────────────────────────────────────── - - data class SlotGridLocation(val slot: Int, val x: Int, val y: Int) - - protected fun slotGrid(x: Int, y: Int, width: Int, height: Int, block: SlotGridLocation.() -> Unit) { - for (row in 0 until height) { - for (col in 0 until width) { - SlotGridLocation(col + row * width, x + col * 18, y + row * 18).block() - } - } - } - - protected fun slotGrid(x: Int, y: Int, width: Int, height: Int, container: Container, filter: Predicate = Predicate { true }) { - slotGrid(x, y, width, height) { slot(container, filter, slot, this.x, this.y) } - } - - protected fun slotGrid(x: Int, y: Int, width: Int, height: Int, storage: CommonStorage, filter: Predicate = Predicate { true }) { - slotGrid(x, y, width, height) { slot(storage, filter, slot, this.x, this.y) } - } - - protected fun slot(mcSlot: Slot) { addSlot(mcSlot) } - - protected fun slot(storage: CommonStorage, filter: Predicate = Predicate { true }, slot: Int, x: Int, y: Int) { - if (slot !in 0 until storage.size()) return - when (storage) - { - is ArchieItemStorage -> slot(storage, filter, slot, x, y) - is AbstractVanillaContainer -> slot(storage, filter, slot, x, y) - } - } - - protected fun slot(container: Container, filter: Predicate = Predicate { true }, slot: Int, x: Int, y: Int) { - addSlot(object : Slot(container, slot, x, y) - { - override fun mayPlace(itemStack: ItemStack): Boolean = filter.test(itemStack) - override fun isActive(): Boolean = isSlotVisible(index) - }) - } - - protected fun slot(storage: ArchieItemStorage, filter: Predicate = Predicate { true }, slot: Int, x: Int, y: Int) { - addSlot(ArchieItemMenuSlot(storage, filter, slot, x, y, this)) - } - - protected fun slot(storage: AbstractVanillaContainer, filter: Predicate = Predicate { true }, slot: Int, x: Int, y: Int) { - addSlot(VanillaMenuSlot(storage, filter, slot, x, y, this)) - } - - // ── AbstractContainerMenu overrides ──────────────────────────────────── - - /** - * Shift-click handling: menu slots move into the player inventory/hotbar, and player - * slots move into the menu, falling back between main inventory and hotbar when the menu - * has no room. Slot ranges are derived from [slots].size rather than hardcoded, since the - * number of menu slots varies with which groups are enabled. - * - * The player-inventory slot excluded via [isPlayerSlotExcluded] (if any) is special-cased - * to return [ItemStack.EMPTY] immediately - shift-clicking a backpack's own slot while its - * GUI is open must not be able to move the backpack into itself. - */ - override fun quickMoveStack(player: Player, index: Int): ItemStack { - val slot = slots.getOrNull(index) ?: return ItemStack.EMPTY - if (slot.container === playerInventory && isPlayerSlotExcluded(slot.containerSlot)) return ItemStack.EMPTY - if (!slot.hasItem()) return ItemStack.EMPTY - - val stackInSlot = slot.item - val copied = stackInSlot.copy() - - val totalSlots = slots.size - val playerSlotCount = 36 - val playerStart = (totalSlots - playerSlotCount).coerceAtLeast(0) - val playerEndExclusive = totalSlots - val menuStart = 0 - val menuEndExclusive = playerStart - val hotbarSize = 9 - val hotbarStart = (playerEndExclusive - hotbarSize).coerceAtLeast(playerStart) - val inventoryStart = playerStart - val inventoryEndExclusive = hotbarStart - - val moved = when { - // From menu -> player inventory/hotbar - index in menuStart until menuEndExclusive -> - moveItemStackTo(stackInSlot, playerStart, playerEndExclusive, true) - - // From player main inventory -> menu first, then hotbar fallback - index in inventoryStart until inventoryEndExclusive -> { - val movedToMenu = menuEndExclusive > menuStart && moveItemStackTo(stackInSlot, menuStart, menuEndExclusive, false) - movedToMenu || moveItemStackTo(stackInSlot, hotbarStart, playerEndExclusive, false) - } - - // From hotbar -> menu first, then main inventory fallback - index in hotbarStart until playerEndExclusive -> { - val movedToMenu = menuEndExclusive > menuStart && moveItemStackTo(stackInSlot, menuStart, menuEndExclusive, false) - movedToMenu || moveItemStackTo(stackInSlot, inventoryStart, inventoryEndExclusive, false) - } - - else -> false - } - - if (!moved) return ItemStack.EMPTY - - if (stackInSlot.isEmpty) slot.set(ItemStack.EMPTY) else slot.setChanged() - slot.onTake(player, stackInSlot) - return copied - } - - override fun clicked(slotId: Int, button: Int, clickType: ClickType, player: Player) { - super.clicked(slotId, button, clickType, player) - broadcastChanges() - } - - override fun removed(player: Player) - { - super.removed(player) - onMenuClosed(player) - } - - // ── Networking ───────────────────────────────────────────────────────── - - companion object { - - /** - * Registers the serverbound [SlotData] packet handler that keeps the server's slot - * positions/clip bounds in sync with whichever [ComposeContainerMenuBase] the sending - * player has open. Call once during network channel setup. - */ - fun register() { - ArchieNetworkChannel.serverbound(SlotData::class) { data, context -> - val menu = context.player.containerMenu - if (menu is ComposeContainerMenuBase<*>) { - menu.slotData = data - // Match slot positions on the server to the client layout - menu.applySlotData() - menu.broadcastFullState() - } - } - } - } - } diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerScreen.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerScreen.kt deleted file mode 100644 index 2212be8ab..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerScreen.kt +++ /dev/null @@ -1,482 +0,0 @@ -package net.kernelpanicsoft.archie.gui - -import androidx.compose.runtime.BroadcastFrameClock -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.ProvidableCompositionLocal -import androidx.compose.runtime.Recomposer -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.snapshots.Snapshot -import com.mojang.blaze3d.platform.InputConstants -import kotlinx.coroutines.* -import net.kernelpanicsoft.archie.gui.access.SlotHighlightClipProvider -import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthProvider -import net.kernelpanicsoft.archie.gui.blockentity.LocalBlockEntityState -import net.kernelpanicsoft.archie.gui.composables.containers.RootContainer -import net.kernelpanicsoft.archie.gui.item.ComposeItemContainerMenu -import net.kernelpanicsoft.archie.gui.item.LocalItemState -import net.kernelpanicsoft.archie.gui.layer.LayerStackManager -import net.kernelpanicsoft.archie.gui.layer.LocalLayerManager -import net.kernelpanicsoft.archie.gui.layout.IntCoordinates -import net.kernelpanicsoft.archie.gui.layout.IntRect -import net.kernelpanicsoft.archie.gui.layout.LayoutNode -import net.kernelpanicsoft.archie.gui.modifiers.Constraints -import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType -import net.kernelpanicsoft.archie.gui.util.extension.processCharEvent -import net.kernelpanicsoft.archie.gui.util.extension.processDragEvent -import net.kernelpanicsoft.archie.gui.util.extension.processKeyEvent -import net.kernelpanicsoft.archie.gui.util.extension.processPointerEvent -import net.kernelpanicsoft.archie.gui.util.extension.processScrollEvent -import net.minecraft.client.gui.GuiGraphics -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen -import net.minecraft.network.chat.Component -import net.minecraft.world.entity.player.Inventory -import net.minecraft.world.inventory.Slot -import org.lwjgl.glfw.GLFW -import kotlin.coroutines.CoroutineContext - -/** Provides the current [ComposeContainerScreen] to any composable in its tree. */ -val LocalContainerScreen: ProvidableCompositionLocal> = - compositionLocalOf { throw IllegalStateException("Screen has not been provided") } - -/** Provides the current [ComposeContainerMenuBase] to any composable in its tree. */ -val LocalContainerMenu: ProvidableCompositionLocal> = - compositionLocalOf { throw IllegalStateException("Screen has not been provided") } - -/** - * A Compose-driven [AbstractContainerScreen] with layer support, async recomposition, and - * vanilla [Slot] rendering kept in sync with the Compose-reported [SlotGroup] layout. - * - * Behaves like [ComposeScreen] but additionally bridges vanilla's container/slot machinery: - * the base layer (layer 0) is rendered from [renderBg] so it draws under vanilla's slots, and - * any additional layers (modals) render on top from [render] via [renderSlot]/[slotClipRect] - * clipping so scrolled-out-of-view slots don't paint over unrelated content. - * - * Extend this class and call [start] inside your `init()` override, the same way as - * [ComposeScreen]. Works uniformly for both [ComposeBlockContainerMenu] (BlockEntity-backed) and - * [ComposeItemContainerMenu] (ItemStack-backed) subclasses - nothing here is holder-specific. - * - * @param T The concrete [ComposeContainerMenuBase] subclass driving this screen. - * @param menu The container menu instance for this screen. - * @param playerInventory The opening player's inventory. - * @param title The screen title passed to the vanilla [AbstractContainerScreen] constructor. - * @param asynchronous When `true` (default), recomposition runs off the main thread and - * the result is joined at the start of the next frame for smooth, non-blocking updates. - * Set to `false` to force synchronous recomposition (simpler but may stutter). - */ -abstract class ComposeContainerScreen>( - menu: T, playerInventory: Inventory, title: Component, - val asynchronous: Boolean = true, -) : AbstractContainerScreen(menu, playerInventory, title), - CoroutineScope, - SlotLayerDepthProvider, - SlotHighlightClipProvider, - ComposeIdleAware, - LayerManagerProvider -{ - companion object { - private const val BASE_LAYER_Z = 100f - private const val LAYER_Z_STEP = 200f - private const val SLOT_LAYER_OFFSET = 120f - - /** The base Z offset used when rendering the layer at [layerDepth], deepest layers on top. */ - fun layerBaseZ(layerDepth: Int): Float = BASE_LAYER_Z + layerDepth * LAYER_Z_STEP - } - - - private var hasFrameWaiters = false - private val clock = BroadcastFrameClock { hasFrameWaiters = true } - - // See ComposeScreen's identical fields for why this is captured once and untyped against - // kotlinx-coroutines-test. - private val testPump = ComposeTestClockOverride.pump - - private val composeScope = CoroutineScope(ComposeTestClockOverride.dispatcher ?: Dispatchers.Default) + clock - final override val coroutineContext: CoroutineContext = composeScope.coroutineContext - - final override lateinit var layerManager: LayerStackManager - private set - private lateinit var recomposer: Recomposer - private var recomposeJob: Job? = null - - private var applyScheduled = false - private val snapshotHandle = Snapshot.registerGlobalWriteObserver { - if (!applyScheduled) { - applyScheduled = true - composeScope.launch { - applyScheduled = false - Snapshot.sendApplyNotifications() - } - } - } - - private var lastMouseX = 0.0 - private var lastMouseY = 0.0 - - override fun isComposeIdle(): Boolean = - !applyScheduled && !hasFrameWaiters && recomposeJob?.isActive != true - - /** [titleLabelX]/[titleLabelY] expressed as an absolute-screen [IntCoordinates] pair. */ - var titleLabelPos: IntCoordinates - get() = IntCoordinates(titleLabelX, titleLabelY) - set(value) { - titleLabelX = value.x - leftPos - titleLabelY = value.y - topPos - } - - /** [inventoryLabelX]/[inventoryLabelY] expressed as an absolute-screen [IntCoordinates] pair. */ - var inventoryLabelPos: IntCoordinates - get() = IntCoordinates(inventoryLabelX, inventoryLabelY) - set(value) { - inventoryLabelX = value.x - leftPos - inventoryLabelY = value.y - topPos - } - - /** - * Initialises the Compose runtime and pushes the base layer with [content]. - * - * Must be called once from [init]. Subsequent calls replace the content. - * - * @param content The root composable content for this screen. - */ - protected fun start(content: @Composable () -> Unit) { - recomposer = Recomposer(coroutineContext) - layerManager = LayerStackManager(recomposer) - - AUIScopeManager.scopes += composeScope - launch { recomposer.runRecomposeAndApplyChanges() } - - layerManager.push { _ -> - CompositionLocalProvider( - LocalContainerScreen provides this, - LocalContainerMenu provides menu, - LocalSlotData provides menu.slotData, - // Only one of these is non-null for any given menu - LocalBlockEntityState / - // LocalItemState are both nullable-by-default composition locals precisely so - // composables reaching for the "wrong" one for this menu's holder kind get a - // clear null rather than a bogus fallback value. - LocalBlockEntityState provides (menu as? ComposeBlockContainerMenu<*, *>)?.blockEntityState, - LocalItemState provides (menu as? ComposeItemContainerMenu<*>)?.itemState, - LocalLayerManager provides layerManager, - ) { - RootContainer { - content() - } - } - } - } - - // ── Rendering ───────────────────────────────────────────────────────── - - /** - * Measures and renders all active layers. - * - * In async mode the previous recompose job is joined before rendering, then a new - * job is launched if there are pending frame waiters. - */ - open fun renderNodes(baseLayer: Boolean, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { - // See ComposeScreen.renderNodes for why this runs first. - testPump?.invoke() - if (asynchronous) { - recomposeJob?.let { job -> - runBlocking { - job.join() - } - recomposeJob = null - } - } else if (hasFrameWaiters) { - hasFrameWaiters = false - clock.sendFrame(System.nanoTime()) - } - - // layerManager.layers is a SnapshotStateList that can be structurally mutated (modal - // push/dismiss) from the recomposer coroutine on another thread while this method runs on - // the render thread. Reading it inside a snapshot gives a frozen, consistent view for the - // whole size-check-then-index sequence below, instead of racing the live list. A mutable - // (not read-only) snapshot is required because measure() can itself write state (e.g. - // ScrollableState.setChildSize), and those writes must be applied back afterward. - val layersSnapshot = Snapshot.takeMutableSnapshot() - try { - layersSnapshot.enter { - val layerIndices = if (baseLayer) { - if (layerManager.layers.isEmpty()) return@enter - 0..0 - } else { - if (layerManager.layers.size <= 1) return@enter - 1 until layerManager.layers.size - } - - for (layerIndex in layerIndices) { - val layer = layerManager.layers[layerIndex] - val rootNode = layer.rootNode - rootNode.measure(Constraints(maxWidth = width, maxHeight = height)) - rootNode.render(0, 0, guiGraphics, mouseX, mouseY, partialTick, layerBaseZ(layerIndex)) - } - - layerManager.screenSize.let { (width, height) -> - imageWidth = width - imageHeight = height - } - layerManager.screenPos.let { (x, y) -> - if (x == 0 && y == 0) - return@let - leftPos = x - topPos = y - menu.screenLeftPos = leftPos - menu.screenTopPos = topPos - } - menu.refreshSlotPositions() - - if (asynchronous && hasFrameWaiters) { - hasFrameWaiters = false - recomposeJob = composeScope.launch { - clock.sendFrame(System.nanoTime()) - } - } - } - layersSnapshot.apply().check() - } finally { - layersSnapshot.dispose() - } - } - - override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { - super.render(guiGraphics, mouseX, mouseY, partialTick) - renderTooltip(guiGraphics, mouseX, mouseY) - if (layerManager.layers.size > 1) - { - renderNodes(false, guiGraphics, mouseX, mouseY, partialTick) - } - } - - override fun isHovering( - x: Int, - y: Int, - width: Int, - height: Int, - mouseX: Double, - mouseY: Double - ): Boolean - { - if (layerManager.layers.size != 1) return false - - if (width == 16 && height == 16) { - val slotIndex = menu.slots.indexOfFirst { it.x == x && it.y == y } - val clip = if (slotIndex >= 0) menu.slotClipBounds(slotIndex) else null - if (clip != null) { - val absX = leftPos + x - val absY = topPos + y - val visible = IntRect(absX, absY, absX + width, absY + height).intersect(clip) ?: return false - return mouseX >= visible.minX - 1 && mouseX < visible.maxX + 1 && - mouseY >= visible.minY - 1 && mouseY < visible.maxY + 1 - } - } - - return super.isHovering(x, y, width, height, mouseX, mouseY) - } - - override fun renderBg(guiGraphics: GuiGraphics, partialTick: Float, mouseX: Int, mouseY: Int) { - renderNodes(true, guiGraphics, mouseX, mouseY, partialTick) - } - - /** - * Hook point for slot rendering customisation. - * - * By default, slots are clipped against the container bounds so partially visible - * slots still render correctly when Compose repositions them. - */ - override fun renderSlot(guiGraphics: GuiGraphics, slot: Slot) { - val clip = slotClipRect(slot) ?: return - - guiGraphics.enableScissor(clip.minX, clip.minY, clip.maxX, clip.maxY) - try { - super.renderSlot(guiGraphics, slot) - } finally { - guiGraphics.disableScissor() - } - } - - /** - * Called by [net.kernelpanicsoft.archie.mixin.client.gui.AbstractContainerScreenMixin] - * (via [net.kernelpanicsoft.archie.gui.access.SlotHighlightClipProvider]) to clip the - * hover-highlight overlay the same way [renderSlot] clips the item icon - vanilla only - * exposes a static `renderSlotHighlight(GuiGraphics, x, y, blitOffset)` with no per-slot - * override point, so this has to be reached from a mixin redirect instead of `override`. - */ - override fun slotHighlightClipRect(x: Int, y: Int): IntRect? { - val slot = menu.slots.firstOrNull { it.x == x && it.y == y } ?: return null - return slotClipRect(slot) - } - - override fun slotRenderLayerOffset(slot: Slot): Float? = slotRenderLayerZ(slot) - - /** - * Z layer used when rendering a specific vanilla [slot]. - * - * Slots render above the compose content of the layer that owns them, while - * still remaining below content from higher layers. - */ - protected open fun slotRenderLayerZ(slot: Slot): Float { - val slotIndex = menu.slots.indexOf(slot).takeIf { it >= 0 } ?: return layerBaseZ(0) + SLOT_LAYER_OFFSET - val layerDepth = menu.slotLayerDepth(slotIndex) - return layerBaseZ(layerDepth) + SLOT_LAYER_OFFSET - } - - protected open fun slotRenderLayerZ(): Float = layerBaseZ(0) + SLOT_LAYER_OFFSET - - /** - * Computes the clip rectangle for [slot] in absolute screen coordinates. - * - * Intersects the overall container bounds with the slot's own group clip (if it sits - * inside a [net.kernelpanicsoft.archie.gui.composables.containers.Scrollable] viewport), - * so a slot scrolled out of view is actually clipped instead of rendering on top of - * whatever else occupies that screen area. - * - * Returns `null` when the slot does not intersect the (possibly narrower) clip area. - */ - protected open fun slotClipRect(slot: Slot): IntRect? { - val containerClip = IntRect(leftPos, topPos, leftPos + imageWidth, topPos + imageHeight) - val slotIndex = menu.slots.indexOf(slot).takeIf { it >= 0 } - val groupClip = slotIndex?.let { menu.slotClipBounds(it) } - val effectiveClip = groupClip?.let { containerClip.intersect(it) } ?: containerClip - - val slotMinX = leftPos + slot.x - val slotMinY = topPos + slot.y - val slotRect = IntRect(slotMinX, slotMinY, slotMinX + 16, slotMinY + 16) - - return effectiveClip.intersect(slotRect) - } - - private var composeDisposed = false - - override fun onClose() { - GLFW.glfwSetCursor(minecraft!!.window.window, 0L) - super.onClose() - disposeCompose() - } - - // See ComposeScreen.removed()'s doc comment - vanilla's Minecraft.setScreen() calls removed() - // on the *old* screen for every transition, not just onClose()'s explicit-close path, and - // without this override this screen's entire Compose runtime leaked on any such swap. - override fun removed() { - super.removed() - disposeCompose() - } - - private fun disposeCompose() { - if (composeDisposed) return - composeDisposed = true - recomposeJob?.cancel("GUI closing") - recomposer.close() - snapshotHandle.dispose() - layerManager.layers.forEach { it.dispose() } - AUIScopeManager.scopes -= composeScope - composeScope.cancel() - } - - private fun getTopNode(): LayoutNode? = layerManager.top?.rootNode - - override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { - val topNode = getTopNode() ?: return super.mouseClicked(mouseX, mouseY, button) - processPointerEvent(topNode, mouseX, mouseY, PointerEventType.GLOBAL_PRESS, true) - val event = processPointerEvent(topNode, mouseX, mouseY, PointerEventType.PRESS) - return event.bypassSuper || super.mouseClicked(mouseX, mouseY, button) - } - - override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { - val topNode = getTopNode() ?: return super.mouseReleased(mouseX, mouseY, button) - processPointerEvent(topNode, mouseX, mouseY, PointerEventType.GLOBAL_RELEASE, true) - val event = processPointerEvent(topNode, mouseX, mouseY, PointerEventType.RELEASE) - return event.bypassSuper || super.mouseReleased(mouseX, mouseY, button) - } - - override fun mouseMoved(mouseX: Double, mouseY: Double) { - val topNode = getTopNode() ?: return super.mouseMoved(mouseX, mouseY) - processPointerEvent(topNode, mouseX, mouseY, PointerEventType.MOVE) - - processPointerEvent( - topNode, - mouseX, - mouseY, - PointerEventType.ENTER - ) { - it.isBounded(mouseX.toInt(), mouseY.toInt()) && !it.isBounded( - lastMouseX.toInt(), - lastMouseY.toInt() - ) - } - - processPointerEvent( - topNode, - mouseX, - mouseY, - PointerEventType.EXIT - ) { - !it.isBounded(mouseX.toInt(), mouseY.toInt()) && it.isBounded( - lastMouseX.toInt(), - lastMouseY.toInt() - ) - } - - lastMouseX = mouseX - lastMouseY = mouseY - super.mouseMoved(mouseX, mouseY) - } - - override fun mouseScrolled( - mouseX: Double, - mouseY: Double, - scrollX: Double, - scrollY: Double - ): Boolean { - val topNode = getTopNode() ?: return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY) - processScrollEvent(topNode, mouseX, mouseY, scrollX, scrollY, PointerEventType.GLOBAL_SCROLL, true) - val event = - processScrollEvent(topNode, mouseX, mouseY, scrollX, scrollY, PointerEventType.SCROLL) - return event.bypassSuper || super.mouseScrolled(mouseX, mouseY, scrollX, scrollY) - } - - override fun mouseDragged( - mouseX: Double, - mouseY: Double, - button: Int, - dragX: Double, - dragY: Double - ): Boolean { - val topNode = - getTopNode() ?: return super.mouseDragged(mouseX, mouseY, button, dragX, dragY) - val event = - processDragEvent(topNode, mouseX, mouseY, button, dragX, dragY, PointerEventType.DRAG) - return event.bypassSuper || super.mouseDragged(mouseX, mouseY, button, dragX, dragY) - } - - override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - val topNode = getTopNode() ?: return super.keyPressed(keyCode, scanCode, modifiers) - // Ctrl+Shift+D toggles the debug overlay. A bitwise check (not `modifiers == 3`) is - // required here - GLFW also sets bits for Caps Lock/Num Lock in `modifiers` when those - // are active, so an exact-equality check against just the Ctrl+Shift bitmask silently - // never matches on those systems. - val ctrlShiftMask = GLFW.GLFW_MOD_CONTROL or GLFW.GLFW_MOD_SHIFT - if (keyCode == InputConstants.KEY_D && (modifiers and ctrlShiftMask) == ctrlShiftMask) { - topNode.debug = !topNode.debug - } - if (topNode.debug && keyCode == InputConstants.KEY_LSHIFT) topNode.extraDebug = true - - val event = processKeyEvent(topNode, keyCode, scanCode, modifiers) - return event.bypassSuper || super.keyPressed(keyCode, scanCode, modifiers) - } - - override fun charTyped(codePoint: Char, modifiers: Int): Boolean { - val topNode = getTopNode() ?: return super.charTyped(codePoint, modifiers) - val event = processCharEvent(topNode, codePoint, modifiers) - return event.bypassSuper || super.charTyped(codePoint, modifiers) - } - - override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - val baseNode = layerManager.layers.firstOrNull()?.rootNode - if (baseNode != null && baseNode.debug && keyCode == InputConstants.KEY_LSHIFT) { - baseNode.extraDebug = false - } - return super.keyReleased(keyCode, scanCode, modifiers) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt deleted file mode 100644 index 9a27044e3..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt +++ /dev/null @@ -1,335 +0,0 @@ -package net.kernelpanicsoft.archie.gui - -import androidx.compose.runtime.* -import androidx.compose.runtime.snapshots.Snapshot -import com.mojang.blaze3d.platform.InputConstants -import kotlinx.coroutines.* -import net.kernelpanicsoft.archie.gui.layer.LayerStackManager -import net.kernelpanicsoft.archie.gui.layer.LocalLayerManager -import net.kernelpanicsoft.archie.gui.layout.* -import net.kernelpanicsoft.archie.gui.modifiers.Constraints -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.fillMaxSize -import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType -import net.kernelpanicsoft.archie.gui.util.extension.processCharEvent -import net.kernelpanicsoft.archie.gui.util.extension.processDragEvent -import net.kernelpanicsoft.archie.gui.util.extension.processKeyEvent -import net.kernelpanicsoft.archie.gui.util.extension.processPointerEvent -import net.kernelpanicsoft.archie.gui.util.extension.processScrollEvent -import net.minecraft.client.gui.GuiGraphics -import net.minecraft.client.gui.screens.Screen -import net.minecraft.network.chat.Component -import org.lwjgl.glfw.GLFW -import kotlin.coroutines.CoroutineContext - -/** Provides the current [ComposeScreen] to any composable in its tree. */ -val LocalScreen: ProvidableCompositionLocal = - compositionLocalOf { throw IllegalStateException("Screen has not been provided") } - -/** - * Implemented by Compose-driven screens that recompose asynchronously, so test harnesses can - * poll for a settled frame (no pending or in-flight recomposition) before asserting on rendered - * output - e.g. before taking a screenshot right after simulating a click. - */ -internal interface ComposeIdleAware { - /** `true` when there is no snapshot-write notification, frame request, or recompose job pending. */ - fun isComposeIdle(): Boolean -} - -/** Implemented by hosts (screens) that own a [LayerStackManager] for their layer stack. */ -interface LayerManagerProvider -{ - /** The layer stack owned by this host. */ - val layerManager: LayerStackManager -} - -/** - * Test-only override, installed by the client GameTest harness (`AClientGameTestHarness.kt`), - * that swaps [ComposeScreen]'s real-time coroutine dispatcher for a virtual one so a composable's - * `delay(...)` (e.g. a dialog's close animation) resolves deterministically against a scheduler - * the harness drives itself, instead of racing real wall-clock time, real thread scheduling, and - * the harness's tick-based polling - the same class of flakiness Jetpack Compose's own test - * tooling (`ComposeTestRule`/`runComposeUiTest`) avoids by construction, backing composition with - * a virtual clock/dispatcher that `waitForIdle()` drives forward instead of polling real - * concurrency. `null` unless a client GameTest explicitly installs one; the running game never - * touches this. - * - * Deliberately untyped against `kotlinx-coroutines-test` (`CoroutineDispatcher` is a - * kotlinx-coroutines-core type; `pump` is a plain lambda) - that dependency is dev/test-only - * (`compileOnly` in `common`, `runtimeLibrary` - present for local runs, never bundled - in the - * loader modules; see `common/build.gradle.kts`). [ComposeScreen] is loaded by every screen in - * the mod, so its own class file must never reference a symbol that isn't resolvable in a real - * player's game; only [AClientGameTestHarness]'s method bodies (never invoked outside - * `AGameTestPlatform.isGameTest`) construct the actual `StandardTestDispatcher`/ - * `TestCoroutineScheduler` instances installed here. - */ -internal object ComposeTestClockOverride { - /** The dispatcher to back new [ComposeScreen]s' coroutine scope with, in place of [kotlinx.coroutines.Dispatchers.Default]. */ - @Volatile - var dispatcher: CoroutineDispatcher? = null - - /** - * Called once per real rendered frame by every live [ComposeScreen] while installed - drains - * all outstanding virtual-time work (recomposition, effects, `delay(...)`) synchronously on - * the render thread. Set alongside [dispatcher] to `{ scheduler.advanceUntilIdle() }`. - */ - @Volatile - var pump: (() -> Unit)? = null -} - -/** - * A Compose-driven Minecraft [Screen] base class with layer support, async recomposition, - * and full pointer/keyboard input dispatch. - * - * Extend this class and call [start] inside your `init()` override: - * - * ```kotlin - * class MyScreen : ComposeScreen(Component.literal("My Screen")) { - * override fun init() { - * super.init() - * start { - * Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - * Text(Component.literal("Hello!")) - * } - * } - * } - * } - * ``` - * - * @param title The screen title passed to the vanilla [Screen] constructor. - * @param asynchronous When `true` (default), recomposition runs off the main thread and - * the result is joined at the start of the next frame for smooth, non-blocking updates. - * Set to `false` to force synchronous recomposition (simpler but may stutter). - */ -abstract class ComposeScreen( - title: Component, - val asynchronous: Boolean = true, -) : Screen(title), CoroutineScope, ComposeIdleAware, LayerManagerProvider { - - private var hasFrameWaiters = false - private val clock = BroadcastFrameClock { hasFrameWaiters = true } - - // Captured once at construction (not read live from ComposeTestClockOverride elsewhere) so - // this screen keeps working consistently even if a later test installs/clears the override - // while this screen is still disposing. - private val testPump = ComposeTestClockOverride.pump - - private val composeScope = CoroutineScope(ComposeTestClockOverride.dispatcher ?: Dispatchers.Default) + clock - final override val coroutineContext: CoroutineContext = composeScope.coroutineContext - - final override lateinit var layerManager: LayerStackManager - private set - private lateinit var recomposer: Recomposer - private var recomposeJob: Job? = null - - private var applyScheduled = false - private val snapshotHandle = Snapshot.registerGlobalWriteObserver { - if (!applyScheduled) { - applyScheduled = true - composeScope.launch { - applyScheduled = false - Snapshot.sendApplyNotifications() - } - } - } - - // Not 0.0 - a node can legitimately sit at the literal origin (e.g. the first item in a - // top-left-aligned Column), and mouseMoved()'s ENTER condition (`nowBounded && !wasBounded`) - // would then read the initial "no prior position" sentinel as if the mouse had already been - // sitting inside that node before any real movement, silently suppressing its very first - // ENTER event. No real screen coordinate is ever negative, so this can never coincide. - private var lastMouseX = Double.NEGATIVE_INFINITY - private var lastMouseY = Double.NEGATIVE_INFINITY - - // `Recomposer.hasPendingWork` is Compose's own atomically-maintained "is there recomposition, - // apply-changes, or effect work outstanding" signal - the same one Compose's own test tooling - // (ComposeTestRule.waitForIdle()) uses. Reimplementing this by hand via applyScheduled/ - // hasFrameWaiters/recomposeJob had a real gap: recomposeJob only wraps `clock.sendFrame(...)`, - // and resuming a dispatched withFrameNanos continuation doesn't block until that continuation's - // *own* subsequent work (the actual recompose + apply-changes) finishes - it's dispatched, not - // synchronous. So recomposeJob could complete (and isComposeIdle() report idle) while the - // Recomposer was still mid-flight applying the very change a test's click()/hover() just - // triggered, letting an assertion race a stale pre-interaction render. hasPendingWork has no - // such gap since the Recomposer updates it itself as part of the same state transition. - override fun isComposeIdle(): Boolean = !recomposer.hasPendingWork - - /** - * Initialises the Compose runtime and pushes the base layer with [content]. - * - * Must be called once from [init]. Subsequent calls replace the content. - * - * @param content The root composable content for this screen. - */ - protected fun start(content: @Composable () -> Unit) { - recomposer = Recomposer(coroutineContext) - layerManager = LayerStackManager(recomposer) - - AUIScopeManager.scopes += composeScope - launch { recomposer.runRecomposeAndApplyChanges() } - - layerManager.push { _ -> - CompositionLocalProvider( - LocalScreen provides this, - LocalLayerManager provides layerManager, - ) { - Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.Center) { - content() - } - } - } - } - - // ── Rendering ───────────────────────────────────────────────────────── - - /** - * Measures and renders all active layers. - * - * In async mode the previous recompose job is joined before rendering, then a new - * job is launched if there are pending frame waiters. - */ - open fun renderNodes(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { - // Under a client GameTest's virtual dispatcher (see ComposeTestClockOverride), nothing - // launched on composeScope runs on its own - it only progresses when the scheduler backing - // it is advanced. Doing that here, once per real rendered frame, deterministically flushes - // recomposition/effects/delay() before the join below, instead of racing real threads. - testPump?.invoke() - if (asynchronous) { - recomposeJob?.let { runBlocking { it.join() } } - recomposeJob = null - } else if (hasFrameWaiters) { - hasFrameWaiters = false - clock.sendFrame(System.nanoTime()) - } - - val layersSnapshot = Snapshot.takeMutableSnapshot() - try { - layersSnapshot.enter { - var zOffset = 0f - for (layer in layerManager.layers) { - val root = layer.rootNode - root.measure(Constraints(maxWidth = width, maxHeight = height)) - root.render(0, 0, guiGraphics, mouseX, mouseY, partialTick, zOffset) - zOffset = root.getMaxZ(zOffset) + 10f - } - } - layersSnapshot.apply().check() - } finally { - layersSnapshot.dispose() - } - - if (asynchronous && hasFrameWaiters) { - hasFrameWaiters = false - recomposeJob = composeScope.launch { clock.sendFrame(System.nanoTime()) } - } - setInitialFocus() - } - - override fun render(guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { - super.render(guiGraphics, mouseX, mouseY, partialTick) - renderNodes(guiGraphics, mouseX, mouseY, partialTick) - } - - // ── Lifecycle ───────────────────────────────────────────────────────── - - private var composeDisposed = false - - override fun onClose() { - GLFW.glfwSetCursor(minecraft!!.window.window, 0L) - super.onClose() - disposeCompose() - } - - // vanilla's Minecraft.setScreen() calls removed() on the *old* screen for every screen - // transition - including a caller directly swapping to a new screen, which never goes - // through onClose() at all (that only fires when a screen closes itself, e.g. Escape). - // Without this override, every such swap - including the GameTest harness moving from one - // test's screen straight to the next's - leaked this screen's entire Compose runtime - // (Recomposer, composeScope and all its coroutines) running forever in the background. - // Confirmed the mechanism (not yet reproduced standalone): a CI-only crash deep inside - // Compose's own SlotTable/Recomposer internals surfaced as a suppressed exception logged - // between two unrelated, otherwise-passing tests - consistent with a leaked prior screen's - // recomposer still running concurrently against Compose-runtime state a newer screen's - // recomposition is also touching. - override fun removed() { - super.removed() - disposeCompose() - } - - private fun disposeCompose() { - if (composeDisposed) return - composeDisposed = true - recomposeJob?.cancel("GUI closing") - recomposer.close() - snapshotHandle.dispose() - layerManager.layers.forEach { it.dispose() } - AUIScopeManager.scopes -= composeScope - composeScope.cancel() - } - - // ── Input ───────────────────────────────────────────────────────────── - - private fun topNode() = layerManager.top?.rootNode - - override fun mouseClicked(mouseX: Double, mouseY: Double, button: Int): Boolean { - val top = topNode() ?: return super.mouseClicked(mouseX, mouseY, button) - processPointerEvent(top, mouseX, mouseY, PointerEventType.GLOBAL_PRESS, global = true) - val event = processPointerEvent(top, mouseX, mouseY, PointerEventType.PRESS) - return event.bypassSuper || super.mouseClicked(mouseX, mouseY, button) - } - - override fun mouseReleased(mouseX: Double, mouseY: Double, button: Int): Boolean { - val top = topNode() ?: return super.mouseReleased(mouseX, mouseY, button) - processPointerEvent(top, mouseX, mouseY, PointerEventType.GLOBAL_RELEASE, global = true) - val event = processPointerEvent(top, mouseX, mouseY, PointerEventType.RELEASE) - return event.bypassSuper || super.mouseReleased(mouseX, mouseY, button) - } - - override fun mouseMoved(mouseX: Double, mouseY: Double) { - val top = topNode() ?: return super.mouseMoved(mouseX, mouseY) - processPointerEvent(top, mouseX, mouseY, PointerEventType.MOVE) - processPointerEvent(top, mouseX, mouseY, PointerEventType.ENTER) { - it.isBounded(mouseX.toInt(), mouseY.toInt()) && !it.isBounded(lastMouseX.toInt(), lastMouseY.toInt()) - } - processPointerEvent(top, mouseX, mouseY, PointerEventType.EXIT) { - !it.isBounded(mouseX.toInt(), mouseY.toInt()) && it.isBounded(lastMouseX.toInt(), lastMouseY.toInt()) - } - lastMouseX = mouseX; lastMouseY = mouseY - super.mouseMoved(mouseX, mouseY) - } - - override fun mouseScrolled(mouseX: Double, mouseY: Double, scrollX: Double, scrollY: Double): Boolean { - val top = topNode() ?: return super.mouseScrolled(mouseX, mouseY, scrollX, scrollY) - processScrollEvent(top, mouseX, mouseY, scrollX, scrollY, PointerEventType.GLOBAL_SCROLL, global = true) - val event = processScrollEvent(top, mouseX, mouseY, scrollX, scrollY, PointerEventType.SCROLL) - return event.bypassSuper || super.mouseScrolled(mouseX, mouseY, scrollX, scrollY) - } - - override fun mouseDragged(mouseX: Double, mouseY: Double, button: Int, dragX: Double, dragY: Double): Boolean { - val top = topNode() ?: return super.mouseDragged(mouseX, mouseY, button, dragX, dragY) - val event = processDragEvent(top, mouseX, mouseY, button, dragX, dragY, PointerEventType.DRAG) - return event.bypassSuper || super.mouseDragged(mouseX, mouseY, button, dragX, dragY) - } - - override fun keyPressed(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - val top = topNode() ?: return super.keyPressed(keyCode, scanCode, modifiers) - val base = layerManager.layers.firstOrNull()?.rootNode - if (base != null) { - if (keyCode == InputConstants.KEY_LSHIFT && modifiers == 3) base.debug = !base.debug - if (base.debug && keyCode == InputConstants.KEY_LSHIFT) base.extraDebug = true - } - val event = processKeyEvent(top, keyCode, scanCode, modifiers) - return event.bypassSuper || super.keyPressed(keyCode, scanCode, modifiers) - } - - override fun keyReleased(keyCode: Int, scanCode: Int, modifiers: Int): Boolean { - val base = layerManager.layers.firstOrNull()?.rootNode - if (base != null && base.debug && keyCode == InputConstants.KEY_LSHIFT) base.extraDebug = false - return super.keyReleased(keyCode, scanCode, modifiers) - } - - override fun charTyped(codePoint: Char, modifiers: Int): Boolean { - val top = topNode() ?: return super.charTyped(codePoint, modifiers) - val event = processCharEvent(top, codePoint, modifiers) - return event.bypassSuper || super.charTyped(codePoint, modifiers) - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/Slot.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/Slot.kt deleted file mode 100644 index 4c41abe30..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/Slot.kt +++ /dev/null @@ -1,295 +0,0 @@ -package net.kernelpanicsoft.archie.gui - -import androidx.compose.runtime.* -import kotlinx.serialization.Serializable -import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates -import net.kernelpanicsoft.archie.gui.layer.LocalLayerDepth -import net.kernelpanicsoft.archie.gui.layout.* -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.onGloballyPositioned -import net.kernelpanicsoft.archie.gui.modifiers.position.padding -import net.kernelpanicsoft.archie.gui.modifiers.sizeIn -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.theme.LocalTheme -import net.kernelpanicsoft.archie.gui.theme.ThemeVariants -import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState -import net.kernelpanicsoft.archie.gui.composables.containers.Scrollable -import net.minecraft.client.gui.GuiGraphics - -/** - * Per-slot-group layout data reported back from the Compose layout to [ComposeContainerMenuBase]. - * - * Stores the group's absolute screen position, dimensions, slot positions, and clip bounds. - */ -@Serializable -data class SlotGroup( - var pos: IntCoordinates = IntCoordinates(0, 0), - var size: IntSize = IntSize(0, 0), - var enabled: Boolean = true, - var layerDepth: Int = 0, - var slots: MutableSet = mutableSetOf(), - var clip: IntRect? = null, -) - -/** - * Aggregated slot layout data for an entire [ComposeContainerScreen]. - * - * Contains named groups for block-entity slots and a separate [playerGroup] for the - * player inventory rows. - */ -@Serializable -data class SlotData( - val groups: MutableMap = mutableMapOf(), - val playerGroup: SlotGroup = SlotGroup(size = IntSize(9, 3)), -) { - /** All slot coordinates across all groups (does not include [playerGroup]). */ - val slots: Set get() = groups.values.filter { it.enabled }.flatMap { it.slots }.toSet() -} - -/** Provides the [SlotData] to all composables within a [ComposeContainerScreen]. */ -val LocalSlotData = compositionLocalOf { SlotData() } - -/** Provides the current [SlotGroup] to [Slot] composables inside a [Slots] container. */ -val LocalSlotGroup = compositionLocalOf { SlotGroup() } - -/** - * A layout-synchronous (non-Compose-state) holder for a [Scrollable]'s - * current clip bounds. - * - * The [Scrollable] updates [bounds] directly from its `onGloballyPositioned`/`onSizeChanged` - * callbacks, which fire every layout pass regardless of composition state. A descendant [Slot] - * reads [bounds] live, from its own `onGloballyPositioned` callback, at the same layout-pass - * granularity as [SlotGroup.pos]. Using [androidx.compose.runtime.mutableStateOf] here instead - * would only propagate the new value on the *next* recomposition - a composition-cycle lag - * behind position tracking that let a slot's clip bounds go stale exactly when the surrounding - * layout had just finished settling into a new position. - */ -class SlotClipSource { - private var origin: IntCoordinates = IntCoordinates(0, 0) - private var size: Size = Size(0, 0) - - var bounds: IntRect? = null - private set - - fun updateOrigin(newOrigin: IntCoordinates) { - origin = newOrigin - recompute() - } - - fun updateSize(newSize: Size) { - size = newSize - recompute() - } - - private fun recompute() { - bounds = if (size.width <= 0 || size.height <= 0) null else IntRect.fromPositionAndSize(origin, size) - } -} - -/** Provides the active [SlotClipSource] (if any) from the nearest ancestor scroll/clip container. */ -val LocalSlotClipBounds = compositionLocalOf { null } - -/** - * Defines a named region of inventory slots within a [ComposeContainerScreen]. - * - * This composable tracks its absolute on-screen position and populates the enclosing - * [SlotData] with the group's location and dimensions so that [ComposeContainerMenuBase] can - * register the corresponding vanilla [net.minecraft.world.inventory.Slot]s. - * - * @param id The name that matches the `handler(id, storage)` call in your menu. - * @param width The number of slot columns in this group. Defaults to 1. - * @param height The number of slot rows in this group. Defaults to 1. - * @param content The composable [Slot] grid inside this region. - * @return The [SlotGroup] that will be populated once the layout runs. - */ -@Composable -fun Slots( - id: String, - width: Int = 1, - height: Int = 1, - content: @Composable () -> Unit = { - Column { - repeat(height) { - Row { - repeat(width) { - Slot() - } - } - } - } - }, -): SlotGroup { - val layerDepth = LocalLayerDepth.current - val clipSource = LocalSlotClipBounds.current - val group = remember(id) { SlotGroup(size = IntSize(width = width, height = height)) } - group.size = IntSize(width = width, height = height) - group.layerDepth = layerDepth - val data = LocalSlotData.current - data.groups[id] = group - group.clip = clipSource?.bounds - - DisposableEffect(data, id, group) { - data.groups[id] = group - group.enabled = true - onDispose { - group.enabled = false - } - } - - // Clear slots so re-layout starts fresh each composition pass - group.slots.clear() - - Box( - modifier = Modifier.onGloballyPositioned { coords -> - group.pos = coords - group.layerDepth = layerDepth - // Live read, not a composition-time snapshot - see SlotClipSource. - group.clip = clipSource?.bounds - data.groups[id] = group - } - ) { - CompositionLocalProvider(LocalSlotGroup provides group) { - content() - } - } - return group -} - -/** - * Renders a single inventory slot graphic and records its absolute screen position. - * - * Triggers [ComposeContainerMenuBase.updateSlotData] once **all** named groups and the player - * group have reported their positions for this layout pass. - * - * @param modifier Additional modifiers applied to the slot layout node. - */ -@Composable -fun Slot(texture: String = "slot", modifier: Modifier = Modifier) { - val data = LocalSlotData.current - val group = LocalSlotGroup.current - val menu = LocalContainerMenu.current - val theme = LocalTheme.current - val composableTheme = theme.getComposableTheme(texture) - val state = composableTheme.getState(TextureStates.DEFAULT, ThemeVariants.DEFAULT) - var lastPos by remember { mutableStateOf(IntCoordinates(0, 0)) } - Layout( - name = "Slot", - measurePolicy = { _, _, constraints -> - MeasureResult(constraints.minWidth, constraints.minHeight) {} - }, - renderer = object : Renderer { - override fun render( - node: UINode, x: Int, y: Int, - guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, - ) { - guiGraphics.drawThemeState(state, x, y, node.width, node.height) - - super.render(node, x, y, guiGraphics, mouseX, mouseY, partialTick) - } - }, - modifier = Modifier - .sizeIn(minWidth = 18, minHeight = 18) - .onGloballyPositioned { pos -> - if (pos == lastPos) return@onGloballyPositioned // Skip if position hasn't changed since last report - if (group.slots.contains(lastPos)) - group.slots.remove(lastPos) - group.slots.add(pos) - lastPos = pos - tryUpdateMenu(data, menu) - } - .then(modifier), - ) -} - -/** - * Renders the standard 4-row player inventory (3 main rows + hotbar) as [Slot] composables. - * - * The 58-pixel gap between the main inventory and the hotbar matches the pixel offset used - * by [ComposeContainerMenuBase.addPlayerSlots] so positions reported to the menu are consistent. - */ -@Composable -fun PlayerSlots() { - val data = LocalSlotData.current - val layerDepth = LocalLayerDepth.current - val clipSource = LocalSlotClipBounds.current - data.playerGroup.layerDepth = layerDepth - data.playerGroup.clip = clipSource?.bounds - - // Clear so re-layout starts fresh - data.playerGroup.slots.clear() - - Box( - modifier = Modifier.onGloballyPositioned { coords -> - data.playerGroup.pos = coords - data.playerGroup.layerDepth = layerDepth - // Live read, not a composition-time snapshot - see SlotClipSource. - data.playerGroup.clip = clipSource?.bounds - } - ) { - Column { - // 3 rows of 9 (main inventory) - repeat(3) { - Row { - repeat(9) { - PlayerSlot() - } - } - } - // Hotbar (1 row of 9) - Row(modifier = Modifier.padding(top = 4)) { - repeat(9) { PlayerSlot() } - } - } - } -} - -/** A single player-inventory slot cell that tracks its position in [SlotData.playerGroup]. */ -@Composable -private fun PlayerSlot(texture: String = "slot", modifier: Modifier = Modifier) { - val data = LocalSlotData.current - val menu = LocalContainerMenu.current - val theme = LocalTheme.current - val composableTheme = theme.getComposableTheme(texture) - val state = composableTheme.getState(TextureStates.DEFAULT, ThemeVariants.DEFAULT) - var lastPos by remember { mutableStateOf(IntCoordinates(0, 0)) } - Layout( - name = "PlayerSlot", - measurePolicy = { _, _, constraints -> - MeasureResult(constraints.minWidth, constraints.minHeight) {} - }, - renderer = object : Renderer { - override fun render( - node: UINode, x: Int, y: Int, - guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, - ) { - guiGraphics.drawThemeState(state, x, y, node.width, node.height) - - super.render(node, x, y, guiGraphics, mouseX, mouseY, partialTick) - } - }, - modifier = modifier - .sizeIn(minWidth = 18, minHeight = 18) - .onGloballyPositioned { pos -> - if (pos == lastPos) return@onGloballyPositioned // Skip if position hasn't changed since last report - if (data.playerGroup.slots.contains(lastPos)) - data.playerGroup.slots.remove(lastPos) - data.playerGroup.slots.add(pos) - lastPos = pos - tryUpdateMenu(data, menu) - }, - ) -} - -/** - * Fires [ComposeContainerMenuBase.updateSlotData] only when every named slot group AND the - * player group have all reported their slot positions for this layout pass. - * - * This prevents partial updates where only some groups are positioned. - */ -private fun tryUpdateMenu(data: SlotData, menu: ComposeContainerMenuBase<*>) { - val namedGroupsFull = data.groups.values.all { g -> g.slots.size >= g.size.width * g.size.height } - val playerGroupFull = data.playerGroup.slots.size >= 36 - if (namedGroupsFull && playerGroupFull) { - menu.updateSlotData(data) - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotHighlightClipProvider.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotHighlightClipProvider.kt deleted file mode 100644 index ad1e2396f..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotHighlightClipProvider.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.kernelpanicsoft.archie.gui.access - -import net.kernelpanicsoft.archie.gui.layout.IntRect - -/** - * Allows container screens to clip vanilla's static `renderSlotHighlight(GuiGraphics, x, y, - * blitOffset)` call to a sub-rect of the slot's own 16x16 bounds. Returning `null` skips the - * highlight draw entirely (fully clipped away); returning the full unclipped rect renders it - * normally. - */ -interface SlotHighlightClipProvider { - /** - * Returns the sub-rect (in slot-local pixel coordinates) to clip the slot highlight to at - * screen position [x], [y], or `null` to skip the highlight entirely. - */ - fun slotHighlightClipRect(x: Int, y: Int): IntRect? -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthProvider.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthProvider.kt deleted file mode 100644 index f2a41b048..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthProvider.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.kernelpanicsoft.archie.gui.access - -import net.minecraft.world.inventory.Slot - -/** - * Allows container screens to tweak the Z depth used when vanilla renders a [Slot]. - * Returning `null` keeps Minecraft's default blit offset. - */ -interface SlotLayerDepthProvider { - fun slotRenderLayerOffset(slot: Slot): Float? = null -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/animation/Animation.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/animation/Animation.kt deleted file mode 100644 index 71af0cec7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/animation/Animation.kt +++ /dev/null @@ -1,94 +0,0 @@ -package net.kernelpanicsoft.archie.gui.animation - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableFloatStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import androidx.compose.runtime.withFrameNanos -import kotlin.math.roundToInt -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds - -/** Describes a time-based interpolation used by Archie GUI animations. */ -fun interface Easing { - /** Maps a linear progress [fraction] in `0f..1f` to an eased progress value. */ - fun transform(fraction: Float): Float -} - -/** - * Common easing curves for small UI interactions. - * - * The curves are intentionally lightweight so they can run smoothly in frequent recompositions. - */ -object Easings { - /** No easing; progress is directly proportional to elapsed time. */ - val Linear = Easing { it } - - /** Starts fast and decelerates into the target value, with no overshoot. */ - val OutCubic = Easing { t -> - val inv = 1f - t - 1f - inv * inv * inv - } - - /** Like [OutCubic] but overshoots the target slightly before settling. */ - val OutBack = Easing { t -> - val c1 = 1.70158f - val c3 = c1 + 1f - val shifted = t - 1f - 1f + c3 * shifted * shifted * shifted + c1 * shifted * shifted - } -} - -/** - * Timing parameters for float/int animations. - * - * @param durationMillis How long the animation takes to reach its target value. - * @param easing The curve applied to progress over that duration. - */ -data class AnimationSpec( - val durationMillis: Duration = 220.milliseconds, - val easing: Easing = Easings.OutCubic, -) - -/** Animates a float value toward [targetValue] using [spec]. */ -@Composable -fun animateFloat(targetValue: Float, spec: AnimationSpec = AnimationSpec()): Float { - var value by remember { mutableFloatStateOf(targetValue) } - - LaunchedEffect(targetValue, spec.durationMillis, spec.easing) { - val duration = spec.durationMillis - if (duration <= 0.milliseconds) { - value = targetValue - return@LaunchedEffect - } - - val start = value - val delta = targetValue - start - if (delta == 0f) return@LaunchedEffect - - val startTime = withFrameNanos { it } - var frameTime = startTime - do { - val elapsedNanos = frameTime - startTime - val rawProgress = (elapsedNanos / (duration.inWholeMilliseconds * 1_000_000f)).coerceIn(0f, 1f) - val eased = spec.easing.transform(rawProgress) - value = start + delta * eased - frameTime = withFrameNanos { it } - } while (rawProgress < 1f) - - value = targetValue - } - - return value -} - -/** Animates an integer by interpolating as float and rounding to the nearest pixel. */ -@Composable -fun animateInt(targetValue: Int, spec: AnimationSpec = AnimationSpec()): Int { - val animatedFloat = animateFloat(targetValue.toFloat(), spec) - return animatedFloat.roundToInt() -} - - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateComposables.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateComposables.kt deleted file mode 100644 index c84610858..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateComposables.kt +++ /dev/null @@ -1,44 +0,0 @@ -package net.kernelpanicsoft.archie.gui.blockentity - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.compositionLocalOf -import kotlinx.serialization.serializer - -/** - * Provides the current block entity state to composables in the composition tree. - * - * Use with `LocalBlockEntityState.current` to access the state, or use the - * [observeProperty] helper for convenience. - */ -val LocalBlockEntityState = compositionLocalOf { null } - -/** - * Observes a property on the block entity in the current composition context. - * - * Returns a [MutableState] that automatically triggers recomposition when the property changes. - * Must be called where [LocalBlockEntityState] has been provided (e.g. inside a block entity's - * screen composition) — otherwise it throws. - * - * ### Example - * ```kotlin - * @Composable - * fun MyComponent() { - * val powerState = observeProperty("power") - * Text("Power: ${powerState.value}") - * } - * ``` - * - * @param propertyName The name of the property to observe. - * @param T The expected type of the property. - * @return A [MutableState] of type T reflecting the property's current value. - * @throws RuntimeException if no [ComposeBlockEntityState] is available in the current composition. - */ -@Composable -inline fun observeProperty( - propertyName: String, - initialValue: T? = null, -): MutableState { - val state = LocalBlockEntityState.current ?: throw RuntimeException("No block entity state available in composition") - return state.observeProperty(propertyName, serializer(), initialValue) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateContainer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateContainer.kt deleted file mode 100644 index b51c0b60b..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateContainer.kt +++ /dev/null @@ -1,171 +0,0 @@ -package net.kernelpanicsoft.archie.gui.blockentity - -import kotlinx.serialization.InternalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.serializerOrNull -import net.kernelpanicsoft.archie.serialization.Sync -import net.minecraft.core.BlockPos -import net.minecraft.world.level.block.entity.BlockEntity -import kotlin.reflect.KClass -import kotlin.reflect.full.hasAnnotation -import kotlin.reflect.full.memberProperties -import kotlin.reflect.full.safeCast -import kotlin.reflect.jvm.isAccessible - -/** - * Wraps a block entity and tracks which properties have changed since the last sync. - * - * This class provides dirty tracking for efficient server-to-client synchronization. - * Only modified properties are included in generated state packets. - * - * @param blockEntity The block entity to monitor for changes. - */ -@OptIn(InternalSerializationApi::class) -class BlockEntityStateContainer( - val blockEntity: BlockEntity, -) { - /** The block position of the wrapped block entity. */ - val pos: BlockPos get() = blockEntity.blockPos - - /** Map of property names to their current values. */ - private val propertyValues = mutableMapOf() - - /** Serializers used to encode dirty properties into a [BlockEntityStatePacket], keyed by property name. */ - internal val propertySerializers = mutableMapOf>() - - @Suppress("UNCHECKED_CAST") - private fun anySerializer(serializer: KSerializer): KSerializer = serializer as KSerializer - - @Suppress("UNCHECKED_CAST") - private fun packetSerializer(propertyName: String): KSerializer = - propertySerializers[propertyName] as KSerializer - - init { - blockEntity::class.memberProperties.forEach { property -> - if (property.hasAnnotation()) - { - property.isAccessible = true - (property.returnType.classifier as KClass).serializerOrNull()?.let { serializer -> - propertySerializers[property.name] = serializer - } - } - } - } - - /** Set of property names that have changed since the last sync. */ - private val dirtyProperties = mutableSetOf() - - /** Server tick when this container was last synced. */ - var lastSyncTick: Long = 0 - - /** Whether any properties have changed. */ - val isDirty: Boolean get() = dirtyProperties.isNotEmpty() - - /** - * Records a property value and marks it dirty if it changed. - * - * @param propertyName The name of the property. - * @param value The new value. - * @return True if the value changed, false if it's the same as before. - */ - fun updateProperty(propertyName: String, value: T): Boolean { - - val oldValue = propertyValues[propertyName] - val changed = oldValue != value - if (changed) { - propertyValues[propertyName] = value - dirtyProperties.add(propertyName) - } - return changed - } - - /** - * Registers a serializer for [propertyName] if one isn't already known. - * - * Only needed for properties whose type can't be resolved automatically via - * [kotlinx.serialization.serializerOrNull] (see the `init` block). - * - * @param propertyName The name of the property. - * @param serializer The serializer to use when encoding this property. - */ - fun setPropertySerializer(propertyName: String, serializer: KSerializer) { - propertySerializers.putIfAbsent(propertyName, anySerializer(serializer)) - } - - /** - * Gets the current value of a property. - * - * @param propertyName The name of the property. - * @return The property value, or null if not tracked. - */ - fun getProperty(propertyName: String): Any? = propertyValues[propertyName] - - /** - * Gets the current value of a property with a type cast. - * - * @param propertyName The name of the property. - * @param T The expected type of the property. - * @return The property value cast to type T, or null if not found/wrong type. - */ - fun getProperty(propertyName: String, type: KClass): T? = - type.safeCast(propertyValues[propertyName]) - - /** - * Generates a state packet containing all dirty properties. - * - * @param serverTick The current server tick. - * @return A packet with all dirty property updates, or null if no changes. - */ - fun generatePacket(serverTick: Long): BlockEntityStatePacket? { - if (dirtyProperties.isEmpty()) return null - - val updates = dirtyProperties.associateWith { propertyName -> - propertyValues[propertyName].toSerializedValue(packetSerializer(propertyName)) - } - - return BlockEntityStatePacket( - pos = pos, - updates = updates, - timestamp = serverTick, - ) - } - - /** - * Clears the dirty flag, marking all properties as synced. - * - * Should be called after successfully sending a state packet to clients. - * - * @param serverTick The server tick when the sync completed. - */ - fun clearDirty(serverTick: Long) { - dirtyProperties.clear() - lastSyncTick = serverTick - } - - /** - * Gets all dirty property names. - * - * Useful for debugging or logging. - * - * @return An immutable set of property names that have changed. - */ - fun getDirtyProperties(): Set = dirtyProperties.toSet() - - /** - * Gets a snapshot of all tracked properties. - * - * @return An immutable map of all property names and values. - */ - fun getAllProperties(): Map = propertyValues.toMap() - - /** - * Resets all tracking, clearing dirty flags and property values. - * - * Useful when the block entity is unloaded or the container is no longer needed. - */ - fun reset() { - propertyValues.clear() - dirtyProperties.clear() - lastSyncTick = 0 - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateManager.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateManager.kt deleted file mode 100644 index 0edb46899..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateManager.kt +++ /dev/null @@ -1,162 +0,0 @@ -package net.kernelpanicsoft.archie.gui.blockentity - -import dev.architectury.event.events.common.TickEvent -import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel -import net.minecraft.core.BlockPos -import net.minecraft.server.level.ServerLevel -import net.minecraft.server.level.ServerPlayer -import net.minecraft.world.level.block.entity.BlockEntity -import java.util.concurrent.ConcurrentHashMap - -/** - * Server-side manager for tracking and syncing block entity state to clients. - * - * This manager: - * - Maintains a registry of block entities by position - * - Tracks which properties have changed on each block entity - * - Sends state packets to players who are observing each block entity - * - Cleans up state when block entities are unloaded - */ -object BlockEntityStateManager { - /** Registry of tracked block entities keyed by (level, pos) */ - private val trackedEntities = ConcurrentHashMap() - - /** Registry of players tracking each block entity, keyed by (level, pos) */ - private val trackedPlayers = ConcurrentHashMap>() - - /** - * Registers the server tick listener that drives [syncDirtyEntities] every tick. - * - * Must be called once during mod init. - */ - fun init() { - TickEvent.SERVER_POST.register { - syncDirtyEntities(it.tickCount.toLong()) - } - - } - - /** - * Registers a block entity for state tracking. - * - * Should be called when a container menu is opened for a block entity. - * - * @param blockEntity The block entity to track. - * @return The state container for this block entity. - */ - fun registerBlockEntity(blockEntity: BlockEntity): BlockEntityStateContainer { - val key = getKey(blockEntity) - val container = trackedEntities.computeIfAbsent(key) { - BlockEntityStateContainer(blockEntity) - } - return container - } - - /** - * Unregisters a block entity from state tracking. - * - * Should be called when a container menu is closed. - * - * @param blockEntity The block entity to untrack. - */ - fun unregisterBlockEntity(blockEntity: BlockEntity) { - val key = getKey(blockEntity) - trackedEntities.remove(key)?.reset() - trackedPlayers.remove(key) - } - - /** - * Adds a player to the tracking list for a block entity. - * - * The player will receive state packets when the block entity changes. - * - * @param blockEntity The block entity. - * @param player The player to add. - */ - fun addTrackedPlayer(blockEntity: BlockEntity, player: ServerPlayer) { - val key = getKey(blockEntity) - trackedPlayers.computeIfAbsent(key) { mutableSetOf() }.add(player) - } - - /** - * Removes a player from the tracking list for a block entity. - * - * @param blockEntity The block entity. - * @param player The player to remove. - */ - fun removeTrackedPlayer(blockEntity: BlockEntity, player: ServerPlayer) { - val key = getKey(blockEntity) - trackedPlayers[key]?.remove(player) - if (trackedPlayers[key]?.isEmpty() == true) { - trackedPlayers.remove(key) - } - } - - /** - * Gets the state container for a block entity, if it exists. - * - * @param blockEntity The block entity. - * @return The state container, or null if not registered. - */ - fun getContainer(blockEntity: BlockEntity): BlockEntityStateContainer? { - return trackedEntities[getKey(blockEntity)] - } - - /** - * Syncs all dirty block entities to their tracked players. - * - * Should be called once per server tick via a tick event. - * - * @param currentTick The current server tick. - * @param networkChannel The network channel to send packets through. - */ - fun syncDirtyEntities( - currentTick: Long, - networkChannel: (BlockEntityStatePacket, List) -> Unit = DEFAULT_NETWORK_SENDER, - ) { - trackedEntities.forEach { (key, container) -> - val packet = container.generatePacket(currentTick) ?: return@forEach - val players = trackedPlayers[key] ?: emptySet() - if (players.isNotEmpty()) { - networkChannel(packet, players.toList()) - container.clearDirty(currentTick) - } - } - } - - /** - * Clears all tracking data. - * - * Useful for cleanup on server shutdown. - */ - fun clear() { - trackedEntities.values.forEach { it.reset() } - trackedEntities.clear() - trackedPlayers.clear() - } - - /** - * Gets a unique key for a block entity based on level and position. - * - * @param blockEntity The block entity. - * @return A unique key string. - */ - private fun getKey(blockEntity: BlockEntity): String { - val levelName = blockEntity.level?.hashCode() ?: 0 - return "${levelName}_${blockEntity.blockPos.x}_${blockEntity.blockPos.y}_${blockEntity.blockPos.z}" - } - - /** - * Default network sender used by [syncDirtyEntities]; sends [BlockEntityStatePacket]s to the - * given players via [ArchieNetworkChannel]. - */ - private val DEFAULT_NETWORK_SENDER: (BlockEntityStatePacket, List) -> Unit = { packet, players -> - ArchieNetworkChannel.toPlayers(players, packet) - } -} - -/** - * Convenience extension to get or create a state container for a block entity. - */ -fun BlockEntity.getStateContainer(): BlockEntityStateContainer = - BlockEntityStateManager.getContainer(this) ?: BlockEntityStateManager.registerBlockEntity(this) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacket.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacket.kt deleted file mode 100644 index 62cf55ea8..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacket.kt +++ /dev/null @@ -1,102 +0,0 @@ -package net.kernelpanicsoft.archie.gui.blockentity - -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import net.kernelpanicsoft.archie.serialization.SerializationManager -import net.kernelpanicsoft.archie.serialization.serializers.SBlockPos -import net.minecraft.core.BlockPos - -/** - * A network packet that carries block entity state changes from server to client. - * - * This packet is used to synchronize block entity property changes with connected clients, - * enabling reactive UI updates in Compose-based screens. - * - * @property pos The block position of the block entity being updated. - * @property updates A map of property names to their serialized values. - * @property timestamp Server tick when this packet was created (for ordering/deduplication). - */ -@Serializable -data class BlockEntityStatePacket( - val pos: SBlockPos, - val updates: Map = emptyMap(), - val timestamp: Long = 0, -) { - /** - * A serialized property value that can be transmitted over the network. - * - * Supports common types (Int, String, Boolean, Float, Double, etc.) as well as - * complex types that need NBT serialization. - */ - @Serializable - sealed class SerializedValue { - @Serializable - data class IntValue(val value: Int) : SerializedValue() - - @Serializable - data class StringValue(val value: String) : SerializedValue() - - @Serializable - data class BooleanValue(val value: Boolean) : SerializedValue() - - @Serializable - data class FloatValue(val value: Float) : SerializedValue() - - @Serializable - data class DoubleValue(val value: Double) : SerializedValue() - - @Serializable - data class LongValue(val value: Long) : SerializedValue() - - @Serializable - data class ByteValue(val value: Byte) : SerializedValue() - - - - - - @Serializable - data class CBORValue(val value: ByteArray) : SerializedValue() - { - override fun equals(other: Any?): Boolean - { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as CBORValue - - return value.contentEquals(other.value) - } - - override fun hashCode(): Int - { - return value.contentHashCode() - } - } - - @Serializable - object NullValue : SerializedValue() - } - - companion object { - /** - * Creates a new packet with a single property update. - * - * @param pos The block position. - * @param propertyName The name of the property being updated. - * @param value The new value. - * @param timestamp The server tick. - */ - fun singleUpdate( - pos: BlockPos, - propertyName: String, - value: SerializedValue, - timestamp: Long = 0, - ): BlockEntityStatePacket = BlockEntityStatePacket( - pos = pos, - updates = mapOf(propertyName to value), - timestamp = timestamp, - ) - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt deleted file mode 100644 index ceb795ab5..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt +++ /dev/null @@ -1,118 +0,0 @@ -package net.kernelpanicsoft.archie.gui.blockentity - -import kotlinx.serialization.KSerializer -import kotlinx.serialization.ExperimentalSerializationApi -import net.kernelpanicsoft.archie.block.entity.NBTBlockEntity -import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel -import net.kernelpanicsoft.archie.serialization.SerializationManager -import net.minecraft.core.BlockPos -import net.minecraft.server.level.ServerLevel -import java.util.concurrent.ConcurrentHashMap - -/** - * Client-side registry of block entity states. - * - * Stores Compose state objects for active block entities so they can be updated - * when network packets arrive. - */ -private val clientBlockEntityStates = ConcurrentHashMap() - -/** - * Gets or creates a Compose state for a block entity by position. - * - * @param pos The block position. - * @return The [ComposeBlockEntityState] for that position. - */ -fun getOrCreateBlockEntityState(pos: BlockPos): ComposeBlockEntityState { - val key = "${pos.x}_${pos.y}_${pos.z}" - return clientBlockEntityStates.computeIfAbsent(key) { - ComposeBlockEntityState(pos) - } -} - -/** - * Removes a block entity state from the client registry. - * - * @param pos The block position. - */ -fun removeBlockEntityState(pos: BlockPos) { - val key = "${pos.x}_${pos.y}_${pos.z}" - clientBlockEntityStates.remove(key) -} - -/** - * Registers block entity state packets with [ArchieNetworkChannel]. - * - * Handles both directions: applying incoming [BlockEntityStatePacket]s to the client-side - * [ComposeBlockEntityState] registry, and applying incoming [BlockEntityUpdatePacket]s (client - * edits) to the server-side [BlockEntityStateManager]-tracked [NBTBlockEntity]. - */ -object BlockEntityStatePacketRegistry { - /** Registers the clientbound and serverbound packet handlers described above. */ - fun register() { - ArchieNetworkChannel.clientbound { packet, context -> - // Update the client-side state with new values from the packet - val state = getOrCreateBlockEntityState(packet.pos) - packet.updates.forEach { (propertyName, value) -> - state.updateProperty(propertyName, value) - } - } - - ArchieNetworkChannel.serverbound { packet, context -> - val player = context.player - val level = player.level() as? ServerLevel ?: return@serverbound - val blockEntity = level.getBlockEntity(packet.pos) as? NBTBlockEntity ?: return@serverbound - val container = BlockEntityStateManager.getContainer(blockEntity) ?: return@serverbound - - packet.updates.forEach { (propertyName, serializedValue) -> - val serializer = container.propertySerializers[propertyName] - if (serializer != null) { - val deserializedValue = serializedValue.deserialize(serializer) - container.updateProperty(propertyName, deserializedValue) - blockEntity.updateProperty(propertyName, serializer as KSerializer, deserializedValue as Any) - } - } - } - } -} - -/** - * Extension function to convert SerializedValue back to its original Kotlin type. - */ -@OptIn(ExperimentalSerializationApi::class) -internal fun BlockEntityStatePacket.SerializedValue.deserialize(serializer: KSerializer? = null): Any? = when (this) { - is BlockEntityStatePacket.SerializedValue.IntValue -> this.value - is BlockEntityStatePacket.SerializedValue.StringValue -> this.value - is BlockEntityStatePacket.SerializedValue.BooleanValue -> this.value - is BlockEntityStatePacket.SerializedValue.FloatValue -> this.value - is BlockEntityStatePacket.SerializedValue.DoubleValue -> this.value - is BlockEntityStatePacket.SerializedValue.LongValue -> this.value - is BlockEntityStatePacket.SerializedValue.ByteValue -> this.value - is BlockEntityStatePacket.SerializedValue.NullValue -> null - is BlockEntityStatePacket.SerializedValue.CBORValue -> { - if (serializer != null) { - SerializationManager.cbor.decodeFromByteArray(serializer, this.value) - } else { - // If no serializer is provided, we can't deserialize CBOR, so return the raw bytes or null - this.value - } - } -} - -/** - * Extension function to convert common Kotlin types to [BlockEntityStatePacket.SerializedValue]. - */ -@Suppress("UNCHECKED_CAST") -@OptIn(ExperimentalSerializationApi::class) -fun T?.toSerializedValue(serializer: KSerializer): BlockEntityStatePacket.SerializedValue = when (this) { - null -> BlockEntityStatePacket.SerializedValue.NullValue - is Int -> BlockEntityStatePacket.SerializedValue.IntValue(this) - is String -> BlockEntityStatePacket.SerializedValue.StringValue(this) - is Boolean -> BlockEntityStatePacket.SerializedValue.BooleanValue(this) - is Float -> BlockEntityStatePacket.SerializedValue.FloatValue(this) - is Double -> BlockEntityStatePacket.SerializedValue.DoubleValue(this) - is Long -> BlockEntityStatePacket.SerializedValue.LongValue(this) - is Byte -> BlockEntityStatePacket.SerializedValue.ByteValue(this) - is ByteArray -> BlockEntityStatePacket.SerializedValue.CBORValue(this) - else -> BlockEntityStatePacket.SerializedValue.CBORValue(SerializationManager.cbor.encodeToByteArray(serializer, this)) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityUpdatePacket.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityUpdatePacket.kt deleted file mode 100644 index 1a7836c07..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityUpdatePacket.kt +++ /dev/null @@ -1,37 +0,0 @@ -package net.kernelpanicsoft.archie.gui.blockentity - -import kotlinx.serialization.Serializable -import net.kernelpanicsoft.archie.serialization.serializers.SBlockPos -import net.minecraft.core.BlockPos - -/** - * A network packet that carries block entity state updates from client to server. - * - * This packet is used to send client-side modifications of block entity properties back to the server. - * - * @property pos The block position of the block entity being updated. - * @property updates A map of property names to their serialized values. - */ -@Serializable -data class BlockEntityUpdatePacket( - val pos: SBlockPos, - val updates: Map, -) { - companion object { - /** - * Creates a new packet with a single property update. - * - * @param pos The block position. - * @param propertyName The name of the property being updated. - * @param value The new value. - */ - fun singleUpdate( - pos: BlockPos, - propertyName: String, - value: BlockEntityStatePacket.SerializedValue, - ): BlockEntityUpdatePacket = BlockEntityUpdatePacket( - pos = pos, - updates = mapOf(propertyName to value), - ) - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/ComposeBlockEntityState.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/ComposeBlockEntityState.kt deleted file mode 100644 index f35fa9681..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/ComposeBlockEntityState.kt +++ /dev/null @@ -1,148 +0,0 @@ -package net.kernelpanicsoft.archie.gui.blockentity - -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KSerializer -import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel -import net.minecraft.core.BlockPos - -/** - * Client-side state holder for a block entity's synchronized properties. - * - * Each property is wrapped in a Compose [MutableState], allowing composables to react - * to changes automatically through recomposition. - * - * @param pos The block position of the block entity. - */ -class ComposeBlockEntityState( - val pos: BlockPos, -) { - /** Map of property names to their Compose state values */ - val propertyStates = mutableMapOf>() - - /** Serializers used to encode/decode each observed property, keyed by property name. */ - val propertySerializers = mutableMapOf>() - - @Suppress("UNCHECKED_CAST") - private fun anySerializer(serializer: KSerializer): KSerializer = serializer as KSerializer - - @Suppress("UNCHECKED_CAST") - private fun typedSerializer(propertyName: String): KSerializer? = propertySerializers[propertyName] as? KSerializer - - @Suppress("UNCHECKED_CAST") - private fun getOrCreateState(propertyName: String, initialValue: T?): MutableState { - return propertyStates.computeIfAbsent(propertyName) { - PropertyState(this, propertyName, mutableStateOf(initialValue)) as MutableState - } as MutableState - } - - /** - * Gets or creates a Compose state for a property with a specific type. - * - * @param propertyName The name of the property. - * @param initialValue The initial value (optional, defaults to null). - * @param T The expected type of the property. - * @return A [MutableState] of type T that can be observed in composables. - */ - fun observeProperty( - propertyName: String, - serializer: KSerializer, - initialValue: T? = null, - ): MutableState { - propertySerializers[propertyName] = anySerializer(serializer) - return getOrCreateState(propertyName, initialValue) - } - - /** - * A [MutableState] delegate that forwards writes to [ComposeBlockEntityState.sendUpdatedProperty], - * so setting [value] from a composable both updates local state and pushes the change to the server. - */ - class PropertyState(private val state: ComposeBlockEntityState, private val propertyName: String, internal val mutableState: MutableState) : MutableState by mutableState - { - override var value: T - get() = mutableState.value - set(value) - { - mutableState.value = value - state.sendUpdatedProperty(propertyName, value) - } - } - - /** - * Updates a property value from a network packet. - * - * If the property doesn't exist yet, it will be created. - * - * @param propertyName The name of the property. - * @param value The new serialized value from the network packet. - */ - fun updateProperty(propertyName: String, value: BlockEntityStatePacket.SerializedValue) { - val deserializedValue = value.deserialize(propertySerializers[propertyName]) - val state = propertyStates.computeIfAbsent(propertyName) { - mutableStateOf(deserializedValue) - } - state.value = deserializedValue - } - - /** - * Updates a property value and sends the change to the server. - * - * This method should be called when a client-side interaction changes a property. - * - * @param propertyName The name of the property. - * @param value The new value. - */ - fun sendUpdatedProperty(propertyName: String, value: T) { - val serializer = typedSerializer(propertyName) ?: run { - println("No serializer found for property $propertyName. Cannot send update to server.") - return - } - - val serializedValue = value.toSerializedValue(serializer) - val packet = BlockEntityUpdatePacket.singleUpdate(pos, propertyName, serializedValue) - ArchieNetworkChannel.toServer(packet) - } - - - - /** - * Gets the current value of a property. - * - * @param propertyName The name of the property. - * @return The property value, or null if not tracked. - */ - fun getProperty(propertyName: String): Any? { - return propertyStates[propertyName]?.value - } - - /** - * Gets the current value of a property with type casting. - * - * @param propertyName The name of the property. - * @param T The expected type. - * @return The property value cast to T, or null if not found/wrong type. - */ - @Suppress("UNCHECKED_CAST") - fun getPropertyTyped(propertyName: String): T? { - return propertyStates[propertyName]?.value as? T - } - - /** - * Clears all tracked properties. - * - * Useful when the block entity is unloaded or the state is no longer needed. - */ - fun clear() { - propertyStates.clear() - } - - /** - * Gets all currently tracked properties. - * - * @return A map of property names to their current values. - */ - fun getAllProperties(): Map { - return propertyStates.mapValues { (_, state) -> state.value } - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Divider.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Divider.kt deleted file mode 100644 index ba6ca4344..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Divider.kt +++ /dev/null @@ -1,54 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.basic - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.appearance.BackgroundModifier -import net.kernelpanicsoft.archie.gui.modifiers.fillMaxHeight -import net.kernelpanicsoft.archie.gui.modifiers.fillMaxWidth -import net.kernelpanicsoft.archie.gui.modifiers.height -import net.kernelpanicsoft.archie.gui.modifiers.width -import net.kernelpanicsoft.archie.gui.util.KColor - -/** - * Draws a thin horizontal or vertical separator line. - * - * @param color The line's fill color, as an ARGB int. - * @param thickness The line's thickness in pixels along its short axis. - * @param vertical When `true`, the line fills its height and is [thickness] pixels wide; - * when `false` (default), it fills its width and is [thickness] pixels tall. - */ -@Composable -fun Divider( - modifier: Modifier = Modifier, - color: Int = KColor.GRAY.argb, - thickness: Int = 1, - vertical: Boolean = false, -) { - val axisModifier = if (vertical) { - Modifier.width(thickness).fillMaxHeight() - } else { - Modifier.height(thickness).fillMaxWidth() - } - - Spacer(modifier = axisModifier.then(BackgroundModifier(color, color)).then(modifier)) -} - -/** Convenience horizontal divider. */ -@Stable -@Composable -fun HorizontalDivider( - modifier: Modifier = Modifier, - color: Int = KColor.GRAY.argb, - thickness: Int = 1, -) = Divider(modifier = modifier, color = color, thickness = thickness) - -/** Convenience vertical divider. */ -@Stable -@Composable -fun VerticalDivider( - modifier: Modifier = Modifier, - color: Int = KColor.GRAY.argb, - thickness: Int = 1, -) = Divider(modifier = modifier, color = color, thickness = thickness, vertical = true) - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt deleted file mode 100644 index 9259def4d..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt +++ /dev/null @@ -1,45 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.basic - -import androidx.compose.runtime.Composable -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.theme.ThemeVariants -import net.kernelpanicsoft.archie.transfer.ArchieEnergyStorage - -/** - * A themed energy-level indicator (looked up in the current theme as `"energy_bar"`), filled - * with a solid color up to `energy / capacity`. - * - * Shares [ProgressBar]'s rendering core but defaults to a bottom-up fill and an energy-flavored - * color, matching how most tech mods orient a power gauge. - * - * @param energy The current stored amount (see [ArchieEnergyStorage.getStoredAmount]). - * @param capacity The maximum capacity (see [ArchieEnergyStorage.getCapacity]); a non-positive - * value renders as empty rather than dividing by zero. - * @param modifier Additional modifiers applied to the outer container. - * @param direction Which edge the fill grows from. - * @param fillColor ARGB color of the filled portion. - * @param variant The theme variant used for the track texture. - */ -@Composable -fun EnergyBar( - energy: Long, - capacity: Long, - modifier: Modifier = Modifier, - direction: ProgressDirection = ProgressDirection.BOTTOM_TO_TOP, - fillColor: Int = 0xFFFF5C33.toInt(), - variant: String = ThemeVariants.DEFAULT, -) -{ - val fraction = if (capacity <= 0L) 0f else (energy.toDouble() / capacity.toDouble()).toFloat().coerceIn(0f, 1f) - ThemedFillBar("energy_bar", fraction, modifier, direction, fillColor, variant) -} - -/** Convenience overload reading directly from an [ArchieEnergyStorage]. */ -@Composable -fun EnergyBar( - storage: ArchieEnergyStorage, - modifier: Modifier = Modifier, - direction: ProgressDirection = ProgressDirection.BOTTOM_TO_TOP, - fillColor: Int = 0xFFFF5C33.toInt(), - variant: String = ThemeVariants.DEFAULT, -) = EnergyBar(storage.getStoredAmount(), storage.getCapacity(), modifier, direction, fillColor, variant) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt deleted file mode 100644 index da29985a0..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt +++ /dev/null @@ -1,91 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.basic - -import androidx.compose.runtime.Composable -import dev.architectury.fluid.FluidStack -import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.MeasureResult -import net.kernelpanicsoft.archie.gui.layout.Renderer -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.sizeIn -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.render.AFluidRenderPlatform -import net.kernelpanicsoft.archie.gui.theme.LocalTheme -import net.kernelpanicsoft.archie.gui.theme.ThemeVariants -import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.kernelpanicsoft.archie.gui.util.extension.scissor -import net.minecraft.client.gui.GuiGraphics - -private const val FLUID_TANK_MIN_WIDTH = 18 -private const val FLUID_TANK_MIN_HEIGHT = 54 -private const val FLUID_TANK_INSET = 1 - -/** - * A themed fluid-level indicator (looked up in the current theme as `"fluid_tank"`): a tank - * frame sprite with the real fluid texture and tint (via [AFluidRenderPlatform]) filling it - * bottom-up to `fluid.amount / capacity`. - * - * The fluid sprite is stretched to the tank's interior and clipped with a scissor rather than - * tiled per-block, so it won't repeat at a pixel-perfect 16px grid - a reasonable tradeoff for a - * UI meter over the complexity of manual tiled-quad rendering. See [AFluidRenderPlatform] for - * why this needs a platform bridge at all: Fabric and NeoForge expose a fluid's client - * appearance through unrelated APIs. - * - * @param fluid The fluid and amount to display; an empty stack renders just the tank frame. - * @param capacity The tank's total capacity; a non-positive value renders as empty rather than - * dividing by zero. - * @param modifier Additional modifiers applied to the outer container. - * @param variant The theme variant used for the tank frame texture. - */ -@Composable -fun FluidTank( - fluid: FluidStack, - capacity: Long, - modifier: Modifier = Modifier, - variant: String = ThemeVariants.DEFAULT, -) -{ - val theme = LocalTheme.current.getComposableTheme("fluid_tank") - val sizeModifier = Modifier.sizeIn(minWidth = FLUID_TANK_MIN_WIDTH, minHeight = FLUID_TANK_MIN_HEIGHT) - - Layout( - name = "FluidTank", - measurePolicy = { _, _, constraints -> MeasureResult(constraints.minWidth, constraints.minHeight) {} }, - modifier = sizeModifier.then(modifier), - renderer = object : Renderer - { - override fun render( - node: UINode, x: Int, y: Int, - guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, - ) = guiGraphics { - val state = theme.getState(TextureStates.DEFAULT, variant) - drawThemeState(state, x, y, node.width, node.height) - - if (fluid.isEmpty || capacity <= 0L) return@guiGraphics - - val fraction = (fluid.amount.toDouble() / capacity.toDouble()).coerceIn(0.0, 1.0).toFloat() - val sprite = AFluidRenderPlatform.getStillSprite(fluid.fluid) ?: return@guiGraphics - - val innerX = x + FLUID_TANK_INSET - val innerY = y + FLUID_TANK_INSET - val innerW = (node.width - FLUID_TANK_INSET * 2).coerceAtLeast(0) - val innerH = (node.height - FLUID_TANK_INSET * 2).coerceAtLeast(0) - val fillH = (innerH * fraction).toInt() - val fillY = innerY + innerH - fillH - - if (innerW <= 0 || fillH <= 0) return@guiGraphics - - val tint = AFluidRenderPlatform.getTintColor(fluid.fluid) - val a = ((tint ushr 24) and 0xFF) / 255f - val r = ((tint ushr 16) and 0xFF) / 255f - val g = ((tint ushr 8) and 0xFF) / 255f - val b = (tint and 0xFF) / 255f - - scissor(innerX, fillY, innerX + innerW, fillY + fillH) { - blit(innerX, innerY, innerW, innerH, 0, sprite, r, g, b, a) - } - } - }, - ) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Icon.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Icon.kt deleted file mode 100644 index 0a1d67533..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Icon.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.basic - -import androidx.compose.runtime.Composable -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.size -import net.minecraft.resources.ResourceLocation - -/** - * Convenience wrapper around [Texture] for fixed-size icon sprites. - * - * @param texture The sprite sheet/texture location. - * @param size The icon's rendered width and height, in pixels. - * @param uOffset The sprite's left edge within [texture], in texture pixels. - * @param vOffset The sprite's top edge within [texture], in texture pixels. - * @param u The sprite's source width within [texture]. Defaults to [size]. - * @param v The sprite's source height within [texture]. Defaults to [size]. - * @param textureWidth The full width of [texture], in pixels. - * @param textureHeight The full height of [texture], in pixels. - */ -@Composable -fun Icon( - texture: ResourceLocation, - size: Int = 16, - uOffset: Float = 0f, - vOffset: Float = 0f, - u: Int = size, - v: Int = size, - textureWidth: Int = 256, - textureHeight: Int = 256, - modifier: Modifier = Modifier, -) { - Texture( - loc = texture, - uOffset = uOffset, - vOffset = vOffset, - u = u, - v = v, - textureWidth = textureWidth, - textureHeight = textureHeight, - modifier = Modifier.size(size, size).then(modifier), - ) -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt deleted file mode 100644 index ba7aa018d..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt +++ /dev/null @@ -1,108 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.basic - -import androidx.compose.runtime.Composable -import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.MeasureResult -import net.kernelpanicsoft.archie.gui.layout.Renderer -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.sizeIn -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.theme.LocalTheme -import net.kernelpanicsoft.archie.gui.theme.ThemeVariants -import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.minecraft.client.gui.GuiGraphics - -internal const val FILL_BAR_MIN_WIDTH = 90 -internal const val FILL_BAR_MIN_HEIGHT = 16 - -/** Which edge of a [ProgressBar]/[net.kernelpanicsoft.archie.gui.composables.basic.EnergyBar] the fill grows from. */ -enum class ProgressDirection -{ - LEFT_TO_RIGHT, - RIGHT_TO_LEFT, - TOP_TO_BOTTOM, - BOTTOM_TO_TOP, -} - -/** - * Shared rendering core for [ProgressBar] and [net.kernelpanicsoft.archie.gui.composables.basic.EnergyBar]: - * a themed track sprite (looked up as [themeName] in the current theme) filled with a solid - * color up to [progress]. - */ -@Composable -internal fun ThemedFillBar( - themeName: String, - progress: Float, - modifier: Modifier, - direction: ProgressDirection, - fillColor: Int, - variant: String, -) -{ - val clamped = progress.coerceIn(0f, 1f) - val theme = LocalTheme.current.getComposableTheme(themeName) - val sizeModifier = Modifier.sizeIn(minWidth = FILL_BAR_MIN_WIDTH, minHeight = FILL_BAR_MIN_HEIGHT) - - Layout( - name = themeName, - measurePolicy = { _, _, constraints -> MeasureResult(constraints.minWidth, constraints.minHeight) {} }, - modifier = sizeModifier.then(modifier), - renderer = object : Renderer - { - override fun render( - node: UINode, x: Int, y: Int, - guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, - ) = guiGraphics { - val state = theme.getState(TextureStates.DEFAULT, variant) - drawThemeState(state, x, y, node.width, node.height) - - var fx = x - var fy = y - var fw = node.width - var fh = node.height - when (direction) - { - ProgressDirection.LEFT_TO_RIGHT -> fw = (node.width * clamped).toInt() - ProgressDirection.RIGHT_TO_LEFT -> - { - fw = (node.width * clamped).toInt() - fx = x + node.width - fw - } - - ProgressDirection.TOP_TO_BOTTOM -> fh = (node.height * clamped).toInt() - ProgressDirection.BOTTOM_TO_TOP -> - { - fh = (node.height * clamped).toInt() - fy = y + node.height - fh - } - } - if (fw > 0 && fh > 0) fill(fx, fy, fx + fw, fy + fh, fillColor) - } - }, - ) -} - -/** - * A themed linear progress indicator: an empty-track sprite from the current theme (looked up as - * `"progress_bar"`), filled with a solid color up to [progress]. - * - * There's no built-in animation or recomposition trigger here - drive [progress] from an - * observed block entity field (see [net.kernelpanicsoft.archie.gui.blockentity.observeProperty]) - * for a live machine-processing indicator. - * - * @param progress Fraction complete, clamped to `0f..1f`. - * @param modifier Additional modifiers applied to the outer container. - * @param direction Which edge the fill grows from. - * @param fillColor ARGB color of the filled portion. - * @param variant The theme variant used for the track texture. - */ -@Composable -fun ProgressBar( - progress: Float, - modifier: Modifier = Modifier, - direction: ProgressDirection = ProgressDirection.LEFT_TO_RIGHT, - fillColor: Int = 0xFF6BA8FF.toInt(), - variant: String = ThemeVariants.DEFAULT, -) = ThemedFillBar("progress_bar", progress, modifier, direction, fillColor, variant) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Spacer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Spacer.kt deleted file mode 100644 index e70879874..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Spacer.kt +++ /dev/null @@ -1,37 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.basic - -import androidx.compose.runtime.Composable -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.MeasureResult -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.fillMaxSize - -/** - * An invisible layout composable that expands to fill available space. - * - * `Spacer` is the idiomatic way to push siblings apart inside [net.kernelpanicsoft.archie.gui.layout.Row] - * or [net.kernelpanicsoft.archie.gui.layout.Column] arrangements. By default it stretches - * to consume all remaining space in its parent. - * - * ### Example - * ```kotlin - * Row { - * Text(Component.literal("Left")) - * Spacer() // pushes "Right" to the far end - * Text(Component.literal("Right")) - * } - * ``` - * - * @param modifier Additional modifiers; most commonly used to constrain the spacer to a - * fixed size with `Modifier.size(width, height)`. - */ -@Composable -fun Spacer(modifier: Modifier = Modifier) { - Layout( - name = "Spacer", - measurePolicy = { _, _, constraints -> - MeasureResult(constraints.minWidth, constraints.minHeight) {} - }, - modifier = modifier.fillMaxSize(), - ) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Text.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Text.kt deleted file mode 100644 index ab8cae180..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Text.kt +++ /dev/null @@ -1,99 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.basic - -import androidx.compose.runtime.Composable -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.MeasureResult -import net.kernelpanicsoft.archie.gui.layout.Renderer -import net.kernelpanicsoft.archie.gui.layout.Size -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.theme.LocalTheme -import net.kernelpanicsoft.archie.gui.util.KColor -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.kernelpanicsoft.archie.gui.util.extension.pose -import net.kernelpanicsoft.archie.util.minecraftClient -import net.minecraft.client.Minecraft -import net.minecraft.client.gui.Font -import net.minecraft.client.gui.GuiGraphics -import net.minecraft.network.chat.Component - -/** - * Returns the rendered pixel size (width × height) of [text] at the given [scale]. - * - * Useful for sizing containers to exactly fit their text content before composition. - * - * @param text The [Component] whose rendered dimensions are measured. - * @param scale Font scale factor (1.0 = native size). - * @param font The [Font] to measure with; defaults to Minecraft's standard font. - */ -fun getTextSize( - text: Component, - scale: Float = 1f, - font: Font = minecraftClient.font, -): Size = Size((font.width(text) * scale).toInt(), (font.lineHeight * scale).toInt()) - -/** - * Renders a [Component] using Minecraft's font renderer. - * - * Supports optional uniform scaling, a custom font face, and an ARGB text color. - * The node automatically sizes itself to the minimum dimensions needed to display the - * text at the requested scale. - * - * ### Example - * ```kotlin - * Text( - * text = Component.literal("Hello, Archie!"), - * fontScale = 1.5f, - * color = KColor.YELLOW, - * ) - * ``` - * - * @param text The text component to render. - * @param fontScale Uniform scale applied to the font. Default `1f` (native size). - * @param font The [Font] used for rendering and size measurement. - * @param color Text color. Defaults to the current [LocalTheme]'s light text color. - * @param dropShadow Whether to render the vanilla text drop shadow. Default `true`. - * @param modifier Additional modifiers applied to the layout node. - */ -@Composable -fun Text( - text: Component, - fontScale: Float = 1f, - font: Font = minecraftClient.font, - color: KColor = LocalTheme.current.lightTextColor, - dropShadow: Boolean = true, - modifier: Modifier = Modifier, -) { - Layout( - name = "Text", - measurePolicy = { _, _, constraints -> - val textSize = getTextSize(text, fontScale, font) - MeasureResult( - textSize.width.coerceIn(constraints.minWidth, constraints.maxWidth), - textSize.height.coerceIn(constraints.minHeight, constraints.maxHeight), - ) {} - }, - renderer = object : Renderer { - override fun render( - node: UINode, - x: Int, y: Int, - guiGraphics: GuiGraphics, - mouseX: Int, mouseY: Int, - partialTick: Float, - ) = guiGraphics { - if (fontScale != 1f) - { - pose { - scale(fontScale, fontScale, fontScale) - translate(x / fontScale, y / fontScale, 0f) - drawString(font, text, 0, 0, color.argb, dropShadow) - } - } else - { - drawString(font, text, x, y, color.argb, dropShadow) - } - } - }, - modifier = modifier, - ) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Texture.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Texture.kt deleted file mode 100644 index 5b473356b..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Texture.kt +++ /dev/null @@ -1,55 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.basic - -import androidx.compose.runtime.Composable -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.MeasureResult -import net.kernelpanicsoft.archie.gui.layout.Renderer -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.DebugModifier -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.minecraft.client.gui.GuiGraphics -import net.minecraft.resources.ResourceLocation - -/** - * Renders a sprite or texture region using UV coordinates. - * - * The composable sizes itself to the minimum constraints provided by its parent and blits - * the specified source region from [loc] into the node's bounds. - * - * @param loc Resource location of the texture or atlas sprite. - * @param uOffset Horizontal UV start offset within the source image (in texture pixels). - * @param vOffset Vertical UV start offset within the source image (in texture pixels). - * @param u Width of the source region in texture pixels. - * @param v Height of the source region in texture pixels. - * @param textureWidth Total width of the source image in pixels. - * @param textureHeight Total height of the source image in pixels. - * @param modifier Additional modifiers applied to the layout node. - */ -@Composable -fun Texture( - loc: ResourceLocation, - uOffset: Float, - vOffset: Float, - u: Int, - v: Int, - textureWidth: Int, - textureHeight: Int, - modifier: Modifier = Modifier, -) { - Layout( - name = "Texture", - measurePolicy = { _, _, constraints -> - MeasureResult(constraints.minWidth, constraints.minHeight) {} - }, - renderer = object : Renderer { - override fun render( - node: UINode, x: Int, y: Int, - guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, - ) = guiGraphics { - blit(loc, x, y, node.width, node.height, uOffset, vOffset, u, v, textureWidth, textureHeight) - } - }, - modifier = Modifier.then(DebugModifier(strs = listOf(loc.toString()))).then(modifier), - ) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Collapsible.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Collapsible.kt deleted file mode 100644 index 721d8d5ff..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Collapsible.kt +++ /dev/null @@ -1,166 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.containers - -import androidx.compose.runtime.* -import com.mojang.math.Axis -import net.kernelpanicsoft.archie.gui.animation.AnimationSpec -import net.kernelpanicsoft.archie.gui.animation.Easings -import net.kernelpanicsoft.archie.gui.animation.animateFloat -import net.kernelpanicsoft.archie.gui.composables.basic.Spacer -import net.kernelpanicsoft.archie.gui.composables.basic.Text -import net.kernelpanicsoft.archie.gui.layout.* -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.appearance.BackgroundModifier -import net.kernelpanicsoft.archie.gui.modifiers.fillMaxHeight -import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType -import net.kernelpanicsoft.archie.gui.modifiers.input.onPointerEvent -import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier -import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingValues -import net.kernelpanicsoft.archie.gui.modifiers.width -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.util.KColor -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.kernelpanicsoft.archie.gui.util.extension.pose -import net.kernelpanicsoft.archie.util.minecraftClient -import net.minecraft.client.Minecraft -import net.minecraft.client.gui.GuiGraphics -import net.minecraft.network.chat.Component -import net.minecraft.network.chat.Style -import kotlin.math.roundToInt -import kotlin.time.Duration.Companion.milliseconds - -private const val COLLAPSIBLE_VISIBILITY_EPSILON = 0.01f - -/** - * A container that can be expanded or collapsed by clicking its header. - * - * The header displays [title] with an animated arrow indicator that rotates 90° when the - * section is open. A vertical separator bar is shown to the left of the expanded content. - * - * ### Example - * ```kotlin - * Collapsible(title = Component.literal("Advanced Settings")) { - * // content shown when expanded - * Text(Component.literal("Option A")) - * } - * ``` - * - * @param title The text displayed in the collapsible header. - * @param modifier Modifiers applied to the outer [Column] container. - * @param initiallyExpanded Whether the section starts in the expanded state. - * @param onToggled Called when the expanded state changes; receives the new state. - * @param content The composable content shown when expanded. - */ -@Composable -fun Collapsible( - title: Component, - modifier: Modifier = Modifier, - initiallyExpanded: Boolean = false, - onToggled: (isExpanded: Boolean) -> Unit = {}, - content: @Composable () -> Unit, -) { - var expanded by remember { mutableStateOf(initiallyExpanded) } - val expandProgress = animateFloat( - targetValue = if (expanded) 1f else 0f, - spec = AnimationSpec(durationMillis = 220.milliseconds, easing = Easings.OutCubic), - ) - - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(4)) { - Row( - modifier = Modifier.onPointerEvent(PointerEventType.PRESS) { _, e -> - expanded = !expanded - onToggled(expanded) - e.consume() - }, - verticalAlignment = Alignment.CenterVertically, - horizontalArrangement = Arrangement.spacedBy(5), - ) { - CollapsibleArrow(isExpanded = expanded) - Text(text = title.copy().withStyle(Style.EMPTY.withUnderlined(true))) - } - - if (expanded || expandProgress > COLLAPSIBLE_VISIBILITY_EPSILON) { - Layout( - name = "CollapsibleContent", - measurePolicy = { _, measurables, constraints -> - if (measurables.isEmpty()) return@Layout MeasureResult(0, 0) {} - - val placeable = measurables.first().measure( - constraints.copy(minHeight = 0, maxHeight = Int.MAX_VALUE), - ) - val visibleHeight = (placeable.height * expandProgress) - .roundToInt() - .coerceAtLeast(0) - - MeasureResult(placeable.width, visibleHeight) { - placeable.placeAt(0, 0) - } - }, - renderer = object : Renderer { - override fun render( - node: UINode, - x: Int, - y: Int, - guiGraphics: GuiGraphics, - mouseX: Int, - mouseY: Int, - partialTick: Float, - ) = guiGraphics { - enableScissor(x, y, x + node.width, y + node.height) - } - - override fun renderAfterChildren( - node: UINode, - x: Int, - y: Int, - guiGraphics: GuiGraphics, - mouseX: Int, - mouseY: Int, - partialTick: Float, - ) = guiGraphics { - disableScissor() - } - }, - ) { - Row(horizontalArrangement = Arrangement.spacedBy(5)) { - Spacer( - modifier = Modifier - .then(PaddingModifier(PaddingValues(left = 5))) - .width(1) - .fillMaxHeight() - .then(BackgroundModifier(KColor.GRAY.argb, KColor.GRAY.argb)) - ) - Box(modifier = Modifier.then(PaddingModifier(PaddingValues(left = 5)))) { - content() - } - } - } - } - } -} - -/** Animated arrow icon that rotates when the collapsible section opens or closes. */ -@Composable -private fun CollapsibleArrow(isExpanded: Boolean) { - val rotation = animateFloat( - targetValue = if (isExpanded) 90f else 0f, - spec = AnimationSpec(durationMillis = 260.milliseconds, easing = Easings.OutBack), - ) - - Layout( - name = "CollapsibleArrow", - measurePolicy = { _, _, _ -> MeasureResult(8, 8) {} }, - renderer = object : Renderer { - override fun render( - node: UINode, x: Int, y: Int, - guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, - ) = guiGraphics { - pose { - translate(x + node.width / 2f, y + node.height / 2f, 0f) - mulPose(Axis.ZP.rotationDegrees(rotation)) - translate(-(x + node.width / 2f), -(y + node.height / 2f), 0f) - drawString(minecraftClient.font, ">", x + 1, y, KColor.WHITE.argb) - } - } - }, - ) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/ContainerPanel.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/ContainerPanel.kt deleted file mode 100644 index a720431f9..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/ContainerPanel.kt +++ /dev/null @@ -1,102 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.containers - -import androidx.compose.runtime.Composable -import net.kernelpanicsoft.archie.gui.LocalContainerScreen -import net.kernelpanicsoft.archie.gui.PlayerSlots -import net.kernelpanicsoft.archie.gui.layout.Box -import net.kernelpanicsoft.archie.gui.layout.Column -import net.kernelpanicsoft.archie.gui.layout.offset -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.onGloballyPositioned -import net.kernelpanicsoft.archie.gui.modifiers.position.offset -import net.kernelpanicsoft.archie.gui.modifiers.position.padding -import net.kernelpanicsoft.archie.gui.modifiers.width -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen - -private const val DEFAULT_CONTENT_WIDTH = 9 * 18 - -/** - * A complete container screen layout following the vanilla chest-screen pattern. - * - * Combines the screen contents (top section) with the player inventory (bottom section), - * properly positioned and spaced. Automatically sets the game's label positions - * ([AbstractContainerScreen.titleLabelX]/[AbstractContainerScreen.titleLabelY] and [AbstractContainerScreen.inventoryLabelX]/[AbstractContainerScreen.inventoryLabelY]) based on the layout. - * - * The game then renders the labels using the title and inventory label components. - * - * ### Layout Structure - * ``` - * ┌─────────────────────────────────┐ - * │ [Screen Contents] │ <- titleLabelX/Y set here - * ├─────────────────────────────────┤ <- contentSpacing - * │ [Player Inventory 3×9] │ <- inventoryLabelX/Y set here - * │ [spacing] │ - * │ [Hotbar 1×9] │ - * └─────────────────────────────────┘ - * ``` - * - * @param contentWidth Width of the panel's content area, in pixels. Defaults to 9 slots wide. - * @param modifier Additional modifiers applied to the outer container. - * @param content The screen contents composable (container inventory, custom widgets, etc.). - */ -@Composable -fun ContainerPanel( - contentWidth: Int = DEFAULT_CONTENT_WIDTH, - modifier: Modifier = Modifier, - content: @Composable () -> Unit, -) { - val screen = LocalContainerScreen.current - - Panel(contentWidth = contentWidth, modifier = modifier) { - Column { - // Screen contents (container inventory) - Box( - modifier = Modifier - .padding(top = 10) - .onGloballyPositioned { coords -> - screen.titleLabelPos = coords - } - ) { - content() - } - // Player inventory section - Box( - modifier = Modifier - .padding(top = 14) - .onGloballyPositioned { coords -> - screen.inventoryLabelPos = coords + offset(x = 1, y = 3) - } - ) { - // Player inventory slots (3×9 main inventory + 1×9 hotbar) - PlayerSlots() - } - } - } -} - -/** - * A [ContainerPanel] whose contents are switched between tabs, following the same - * container/player-inventory layout as [ContainerPanel]. - * - * @param contentWidth Width of the panel's content area, in pixels. Defaults to 9 slots wide. - * @param modifier Additional modifiers applied to the outer [TabPanel]. - * @param builder Declares the tabs; see [TabContainerScope]. - */ -@Composable -fun TabContainerPanel( - contentWidth: Int = DEFAULT_CONTENT_WIDTH, - modifier: Modifier = Modifier, - builder: TabContainerScope.() -> Unit -) { - TabPanel( - modifier = modifier.width(contentWidth + 16), - contentWrapper = { content -> - ContainerPanel( - contentWidth = contentWidth, - modifier = Modifier.offset(y = -12), - content = content, - ) - }, - builder = builder - ) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Panel.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Panel.kt deleted file mode 100644 index 18e169d39..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Panel.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.containers - -import androidx.compose.runtime.Composable -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.layout.Box -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.position.padding -import net.kernelpanicsoft.archie.gui.modifiers.width -import net.kernelpanicsoft.archie.gui.theme.ThemeVariants - -/** - * A padded themed [Surface] used as a general-purpose container for grouped UI content. - * - * @param contentAlignment Alignment of [content] within the panel. - * @param contentWidth When non-null, the panel's inner content area is fixed to this width - * (in pixels); the panel itself is sized to fit that plus [contentPadding] on both sides. - * @param texture The themed texture/style key drawn as the panel's background. See [Surface]. - * @param variant The theme variant of [texture] to use. See [ThemeVariants]. - * @param contentPadding Padding (in pixels) inserted between the panel edge and [content]. - */ -@Composable -fun Panel( - modifier: Modifier = Modifier, - contentAlignment: Alignment = Alignment.TopStart, - contentWidth: Int? = null, - texture: String = "surface", - variant: String = ThemeVariants.DEFAULT, - contentPadding: Int = 8, - content: @Composable () -> Unit, -) { - val resolvedModifier = if (contentWidth != null) modifier.width(contentWidth + contentPadding*2) else modifier - Surface( - modifier = resolvedModifier, - texture = texture, - variant = variant, - contentAlignment = contentAlignment, - ) { - Box(modifier = Modifier.padding(contentPadding), contentAlignment = contentAlignment) { - content() - } - } -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/RootContainer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/RootContainer.kt deleted file mode 100644 index ec29b1db3..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/RootContainer.kt +++ /dev/null @@ -1,36 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.containers - -import androidx.compose.runtime.Composable -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.layout.Box -import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy -import net.kernelpanicsoft.archie.gui.layout.EmptyRenderer -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.fillMaxSize - -/** - * The top-level layout node for a screen's content, centered within the full screen bounds. - * - * [ComposeScreen][net.kernelpanicsoft.archie.gui.ComposeScreen] and - * [ComposeContainerScreen][net.kernelpanicsoft.archie.gui.ComposeContainerScreen] wrap their - * `start` content in this composable so [content] is measured/placed like a [Box] (children - * stacked and top-start-aligned by default) while the whole subtree stays centered on screen. - * - * @param modifier Additional modifiers applied to the content layout node. - */ -@Composable -fun RootContainer( - modifier: Modifier = Modifier, - content: @Composable () -> Unit -) { - Box(contentAlignment = Alignment.Center, modifier = Modifier.fillMaxSize()) { - Layout( - name = "RootContainer", - measurePolicy = BoxMeasurePolicy(Alignment.TopStart), - renderer = EmptyRenderer, - modifier = modifier, - content = content - ) - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Scrollable.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Scrollable.kt deleted file mode 100644 index a343d5367..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Scrollable.kt +++ /dev/null @@ -1,267 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.containers - -import androidx.compose.runtime.* -import net.kernelpanicsoft.archie.gui.LocalSlotClipBounds -import net.kernelpanicsoft.archie.gui.SlotClipSource -import net.kernelpanicsoft.archie.gui.layout.* -import net.kernelpanicsoft.archie.gui.modifiers.Constraints -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.onGloballyPositioned -import net.kernelpanicsoft.archie.gui.modifiers.onSizeChanged -import net.kernelpanicsoft.archie.gui.modifiers.input.* -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.util.KColor -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.minecraft.client.gui.GuiGraphics -import net.minecraft.util.Mth -import org.lwjgl.glfw.GLFW -import kotlin.math.abs -import kotlin.math.max -import kotlin.math.roundToInt - -private const val SCROLLBAR_THICKNESS = 4 -private const val SCROLL_SENSITIVITY = 15.0 -private const val SCROLLBAR_FADE_DURATION_MS = 1000L -private const val MIN_SCROLLBAR_THUMB_SIZE = 10 -private const val SCROLL_SNAP_EPSILON = 0.1 - -/** - * The axis along which a [Scrollable] container scrolls its content. - */ -enum class ScrollDirection { - VERTICAL, HORIZONTAL; - - /** Returns [horizontal] or [vertical] depending on the direction. */ - fun choose(horizontal: Double, vertical: Double): Double = - if (this == VERTICAL) vertical else horizontal -} - -/** - * Mutable state holder for a [Scrollable] composable. - * - * Create and remember an instance via [rememberScrollableState] and pass it to [Scrollable] - * when you need programmatic control over the scroll position. - */ -@Stable -class ScrollableState { - /** Current target scroll offset in pixels. Animate towards [currentScrollPosition]. */ - var scrollOffset by mutableStateOf(0.0) - /** Smoothly interpolated scroll position used for actual rendering. */ - var currentScrollPosition by mutableStateOf(0.0) - /** Maximum scroll offset (content size − container size). */ - var maxScroll by mutableStateOf(0) - /** Size of the scrollable content in the scroll axis, in pixels. */ - var childSize by mutableStateOf(0) - /** Size of the visible container in the scroll axis, in pixels. */ - var containerSize by mutableStateOf(0) - /** Whether the user is currently dragging the scrollbar thumb. */ - var isDraggingScrollbar by mutableStateOf(false) - /** Timestamp of the last user interaction (used for fade-out animation). */ - var lastInteractTime by mutableStateOf(0L) - - /** Records an interaction so the scrollbar fade-out timer resets. */ - fun onInteraction() { lastInteractTime = System.currentTimeMillis() } - - /** - * Scrolls by [delta] pixels, clamping the result to the valid range. - * - * @param delta Positive values scroll forward (down/right); negative scrolls back. - */ - fun scrollBy(delta: Double) { - scrollOffset = (scrollOffset + delta).coerceIn(0.0, maxScroll.toDouble()) - onInteraction() - } -} - -/** - * Creates and remembers a [ScrollableState] for use with [Scrollable]. - */ -@Composable -fun rememberScrollableState(): ScrollableState = remember { ScrollableState() } - -/** - * A container that allows its single child to be scrolled when the child's content - * exceeds the container's bounds. - * - * A fade-in/out scrollbar thumb is rendered automatically when content overflows. The - * scrollbar supports mouse-drag interaction and responds to the keyboard arrow keys, - * Page Up/Down. - * - * ### Example - * ```kotlin - * Scrollable(modifier = Modifier.size(200, 100)) { - * Column { - * repeat(20) { Text(Component.literal("Item $it")) } - * } - * } - * ``` - * - * @param direction The [ScrollDirection] (vertical or horizontal). - * @param scrollbarColor The fill colour of the scrollbar thumb. - * @param modifier Modifiers applied to the Scrollable layout node. - * @param state External [ScrollableState]; defaults to a locally remembered instance. - * @param content The single scrollable child composable. - */ -@Composable -fun Scrollable( - direction: ScrollDirection = ScrollDirection.VERTICAL, - scrollbarColor: KColor = KColor.DARK_GRAY, - modifier: Modifier = Modifier, - state: ScrollableState = rememberScrollableState(), - content: @Composable () -> Unit, -) { - val clipSource = remember { SlotClipSource() } - - val measurePolicy = remember(direction) { - object : MeasurePolicy { - override fun measure( - scope: MeasureScope, - measurables: List, - constraints: Constraints, - ): MeasureResult { - if (measurables.isEmpty()) return MeasureResult(constraints.minWidth, constraints.minHeight) {} - - val contentConstraints = if (direction == ScrollDirection.VERTICAL) - constraints.copy(minHeight = 0, maxHeight = Int.MAX_VALUE) - else - constraints.copy(minWidth = 0, maxWidth = Int.MAX_VALUE) - - val placeable = measurables.first().measure(contentConstraints) - - val resolvedWidth = if (direction == ScrollDirection.HORIZONTAL) { - resolveScrollableViewportAxis(placeable.width, constraints.minWidth, constraints.maxWidth) - } else { - resolveScrollableContentAxis(placeable.width, constraints.minWidth, constraints.maxWidth) - } - - val resolvedHeight = if (direction == ScrollDirection.VERTICAL) { - resolveScrollableViewportAxis(placeable.height, constraints.minHeight, constraints.maxHeight) - } else { - resolveScrollableContentAxis(placeable.height, constraints.minHeight, constraints.maxHeight) - } - - state.childSize = direction.choose(placeable.width.toDouble(), placeable.height.toDouble()).toInt() - state.containerSize = direction.choose(resolvedWidth.toDouble(), resolvedHeight.toDouble()).toInt() - state.maxScroll = max(0, state.childSize - state.containerSize) - state.scrollOffset = state.scrollOffset.coerceIn(0.0, state.maxScroll.toDouble()) - - return MeasureResult(resolvedWidth, resolvedHeight) { - val scrollPos = state.currentScrollPosition.roundToInt() - if (direction == ScrollDirection.VERTICAL) placeable.placeAt(0, -scrollPos) - else placeable.placeAt(-scrollPos, 0) - } - } - } - } - - CompositionLocalProvider(LocalSlotClipBounds provides clipSource) { - Layout( - name = "Scrollable", - measurePolicy = measurePolicy, - renderer = object : Renderer { - override fun render(node: UINode, x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) = guiGraphics { - enableScissor(x, y, x + node.width, y + node.height) - } - - override fun renderAfterChildren(node: UINode, x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) = guiGraphics { - val lerpFactor = (0.4f * partialTick).coerceIn(0.05f, 1f).toDouble() - val next = state.currentScrollPosition + (state.scrollOffset - state.currentScrollPosition) * lerpFactor - state.currentScrollPosition = if (abs(state.scrollOffset - next) <= SCROLL_SNAP_EPSILON) state.scrollOffset else next - - if (state.maxScroll > 0) { - val timeSinceInteract = System.currentTimeMillis() - state.lastInteractTime - if (!(timeSinceInteract > SCROLLBAR_FADE_DURATION_MS && !state.isDraggingScrollbar)) { - val fadeAlpha = if (state.isDraggingScrollbar) 1f else 1f - (timeSinceInteract.toFloat() / SCROLLBAR_FADE_DURATION_MS) - val alpha = Mth.clamp((fadeAlpha * scrollbarColor.alpha).toInt(), 0, 255) - if (alpha > 0) { - val colorWithAlpha = scrollbarColor.rgb or (alpha shl 24) - val trackSize = state.containerSize - val thumbSize = max(MIN_SCROLLBAR_THUMB_SIZE, (trackSize.toFloat() / state.childSize * trackSize).toInt()) - val scrollPct = if (state.maxScroll > 0) state.currentScrollPosition / state.maxScroll else 0.0 - val thumbPos = scrollPct * (trackSize - thumbSize) - - if (direction == ScrollDirection.VERTICAL) { - val tx = x + node.width - SCROLLBAR_THICKNESS - val ty = y + thumbPos.roundToInt() - fill(tx, ty, tx + SCROLLBAR_THICKNESS, ty + thumbSize, colorWithAlpha) - } else { - val tx = x + thumbPos.roundToInt() - val ty = y + node.height - SCROLLBAR_THICKNESS - fill(tx, ty, tx + thumbSize, ty + SCROLLBAR_THICKNESS, colorWithAlpha) - } - } - } - } - disableScissor() - } - }, - modifier = modifier - .onGloballyPositioned { coords -> - clipSource.updateOrigin(coords) - } - .onSizeChanged { size -> - clipSource.updateSize(size) - } - .onScroll { _, event -> - val rawDelta = if (direction == ScrollDirection.HORIZONTAL && event.scrollX != 0.0) { - -event.scrollX - } else { - -event.scrollY - } - state.scrollBy(rawDelta * SCROLL_SENSITIVITY) - event.consume() - } - .onPointerEvent(PointerEventType.PRESS) { node, event -> - val minX = if (direction == ScrollDirection.VERTICAL) node.x + node.width - SCROLLBAR_THICKNESS else node.x - val minY = if (direction == ScrollDirection.VERTICAL) node.y else node.y + node.height - SCROLLBAR_THICKNESS - val maxX = node.x + node.width - val maxY = node.y + node.height - - if (event.mouseX >= minX && event.mouseX <= maxX && event.mouseY >= minY && event.mouseY <= maxY) { - state.isDraggingScrollbar = true - state.onInteraction() - event.consume() - } - } - .onPointerEvent(PointerEventType.GLOBAL_RELEASE) { _, _ -> state.isDraggingScrollbar = false } - .onDrag { _, event -> - if (!state.isDraggingScrollbar) return@onDrag - val pixelDelta = direction.choose(event.dragX, event.dragY) - val trackSize = state.containerSize - val thumbSize = max(MIN_SCROLLBAR_THUMB_SIZE, (trackSize.toFloat() / state.childSize * trackSize).toInt()) - if (trackSize > thumbSize) { - state.scrollBy(pixelDelta * (state.maxScroll.toFloat() / (trackSize - thumbSize))) - // Keep drag feedback immediate while preserving smoothing for wheel/key input. - state.currentScrollPosition = state.scrollOffset - } - event.consume() - } - .onKeyEvent { _, event -> - val amount = state.containerSize * 0.8 - when (event.keyCode) { - GLFW.GLFW_KEY_DOWN -> if (direction == ScrollDirection.VERTICAL) state.scrollBy(SCROLL_SENSITIVITY) - GLFW.GLFW_KEY_UP -> if (direction == ScrollDirection.VERTICAL) state.scrollBy(-SCROLL_SENSITIVITY) - GLFW.GLFW_KEY_RIGHT -> if (direction == ScrollDirection.HORIZONTAL) state.scrollBy(SCROLL_SENSITIVITY) - GLFW.GLFW_KEY_LEFT -> if (direction == ScrollDirection.HORIZONTAL) state.scrollBy(-SCROLL_SENSITIVITY) - GLFW.GLFW_KEY_PAGE_DOWN -> state.scrollBy(amount) - GLFW.GLFW_KEY_PAGE_UP -> state.scrollBy(-amount) - else -> return@onKeyEvent - } - event.consume() - }, - content = content, - ) - } -} - -internal fun resolveScrollableViewportAxis(childSize: Int, min: Int, max: Int): Int { - // For the scrolling axis, fill the available finite viewport so overflow can scroll. - if (max == Int.MAX_VALUE) return childSize.coerceAtLeast(min) - return max.coerceAtLeast(min) -} - -internal fun resolveScrollableContentAxis(childSize: Int, min: Int, max: Int): Int { - if (max == Int.MAX_VALUE) return childSize.coerceAtLeast(min) - return childSize.coerceIn(min, max) -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Surface.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Surface.kt deleted file mode 100644 index 44ceb8f7e..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Surface.kt +++ /dev/null @@ -1,77 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.containers - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.Renderer -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.debug -import net.kernelpanicsoft.archie.gui.modifiers.sizeIn -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.theme.LocalTheme -import net.kernelpanicsoft.archie.gui.theme.SimpleThemeState -import net.kernelpanicsoft.archie.gui.theme.ThemeVariants -import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.minecraft.client.gui.GuiGraphics - - -/** - * A [net.kernelpanicsoft.archie.gui.layout.Box]-like layout node that paints a themed background texture behind its children. - * - * The texture is resolved from the current [LocalTheme] by [texture] key and [variant], and - * is drawn nine-sliced if the theme defines it as such, otherwise stretched to fit like a - * simple sprite (in which case the surface has a minimum size matching the sprite's own). - * [Panel] builds on top of this to add content padding. - * - * @param contentAlignment Alignment of [content] within the surface, as in [net.kernelpanicsoft.archie.gui.layout.Box]. - * @param modifier Additional modifiers applied to the layout node. - * @param texture The themed texture key to look up via [LocalTheme]. - * @param variant The theme variant of [texture] to use. See [ThemeVariants]. - */ -@Composable -fun Surface( - contentAlignment: Alignment = Alignment.TopStart, - modifier: Modifier = Modifier, - texture: String = "surface", - variant: String = ThemeVariants.DEFAULT, - content: @Composable () -> Unit -) { - val measurePolicy = remember(contentAlignment) { BoxMeasurePolicy(contentAlignment) } - val theme = LocalTheme.current - val composableTheme = theme.getComposableTheme(texture) - val state = composableTheme.getState(TextureStates.DEFAULT, variant) - - Layout( - name = "Surface", - measurePolicy = measurePolicy, - renderer = object : Renderer - { - override fun render( - node: UINode, - x: Int, - y: Int, - guiGraphics: GuiGraphics, - mouseX: Int, - mouseY: Int, - partialTick: Float - ) = guiGraphics { - drawThemeState(state, x, y, node.width, node.height) - } - }, - modifier = Modifier.debug(state.texture.toString()).apply { - if (!composableTheme.isNineslice) { - with(composableTheme.states[TextureStates.DEFAULT] as SimpleThemeState) { - sizeIn( - minWidth = width, - minHeight = height - ) - } - } - } then modifier, - content = content - ) -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/TabContainer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/TabContainer.kt deleted file mode 100644 index f9ff390f7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/TabContainer.kt +++ /dev/null @@ -1,410 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.containers - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Stable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import net.kernelpanicsoft.archie.gui.composables.basic.Text -import net.kernelpanicsoft.archie.gui.composables.basic.Texture -import net.kernelpanicsoft.archie.gui.composables.input.ButtonCore -import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates -import net.kernelpanicsoft.archie.gui.composables.theme.WidgetState -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.layout.Arrangement -import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy -import net.kernelpanicsoft.archie.gui.layout.Column -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.Renderer -import net.kernelpanicsoft.archie.gui.layout.Row -import net.kernelpanicsoft.archie.gui.layout.dp -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.sizeIn -import net.kernelpanicsoft.archie.gui.modifiers.position.padding -import net.kernelpanicsoft.archie.gui.modifiers.position.offset -import net.kernelpanicsoft.archie.gui.modifiers.position.zIndex -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.theme.LocalTheme -import net.kernelpanicsoft.archie.gui.theme.SimpleThemeState -import net.kernelpanicsoft.archie.gui.theme.ThemeVariants -import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.minecraft.network.chat.Component -import net.minecraft.resources.ResourceLocation -import net.minecraft.client.gui.GuiGraphics - -/** Built-in themed texture keys for [Tab]/[TabContainer], matching vanilla tab styles. */ -object TabTextures -{ - /** The in-game pause-menu tab style (e.g. Create World screen). */ - const val GAME = "tab_game" - /** The main-menu tab style. */ - const val MENU = "tab_menu" -} - -private const val SELECTED_ELEVATION_PX = 2 -private const val DEFAULT_ICON_SPACING = 4 -private const val DEFAULT_CONTENT_SPACING = 6 -private const val DEFAULT_CONTENT_WIDTH = 9 * 18 - -/** - * Declarative tab bar modeled after the vanilla Create World screen tabs. - * - * @param tabs Ordered list of tab specs to render. - * @param state External state holder controlling the selected tab. - * @param modifier Modifier applied to the outer container (or scrollable wrapper). - * @param onTabSelected Callback invoked after a tab becomes selected. - * @param tabSpacing Horizontal spacing between neighboring tabs, in pixels. - * @param scrollable When true, wraps the tab row in a horizontal [Scrollable] viewport. - * @param scrollState Optional externally managed [ScrollableState] (only used when [scrollable]). - * @param contentSpacing Vertical spacing between the tab row and the selected tab content, in pixels. - */ -@Composable -fun TabContainer( - tabs: List, - state: TabContainerState = rememberTabContainerState(tabs), - modifier: Modifier = Modifier, - onTabSelected: (TabSpec) -> Unit = {}, - tabSpacing: Int = 2, - scrollable: Boolean = true, - scrollState: ScrollableState? = null, - contentSpacing: Int = DEFAULT_CONTENT_SPACING, - elevateSelected: Boolean = false, - tabTexture: String = TabTextures.GAME, -) { - state.ensureSelection(tabs) - - if (tabs.isEmpty()) { - if (scrollable) { - Scrollable( - direction = ScrollDirection.HORIZONTAL, - modifier = modifier, - state = scrollState ?: rememberScrollableState(), - ) {} - } - return - } - - val rowContent: @Composable (Modifier) -> Unit = { rowModifier -> - Row( - modifier = rowModifier, - horizontalArrangement = Arrangement.spacedBy(tabSpacing.dp), - verticalAlignment = Alignment.Bottom, - ) { - tabs.forEach { tab -> - Tab( - spec = tab, - selected = state.isSelected(tab.id), - texture = tabTexture, - elevateSelected = elevateSelected, - onClick = { - if (!tab.enabled) return@Tab - state.select(tab.id) - onTabSelected(tab) - }, - ) - } - } - } - - Column( - modifier = modifier, - verticalArrangement = Arrangement.spacedBy(contentSpacing.dp), - ) { - if (scrollable) { - Scrollable( - direction = ScrollDirection.HORIZONTAL, - state = scrollState ?: rememberScrollableState(), - ) { - rowContent(Modifier.padding(horizontal = 4, vertical = 2)) - } - } else { - rowContent(Modifier) - } - - state.selectedTab(tabs)?.content?.let { content -> - content() - } - } -} - -/** - * DSL overload allowing tabs to be declared inline via [TabContainerScope.tab] without - * manually building a [TabSpec] list. - * - * @param contentWrapper Wraps each tab's content composable, e.g. to add common padding. - * Defaults to rendering the content unwrapped. - * @param builder Declares the tabs, in order, via [TabContainerScope.tab]. - */ -@Composable -fun TabContainer( - modifier: Modifier = Modifier, - state: TabContainerState? = null, - onTabSelected: (TabSpec) -> Unit = {}, - tabSpacing: Int = 2, - scrollable: Boolean = true, - scrollState: ScrollableState? = null, - contentSpacing: Int = DEFAULT_CONTENT_SPACING, - contentWrapper: @Composable ((@Composable (() -> Unit)) -> Unit)? = null, - tabTexture: String = TabTextures.GAME, - builder: TabContainerScope.() -> Unit, -) { - val scope = remember { TabContainerScope(contentWrapper = contentWrapper ?: { content -> content()}) } - scope.reset() - scope.builder() - val tabs = scope.build() - val resolvedState = state ?: rememberTabContainerState(tabs) - - TabContainer( - tabs = tabs, - state = resolvedState, - modifier = modifier, - onTabSelected = onTabSelected, - tabSpacing = tabSpacing, - scrollable = scrollable, - scrollState = scrollState, - contentSpacing = contentSpacing, - tabTexture = tabTexture, - ) -} - -/** - * A [TabContainer] whose selected tab content is wrapped in a [Panel] by default, elevated - * (drawn above neighboring tabs) when selected. Used by [TabContainerPanel]. - * - * @param contentWrapper Wraps each tab's content; defaults to a [Panel] offset to sit flush - * under the tab row. - * @param builder Declares the tabs, in order, via [TabContainerScope.tab]. - */ -@Composable -fun TabPanel( - modifier: Modifier = Modifier, - state: TabContainerState? = null, - onTabSelected: (TabSpec) -> Unit = {}, - tabSpacing: Int = 2, - scrollable: Boolean = true, - scrollState: ScrollableState? = null, - contentWrapper: @Composable ((@Composable (() -> Unit)) -> Unit)? = null, - builder: TabContainerScope.() -> Unit, -) { - val scope = remember { TabContainerScope(contentWrapper = contentWrapper ?: { content -> - Panel(modifier = Modifier.offset(y = -12)) { - content() - } - }) } - scope.reset() - scope.builder() - val tabs = scope.build() - val resolvedState = state ?: rememberTabContainerState(tabs) - - TabContainer( - tabs = tabs, - state = resolvedState, - modifier = modifier, - onTabSelected = onTabSelected, - tabSpacing = tabSpacing, - scrollable = scrollable, - scrollState = scrollState, - tabTexture = TabTextures.GAME, - elevateSelected = true, - ) -} - -/** Restricts the [TabContainerScope.tab] DSL to its own receiver scope. */ -@DslMarker -annotation class TabContainerDsl - -/** Receiver scope for the [TabContainer]/[TabPanel] DSL `builder` lambda. */ -@TabContainerDsl -class TabContainerScope internal constructor(val contentWrapper: @Composable (@Composable () -> Unit) -> Unit = {it()}) { - private val specs = mutableListOf() - - /** Declares a tab with the given [id], [title], and [content]. */ - fun tab( - id: String, - title: Component, - icon: TabIcon? = null, - enabled: Boolean = true, - content: @Composable (() -> Unit), - ) { - specs += TabSpec( - id = id, - title = title, - icon = icon, - enabled = enabled, - content = { contentWrapper(content) }, - ) - } - - /** Declares a tab from a pre-built [TabSpec], bypassing [contentWrapper]. */ - fun tab(spec: TabSpec) { - specs += spec - } - - internal fun reset() = specs.clear() - - internal fun build(): List = specs.toList() -} - -/** Data describing a single Create World style tab. */ -data class TabSpec( - val id: String, - val title: Component, - val icon: TabIcon? = null, - val enabled: Boolean = true, - val content: @Composable (() -> Unit), -) - -/** Sprite descriptor used for optional tab icons. */ -data class TabIcon( - val texture: ResourceLocation, - val uOffset: Float = 0f, - val vOffset: Float = 0f, - val regionWidth: Int = 28, - val regionHeight: Int = 32, - val textureWidth: Int = 256, - val textureHeight: Int = 256, - val displayWidth: Int = regionWidth, - val displayHeight: Int = regionHeight, -) - -/** - * Tracks which tab id is selected in a [TabContainer]. Create via [rememberTabContainerState]. - */ -@Stable -class TabContainerState internal constructor(initialSelectedId: String?) { - private var selectedId by mutableStateOf(initialSelectedId) - - /** The currently selected tab's id, or `null` if nothing is selected yet. */ - val selectedTabId: String? get() = selectedId - - /** Whether [tabId] is the currently selected tab. */ - fun isSelected(tabId: String): Boolean = selectedId == tabId - - /** Selects the tab with the given [tabId]. */ - fun select(tabId: String) { - selectedId = tabId - } - - /** - * Ensures the selection is valid for [tabs]: falls back to the first enabled tab (or the - * very first tab, if none are enabled) when there's no selection or the selected id no - * longer exists/is disabled among [tabs]. - */ - fun ensureSelection(tabs: List) { - if (tabs.isEmpty()) { - selectedId = null - return - } - val current = selectedId - val active = current?.let { id -> tabs.firstOrNull { it.id == id && it.enabled } } - if (active != null) return - selectedId = tabs.firstOrNull { it.enabled }?.id ?: tabs.first().id - } - - /** The index of the selected tab within [tabs], or -1 if none is selected. */ - fun selectedIndex(tabs: List): Int = - tabs.indexOfFirst { it.id == selectedId } - - /** The [TabSpec] currently selected within [tabs], or `null` if none is selected. */ - fun selectedTab(tabs: List): TabSpec? = - tabs.firstOrNull { it.id == selectedId } -} - -/** Creates and remembers a [TabContainerState], initially selecting [initialSelectedId]. */ -@Composable -fun rememberTabContainerState( - tabs: List, - initialSelectedId: String? = tabs.firstOrNull { it.enabled }?.id, -): TabContainerState = remember(initialSelectedId) { TabContainerState(initialSelectedId) } - -/** - * A single clickable tab button, rendering [spec]'s icon/title and switching its themed - * texture state based on [selected]/hover/press. Used internally by [TabContainer]; use that - * (or the DSL/[TabPanel] variants) rather than calling this directly in most cases. - */ -@Composable -fun Tab( - spec: TabSpec, - selected: Boolean, - modifier: Modifier = Modifier, - elevateSelected: Boolean = false, - enabled: Boolean = spec.enabled, - texture: String = TabTextures.GAME, - variant: String = ThemeVariants.DEFAULT, - iconSpacing: Int = DEFAULT_ICON_SPACING, - onClick: (TabSpec) -> Unit, -) { - val theme = LocalTheme.current - val composableTheme = theme.getComposableTheme(texture) - val measurePolicy = remember { BoxMeasurePolicy(Alignment.CenterStart) } - - ButtonCore( - onClick = { onClick(spec) }, - enabled = enabled, - modifier = modifier, - ) { isHovered, isPressed -> - val stateKey = WidgetState.resolve( - composableTheme, variant, - WidgetState.clicked(selected || isPressed), WidgetState.hovered(isHovered), - enabled = enabled, - ) - val state = composableTheme.getState(stateKey, variant) - val offsetModifier = Modifier - .zIndex(if (selected && elevateSelected) 1f else 0f) - .offset(x = 0, y = if (selected && !elevateSelected) -SELECTED_ELEVATION_PX else 0) - .padding(horizontal = 10, vertical = 6) - val sizeModifier = if (!composableTheme.isNineslice) { - val defaultState = composableTheme.states[TextureStates.DEFAULT] as SimpleThemeState - Modifier.sizeIn(minWidth = defaultState.width, minHeight = defaultState.height) - } else Modifier - - Layout( - name = "Tab", - measurePolicy = measurePolicy, - renderer = object : Renderer { - override fun render( - node: UINode, - x: Int, - y: Int, - guiGraphics: GuiGraphics, - mouseX: Int, - mouseY: Int, - partialTick: Float, - ) = guiGraphics { - node.renderState = stateKey - drawThemeState(state, x, y, node.width, node.height) - } - }, - modifier = sizeModifier.then(offsetModifier), - ) { - Row( - modifier = Modifier, - horizontalArrangement = Arrangement.spacedBy(iconSpacing.dp), - verticalAlignment = Alignment.CenterVertically, - ) { - spec.icon?.let { icon -> - Texture( - loc = icon.texture, - uOffset = icon.uOffset, - vOffset = icon.vOffset, - u = icon.regionWidth, - v = icon.regionHeight, - textureWidth = icon.textureWidth, - textureHeight = icon.textureHeight, - modifier = Modifier.sizeIn( - minWidth = icon.displayWidth, - minHeight = icon.displayHeight, - ), - ) - } - Text( - text = spec.title, - color = if (selected) theme.darkTextColor else theme.lightTextColor, - dropShadow = !selected - ) - } - } - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Button.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Button.kt deleted file mode 100644 index 1a8ec9093..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Button.kt +++ /dev/null @@ -1,145 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.input - -import androidx.compose.runtime.* -import net.kernelpanicsoft.archie.gui.animation.AnimationSpec -import net.kernelpanicsoft.archie.gui.animation.Easings -import net.kernelpanicsoft.archie.gui.animation.animateInt -import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates -import net.kernelpanicsoft.archie.gui.composables.theme.WidgetState -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.Renderer -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.DebugModifier -import net.kernelpanicsoft.archie.gui.modifiers.sizeIn -import net.kernelpanicsoft.archie.gui.modifiers.position.offset -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.theme.LocalTheme -import net.kernelpanicsoft.archie.gui.theme.SimpleThemeState -import net.kernelpanicsoft.archie.gui.theme.ThemeVariants -import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.minecraft.client.gui.GuiGraphics -import kotlin.time.Duration.Companion.milliseconds - -/** - * A standard themed, clickable button. - * - * Renders the themed [texture] state ([TextureStates.DEFAULT]/[TextureStates.HOVERED]/ - * [TextureStates.CLICKED]/[TextureStates.DISABLED]) behind [content], animating a 1px press - * offset while held. For fully custom visuals, use [ButtonCore] directly instead. - * - * @param onClick Invoked with the receiving [UINode] when the button is pressed. - * @param modifier Additional modifiers applied to the outer clickable container. - * @param enabled When `false`, the disabled state is drawn and pointer events are ignored. - * @param texture The themed texture key to look up via [LocalTheme]. - * @param variant The theme variant of [texture] to use. See [ThemeVariants]. - * @param content The button's foreground content (e.g. a [net.kernelpanicsoft.archie.gui.composables.basic.Text]). - */ -@Composable -fun Button( - onClick: (UINode) -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - texture: String = "button", - variant: String = ThemeVariants.DEFAULT, - content: @Composable () -> Unit = {} -) { - val theme = LocalTheme.current - val composableTheme = theme.getComposableTheme(texture) - val measurePolicy = remember { BoxMeasurePolicy(Alignment.Center) } - - ButtonCore( - onClick, - modifier, - enabled - ) { isHovered, isPressed -> - val pressOffset = animateInt( - targetValue = if (isPressed) 1 else 0, - spec = AnimationSpec(durationMillis = 90.milliseconds, easing = Easings.OutCubic), - ) - - Layout( - name = "Button", - content = content, - measurePolicy = measurePolicy, - renderer = object : Renderer - { - override fun render( - node: UINode, - x: Int, - y: Int, - guiGraphics: GuiGraphics, - mouseX: Int, - mouseY: Int, - partialTick: Float - ) = guiGraphics { - val stateKey = WidgetState.resolve( - composableTheme, variant, - WidgetState.clicked(isPressed), WidgetState.hovered(isHovered), - enabled = enabled, - ) - node.renderState = stateKey - val state = composableTheme.getState(stateKey, variant) - - drawThemeState(state, x, y, node.width, node.height) - } - }, - modifier = modifier.apply { - if (!composableTheme.isNineslice) { - with(composableTheme.states[TextureStates.DEFAULT] as SimpleThemeState) { - sizeIn( - minWidth = width, - minHeight = height - ) - } - } - }.offset(x = 0, y = pressOffset) - ) - } -} - - -/** - * A stateless clickable container composable. - * - * `ButtonCore` manages hover and pressed state internally and exposes them to [content] - * via the lambda parameters. It handles cursor changes and the full pointer-event lifecycle, - * but applies no visual styling of its own — that is left entirely to [content]. - * - * Use [ButtonCore] when you need custom button visuals. For a standard themed button, use - * [Button] instead. - * - * ### Example - * ```kotlin - * ButtonCore(onClick = { println("Clicked!") }) { isHovered, isPressed -> - * Box( - * modifier = Modifier.background(if (isHovered) KColor.LIGHT_GRAY else KColor.GRAY) - * .size(80, 20) - * ) { - * Text(Component.literal("Click me")) - * } - * } - * ``` - * - * @param onClick Invoked with the receiving [UINode] when the button is pressed. - * @param modifier Additional modifiers applied to the outer clickable container. - * @param enabled When `false`, pointer events are ignored and no cursor change occurs. - * @param content The button's visual content, receiving `isHovered` and `isPressed` booleans. - */ -@Composable -fun ButtonCore( - onClick: (UINode) -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - content: @Composable (isHovered: Boolean, isPressed: Boolean) -> Unit, -) { - Clickable( - onClick = onClick, - enabled = enabled, - modifier = Modifier.then(DebugModifier(strs = listOf("Enabled: $enabled"))).then(modifier), - ) { isHovered, isPressed -> - content(isHovered, isPressed) - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Checkbox.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Checkbox.kt deleted file mode 100644 index 82d2a01bd..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Checkbox.kt +++ /dev/null @@ -1,129 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.input - -import androidx.compose.runtime.* -import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates -import net.kernelpanicsoft.archie.gui.composables.theme.WidgetState -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.layout.Box -import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.Renderer -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.debug -import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType -import net.kernelpanicsoft.archie.gui.modifiers.input.onPointerEvent -import net.kernelpanicsoft.archie.gui.modifiers.sizeIn -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.theme.LocalTheme -import net.kernelpanicsoft.archie.gui.theme.SimpleThemeState -import net.kernelpanicsoft.archie.gui.theme.ThemeVariants -import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.minecraft.client.gui.GuiGraphics - -/** - * A standard themed checkbox. - * - * Renders the themed [texture] state - a combined checked+hovered state is used when both - * apply and the theme defines it. Built on top of [CheckboxCore]; use that directly for - * fully custom visuals. - * - * @param checked The current checked state. - * @param modifier Additional modifiers applied to the outer container. - * @param texture The themed texture key to look up via [LocalTheme]. - * @param variant The theme variant of [texture] to use. See [ThemeVariants]. - * @param onCheckedChange Called with the new checked value when the user clicks. - */ -@Composable -fun Checkbox( - checked: Boolean = false, - modifier: Modifier = Modifier, - texture: String = "checkbox", - variant: String = ThemeVariants.DEFAULT, - onCheckedChange: (Boolean) -> Unit, -) { - val theme = LocalTheme.current - val composableTheme = theme.getComposableTheme(texture) - val sizeModifier = if (!composableTheme.isNineslice) { - with(composableTheme.states[TextureStates.DEFAULT] as SimpleThemeState) { - Modifier.sizeIn( - minWidth = width, - minHeight = height - ) - } - } else Modifier - - CheckboxCore( - checked, - sizeModifier.then(modifier), - onCheckedChange - ) { isHovered -> - Layout( - name = "Checkbox", - measurePolicy = BoxMeasurePolicy(Alignment.Center), - renderer = object : Renderer - { - override fun render( - node: UINode, - x: Int, - y: Int, - guiGraphics: GuiGraphics, - mouseX: Int, - mouseY: Int, - partialTick: Float - ) = guiGraphics { - val stateKey = WidgetState.resolve( - composableTheme, variant, - WidgetState.clicked(checked), WidgetState.hovered(isHovered), - ) - node.renderState = stateKey - val state = composableTheme.getState(stateKey, variant) - - drawThemeState(state, x, y, node.width, node.height) - } - }, - modifier = sizeModifier - ) - } -} - -/** - * A stateless, unstyled toggle composable. - * - * `CheckboxCore` manages hover state internally and exposes it to [content]. All visual - * styling (textures, colours, checked indicator) is the responsibility of [content]. Use - * this as the base for custom or theme-driven checkbox implementations. - * - * ### Example - * ```kotlin - * var checked by remember { mutableStateOf(false) } - * CheckboxCore(checked = checked, onCheckedChange = { checked = it }) { isHovered -> - * Box(modifier = Modifier.size(16, 16).background(if (checked) KColor.GREEN else KColor.GRAY)) - * } - * ``` - * - * @param checked The current checked state. - * @param modifier Additional modifiers applied to the outer [Box]. - * @param onCheckedChange Called with the new checked value when the user clicks. - * @param content The visual content; receives `isHovered` for styling. - */ -@Composable -fun CheckboxCore( - checked: Boolean = false, - modifier: Modifier = Modifier, - onCheckedChange: (Boolean) -> Unit, - content: @Composable (isHovered: Boolean) -> Unit, -) { - var hovered by remember { mutableStateOf(false) } - - Box( - modifier = Modifier - .debug("Hovered: $hovered") - .onPointerEvent(PointerEventType.ENTER) { _, e -> hovered = true; e.consume() } - .onPointerEvent(PointerEventType.EXIT) { _, e -> hovered = false; e.consume() } - .onPointerEvent(PointerEventType.PRESS) { _, e -> onCheckedChange(!checked); e.consume() } - .then(modifier), - ) { - content(hovered) - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Clickable.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Clickable.kt deleted file mode 100644 index cbd76eb7e..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Clickable.kt +++ /dev/null @@ -1,92 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.input - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.DisposableEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.layout.Box -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType -import net.kernelpanicsoft.archie.gui.modifiers.input.onPointerEvent -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.util.minecraftClient -import net.minecraft.client.Minecraft -import org.lwjgl.glfw.GLFW - -private object CursorCache { - val handCursor: Long by lazy { GLFW.glfwCreateStandardCursor(GLFW.GLFW_HAND_CURSOR) } -} - -private fun setHandCursor(enabled: Boolean) { - // GLFW calls must happen on the render thread; DisposableEffect callbacks run on the - // recomposition dispatcher, so hop over via Minecraft's thread-safe task queue. - minecraftClient.execute { - val window = minecraftClient.window.window - GLFW.glfwSetCursor(window, if (enabled) CursorCache.handCursor else 0L) - } -} - -/** - * Low-level unstyled clickable container used by higher-level inputs like [ButtonCore]. - * - * Tracks hover/press state and fires [onClick] on press (not release), showing the system - * hand cursor on hover when [showHandCursor] is `true`. Applies no visual styling itself - - * that is entirely up to [content]. - * - * @param onClick Invoked with the receiving [UINode] on press. - * @param modifier Additional modifiers applied to the outer [Box]. - * @param enabled When `false`, pointer events are ignored and no cursor change occurs. - * @param showHandCursor Whether to switch to the hand cursor while hovered. - * @param content The visual content; receives `isHovered`/`isPressed` for styling. - */ -@Composable -fun Clickable( - onClick: (UINode) -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - showHandCursor: Boolean = true, - content: @Composable (isHovered: Boolean, isPressed: Boolean) -> Unit, -) { - var hovered by remember { mutableStateOf(false) } - var pressed by remember { mutableStateOf(false) } - - DisposableEffect(enabled, hovered, showHandCursor) { - if ((!enabled || !hovered) && showHandCursor) setHandCursor(false) - onDispose { - if (showHandCursor) setHandCursor(false) - } - } - - Box( - modifier = Modifier - .onPointerEvent(PointerEventType.ENTER) { _, e -> - if (!enabled) return@onPointerEvent - hovered = true - if (showHandCursor) setHandCursor(true) - e.consume() - } - .onPointerEvent(PointerEventType.EXIT) { _, e -> - hovered = false - pressed = false - if (showHandCursor) setHandCursor(false) - if (enabled) e.consume() - } - .onPointerEvent(PointerEventType.PRESS) { node, e -> - if (!enabled) return@onPointerEvent - pressed = true - onClick(node) - e.consume(true) - } - .onPointerEvent(PointerEventType.GLOBAL_RELEASE) { _, _ -> - pressed = false - } - .then(modifier), - contentAlignment = Alignment.Center, - ) { - content(hovered, pressed) - } -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/ColorPicker.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/ColorPicker.kt deleted file mode 100644 index 1960dda16..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/ColorPicker.kt +++ /dev/null @@ -1,179 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.input - -import androidx.compose.runtime.* -import net.kernelpanicsoft.archie.gui.layout.* -import net.kernelpanicsoft.archie.gui.modifiers.Constraints -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.input.* -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.util.HsvColor -import net.kernelpanicsoft.archie.gui.util.KColor -import net.kernelpanicsoft.archie.gui.util.extension.drawRectOutline -import net.kernelpanicsoft.archie.gui.util.extension.fillGradient -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.minecraft.client.gui.GuiGraphics -import kotlin.math.max - -// ── Internal sub-composables ────────────────────────────────────────────── - -@Composable -private fun SaturationValueArea( - modifier: Modifier = Modifier, - hue: Float, - saturation: Float, - value: Float, - onSaturationValueChanged: (saturation: Float, value: Float) -> Unit, -) { - val onEvent = { node: LayoutNode, event: PointerEvent -> - val newSat = ((event.mouseX - node.absoluteCoords.x) / node.width).toFloat().coerceIn(0f, 1f) - val newVal = (1f - ((event.mouseY - node.absoluteCoords.y) / node.height).toFloat()).coerceIn(0f, 1f) - onSaturationValueChanged(newSat, newVal) - event.consume() - } - - Layout( - name = "SaturationValueArea", - measurePolicy = { _, _, constraints -> MeasureResult(constraints.minWidth, constraints.minHeight) {} }, - renderer = object : Renderer { - override fun render(node: UINode, x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) = guiGraphics { - fillGradient(x, y, node.width, node.height, - KColor.ofHsv(hue, 0f, 1f).argb, KColor.ofHsv(hue, 1f, 1f).argb, - KColor.ofHsv(hue, 0f, 0f).argb, KColor.ofHsv(hue, 1f, 0f).argb) - drawRectOutline(x + (saturation * node.width).toInt() - 2, y + ((1 - value) * node.height).toInt() - 2, 4, 4, KColor.WHITE.argb) - } - }, - modifier = modifier - .onPointerEvent(PointerEventType.PRESS, onEvent) - .onDrag(onDragEvent = onEvent), - ) -} - -@Composable -private fun HueBar(modifier: Modifier = Modifier, hue: Float, onHueChanged: (Float) -> Unit) { - val onEvent = { node: LayoutNode, event: PointerEvent -> - val newHue = (1f - ((event.mouseY - node.absoluteCoords.y) / node.height).toFloat()).coerceIn(0f, 1f) - onHueChanged(newHue); event.consume() - } - Layout( - name = "HueBar", - measurePolicy = { _, _, constraints -> MeasureResult(constraints.minWidth, constraints.minHeight) {} }, - renderer = object : Renderer { - override fun render(node: UINode, x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) = guiGraphics { - for (j in 0 until node.height) { - fill(x, y + j, x + node.width, y + j + 1, KColor.ofHsv(1f - (j.toFloat() / node.height), 1f, 1f).argb) - } - drawRectOutline(x - 1, y + ((1 - hue) * node.height).toInt() - 1, node.width + 2, 3, KColor.WHITE.argb) - } - }, - modifier = modifier.onPointerEvent(PointerEventType.PRESS, onEvent).onDrag(onDragEvent = onEvent), - ) -} - -@Composable -private fun AlphaBar(modifier: Modifier = Modifier, color: HsvColor, onAlphaChanged: (Float) -> Unit) { - val onEvent = { node: LayoutNode, event: PointerEvent -> - val newAlpha = ((event.mouseX - node.absoluteCoords.x) / node.width).toFloat().coerceIn(0f, 1f) - onAlphaChanged(newAlpha); event.consume() - } - Layout( - name = "AlphaBar", - measurePolicy = { _, _, constraints -> MeasureResult(constraints.minWidth, constraints.minHeight) {} }, - renderer = object : Renderer { - override fun render(node: UINode, x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) = guiGraphics { - val checkerSize = 4 - for (cx in 0 until node.width step checkerSize) - for (cy in 0 until node.height step checkerSize) - fill(x + cx, y + cy, x + cx + checkerSize, y + cy + checkerSize, - if ((cx / checkerSize + cy / checkerSize) % 2 == 0) KColor.WHITE.rgb else KColor.LIGHT_GRAY.rgb) - val opaque = color.copy(alpha = 1f).toKColor().argb - val transparent = color.copy(alpha = 0f).toKColor().argb - fillGradient(x, y, node.width, node.height, transparent, opaque, transparent, opaque) - drawRectOutline(x + (color.alpha * node.width).toInt() - 1, y - 1, 3, node.height + 2, KColor.WHITE.argb) - } - }, - modifier = modifier.onPointerEvent(PointerEventType.PRESS, onEvent).onDrag(onDragEvent = onEvent), - ) -} - -// ── Public API ───────────────────────────────────────────────────────────── - -/** - * A fully controlled HSV + alpha colour picker composable. - * - * This composable is **stateless**: it displays the colour provided by [color] and - * reports changes through [onColorChanged]. The caller is responsible for creating - * and hoisting the state, typically via `remember { mutableStateOf(HsvColor(...)) }`. - * - * The picker consists of a saturation-value gradient area, an optional alpha slider, and - * a vertical hue slider. Apply a `Modifier.size(width, height)` to set the picker's - * overall dimensions. - * - * ### Example - * ```kotlin - * var color by remember { mutableStateOf(HsvColor.from(KColor.RED)) } - * ColorPicker( - * color = color, - * modifier = Modifier.size(200, 150), - * onColorChanged = { color = it }, - * ) - * ``` - * - * @param color The current colour value to display. - * @param showAlphaBar Whether to show the horizontal alpha slider. - * @param alphaBarHeight Height of the alpha slider in pixels. - * @param hueBarWidth Width of the vertical hue slider in pixels. - * @param barPadding Gap in pixels between the main SV area and the sliders. - * @param modifier Modifiers applied to the picker container (size required). - * @param onColorChanged Called with the updated [HsvColor] on every user interaction. - */ -@Composable -fun ColorPicker( - color: HsvColor, - showAlphaBar: Boolean = true, - alphaBarHeight: Int = 12, - hueBarWidth: Int = 16, - barPadding: Int = 8, - modifier: Modifier = Modifier, - onColorChanged: (HsvColor) -> Unit, -) { - val updatedCallback by rememberUpdatedState(onColorChanged) - - val measurePolicy = remember(showAlphaBar, alphaBarHeight, hueBarWidth, barPadding) { - MeasurePolicy { _, measurables, constraints -> - if (showAlphaBar) { - check(measurables.size == 3) { "ColorPicker with showAlphaBar=true expects exactly 3 children" } - val (svM, alphaM, hueM) = measurables - val svW = max(0, constraints.maxWidth - hueBarWidth - barPadding) - val svH = max(0, constraints.maxHeight - alphaBarHeight - barPadding) - val svP = svM.measure(Constraints(svW, svW, svH, svH)) - val alphaP = alphaM.measure(Constraints(svW, svW, alphaBarHeight, alphaBarHeight)) - val hueP = hueM.measure(Constraints(hueBarWidth, hueBarWidth, constraints.maxHeight, constraints.maxHeight)) - MeasureResult(constraints.maxWidth, constraints.maxHeight) { - svP.placeAt(0, 0) - alphaP.placeAt(0, svP.height + barPadding) - hueP.placeAt(svP.width + barPadding, 0) - } - } else { - check(measurables.size == 2) { "ColorPicker with showAlphaBar=false expects exactly 2 children" } - val (svM, hueM) = measurables - val svW = max(0, constraints.maxWidth - hueBarWidth - barPadding) - val svP = svM.measure(Constraints(svW, svW, constraints.maxHeight, constraints.maxHeight)) - val hueP = hueM.measure(Constraints(hueBarWidth, hueBarWidth, constraints.maxHeight, constraints.maxHeight)) - MeasureResult(constraints.maxWidth, constraints.maxHeight) { - svP.placeAt(0, 0) - hueP.placeAt(svP.width + barPadding, 0) - } - } - } - } - - Layout(name = "ColorPicker", measurePolicy = measurePolicy, modifier = modifier) { - SaturationValueArea(hue = color.hue, saturation = color.saturation, value = color.value) { s, v -> - updatedCallback(color.copy(saturation = s, value = v)) - } - if (showAlphaBar) { - AlphaBar(color = color) { a -> updatedCallback(color.copy(alpha = a)) } - } - HueBar(hue = color.hue) { h -> updatedCallback(color.copy(hue = h)) } - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Radio.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Radio.kt deleted file mode 100644 index 2ef5cd187..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Radio.kt +++ /dev/null @@ -1,159 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.input - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import net.kernelpanicsoft.archie.gui.composables.basic.Text -import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates -import net.kernelpanicsoft.archie.gui.composables.theme.WidgetState -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.layout.Arrangement -import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy -import net.kernelpanicsoft.archie.gui.layout.Column -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.Renderer -import net.kernelpanicsoft.archie.gui.layout.Row -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.sizeIn -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.theme.LocalTheme -import net.kernelpanicsoft.archie.gui.theme.SimpleThemeState -import net.kernelpanicsoft.archie.gui.theme.ThemeVariants -import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.minecraft.client.gui.GuiGraphics -import net.minecraft.network.chat.Component - -/** - * Low-level unstyled radio-button behavior, built on [Clickable]. - * - * Calls [onSelect] on press only when not already [selected] (clicking an already-selected - * radio option is a no-op, matching standard radio-group semantics). Applies no visuals - - * that is up to [content]. - * - * @param selected Whether this option is currently selected. - * @param onSelect Invoked when this (unselected) option is clicked. - * @param modifier Additional modifiers applied to the outer clickable container. - * @param enabled When `false`, pointer events are ignored. - * @param content The visual content; receives hover/press state and [selected]. - */ -@Composable -fun RadioButtonCore( - selected: Boolean, - onSelect: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - content: @Composable (isHovered: Boolean, isPressed: Boolean, selected: Boolean) -> Unit, -) { - Clickable( - onClick = { if (!selected) onSelect() }, - enabled = enabled, - modifier = modifier, - ) { hovered, pressed -> - content(hovered, pressed, selected) - } -} - -/** - * A standard themed radio button with a filled center dot when [selected]. - * - * Renders the themed [texture] state - a combined selected+hovered state is used when both - * apply and the theme defines it. See [RadioGroup] for a labeled option list. - * - * @param selected Whether this option is currently selected. - * @param onSelect Invoked when this (unselected) option is clicked. - * @param modifier Additional modifiers applied to the outer container. - * @param enabled When `false`, pointer events are ignored and the [TextureStates.DISABLED] state is shown. - * @param texture The themed texture key to look up via [LocalTheme]. - * @param variant The theme variant of [texture] to use. See [ThemeVariants]. - */ -@Composable -fun RadioButton( - selected: Boolean, - onSelect: () -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - texture: String = "radio", - variant: String = ThemeVariants.DEFAULT, -) { - val theme = LocalTheme.current - val composableTheme = theme.getComposableTheme(texture) - val measurePolicy = remember { BoxMeasurePolicy(Alignment.Center) } - val sizeModifier = if (!composableTheme.isNineslice) { - with(composableTheme.states[TextureStates.DEFAULT] as SimpleThemeState) { - Modifier.sizeIn(minWidth = width, minHeight = height) - } - } else Modifier - - RadioButtonCore( - selected = selected, - onSelect = onSelect, - enabled = enabled, - modifier = sizeModifier.then(modifier), - ) { hovered, _, currentSelected -> - Layout( - name = "RadioButton", - measurePolicy = measurePolicy, - modifier = sizeModifier, - renderer = object : Renderer { - override fun render( - node: UINode, - x: Int, - y: Int, - guiGraphics: GuiGraphics, - mouseX: Int, - mouseY: Int, - partialTick: Float, - ) = guiGraphics { - val stateKey = WidgetState.resolve( - composableTheme, variant, - WidgetState.clicked(currentSelected), WidgetState.hovered(hovered), - enabled = enabled, - ) - node.renderState = stateKey - val state = composableTheme.getState(stateKey, variant) - - drawThemeState(state, x, y, node.width, node.height) - } - }, - ) - } -} - -/** A single labeled choice within a [RadioGroup]. */ -data class RadioOption( - val value: T, - val label: Component, - val enabled: Boolean = true, -) - -/** - * A vertical list of labeled, mutually exclusive [RadioButton]s. - * - * @param options The selectable options, in display order. - * @param selected The currently selected value, or `null` if none is selected. - * @param onSelected Called with an option's value when it is selected. - * @param modifier Additional modifiers applied to the outer [Column]. - * @param optionSpacing Vertical spacing between options, in pixels. - */ -@Composable -fun RadioGroup( - options: List>, - selected: T?, - onSelected: (T) -> Unit, - modifier: Modifier = Modifier, - optionSpacing: Int = 3, -) { - Column(modifier = modifier, verticalArrangement = Arrangement.spacedBy(optionSpacing)) { - options.forEach { option -> - Row(horizontalArrangement = Arrangement.spacedBy(4), verticalAlignment = Alignment.CenterVertically) { - RadioButton( - selected = option.value == selected, - enabled = option.enabled, - onSelect = { onSelected(option.value) }, - ) - Text(option.label, dropShadow = false) - } - } - } -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Slider.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Slider.kt deleted file mode 100644 index 149d01735..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Slider.kt +++ /dev/null @@ -1,203 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.input - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.Renderer -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType -import net.kernelpanicsoft.archie.gui.modifiers.input.onDrag -import net.kernelpanicsoft.archie.gui.modifiers.input.onPointerEvent -import net.kernelpanicsoft.archie.gui.modifiers.sizeIn -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.composables.theme.WidgetState -import net.kernelpanicsoft.archie.gui.theme.ComposableTheme -import net.kernelpanicsoft.archie.gui.theme.LocalTheme -import net.kernelpanicsoft.archie.gui.theme.ThemeVariants -import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.minecraft.client.gui.GuiGraphics -import kotlin.math.roundToInt - -private const val SLIDER_MIN_WIDTH = 96 -private const val SLIDER_MIN_HEIGHT = 20 -private const val SLIDER_THUMB_WIDTH = 8 -private const val SLIDER_THUMB_HEIGHT = 20 -private const val SLIDER_TRACK_HEIGHT = 2 - -/** Clamps a slider value into the normalized `0f..1f` range. */ -internal fun normalizeSliderValue(value: Float): Float = value.coerceIn(0f, 1f) - -/** Normalizes [value] then rounds it to the nearest of [steps] evenly spaced increments (no snapping when [steps] <= 0). */ -internal fun snapSliderValue(value: Float, steps: Int): Float { - if (steps <= 0) return normalizeSliderValue(value) - val clamped = normalizeSliderValue(value) - val stepSize = 1f / steps.toFloat() - return (clamped / stepSize).roundToInt() * stepSize -} - -/** Clamps a raw thumb x-position so the [thumbWidth]-wide thumb stays within the track bounds. */ -internal fun resolveSliderThumbX(rawThumbX: Int, sliderX: Int, sliderWidth: Int, thumbWidth: Int = SLIDER_THUMB_WIDTH): Int { - val minThumbX = sliderX - val maxThumbX = (sliderX + sliderWidth - thumbWidth).coerceAtLeast(minThumbX) - return rawThumbX.coerceIn(minThumbX, maxThumbX) -} - -private fun resolveSliderStateName(theme: ComposableTheme, variant: String, enabled: Boolean, hovered: Boolean, dragging: Boolean): String = - WidgetState.resolve(theme, variant, WidgetState.clicked(dragging), WidgetState.hovered(hovered), enabled = enabled) - -/** - * Low-level unstyled slider behavior: drag/click-to-position and hover/drag state tracking, - * with no visuals of its own. - * - * @param value The current value, normalized/snapped via [snapSliderValue]. - * @param onValueChange Called with the new normalized value on every drag/click update. - * @param modifier Additional modifiers applied to the outer container. - * @param enabled When `false`, pointer events are ignored. - * @param steps Number of discrete increments to snap to; `0` means continuous. - * @param onValueChangeFinished Called once when a drag interaction ends (on release). - * @param content The visual content; receives hover/drag state and the - * normalized, snapped value to render. - */ -@Composable -fun SliderCore( - value: Float, - onValueChange: (Float) -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - steps: Int = 0, - onValueChangeFinished: () -> Unit = {}, - content: @Composable (isHovered: Boolean, isDragging: Boolean, normalizedValue: Float) -> Unit, -) { - val normalizedValue = snapSliderValue(value, steps) - - var hovered by remember { mutableStateOf(false) } - var dragging by remember { mutableStateOf(false) } - - fun updateFromPointer(node: UINode, mouseX: Double) { - val localX = (mouseX - node.x).toFloat() - val fraction = if (node.width <= 1) 0f else localX / node.width.toFloat() - onValueChange(snapSliderValue(fraction, steps)) - } - - Layout( - name = "SliderCore", - measurePolicy = remember { BoxMeasurePolicy(Alignment.CenterStart) }, - modifier = Modifier - .onPointerEvent(PointerEventType.ENTER) { _, event -> - if (!enabled) return@onPointerEvent - hovered = true - event.consume() - } - .onPointerEvent(PointerEventType.EXIT) { _, event -> - hovered = false - dragging = false - if (enabled) event.consume() - } - .onPointerEvent(PointerEventType.PRESS) { node, event -> - if (!enabled) return@onPointerEvent - dragging = true - updateFromPointer(node, event.mouseX) - event.consume(true) - } - .onDrag { node, event -> - if (!enabled || !dragging) return@onDrag - updateFromPointer(node, event.mouseX) - event.consume() - } - .onPointerEvent(PointerEventType.GLOBAL_RELEASE) { _, _ -> - if (!enabled || !dragging) return@onPointerEvent - dragging = false - onValueChangeFinished() - } - .then(modifier), - ) { - content(hovered, dragging, normalizedValue) - } -} - -/** - * A standard themed horizontal slider, drawing a "slider" track and "slider_handle" thumb - * from the current theme, plus a solid-color fill up to the thumb. - * - * @param value The current value, normalized/snapped via [snapSliderValue]. - * @param onValueChange Called with the new normalized value on every drag/click update. - * @param modifier Additional modifiers applied to the outer container. - * @param enabled When `false`, the disabled state is drawn and input is ignored. - * @param variant The theme variant used for both the track and thumb textures. - * @param steps Number of discrete increments to snap to; `0` means continuous. - * @param onValueChangeFinished Called once when a drag interaction ends (on release). - */ -@Composable -fun Slider( - value: Float, - onValueChange: (Float) -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - variant: String = ThemeVariants.DEFAULT, - steps: Int = 0, - onValueChangeFinished: () -> Unit = {}, -) { - val measurePolicy = remember { BoxMeasurePolicy(Alignment.CenterStart) } - val theme = LocalTheme.current - val trackTheme = theme.getComposableTheme("slider") - val thumbTheme = theme.getComposableTheme("slider_handle") - val sizeModifier = Modifier.sizeIn(minWidth = SLIDER_MIN_WIDTH, minHeight = SLIDER_MIN_HEIGHT) - SliderCore( - value = value, - onValueChange = onValueChange, - enabled = enabled, - steps = steps, - onValueChangeFinished = onValueChangeFinished, - modifier = sizeModifier.then(modifier), - ) { hovered, dragging, normalizedValue -> - Layout( - name = "Slider", - measurePolicy = measurePolicy, - modifier = sizeModifier, - renderer = object : Renderer { - override fun render( - node: UINode, - x: Int, - y: Int, - guiGraphics: GuiGraphics, - mouseX: Int, - mouseY: Int, - partialTick: Float, - ) = guiGraphics { - val trackY = y + (node.height - SLIDER_TRACK_HEIGHT) / 2 - val trackStart = x + (SLIDER_THUMB_WIDTH / 2) - val trackEnd = x + node.width - (SLIDER_THUMB_WIDTH / 2) - val availableTrack = (trackEnd - trackStart).coerceAtLeast(1) - val fillEnd = trackStart + (availableTrack * normalizedValue).roundToInt() - val thumbX = resolveSliderThumbX( - rawThumbX = fillEnd - (SLIDER_THUMB_WIDTH / 2), - sliderX = x, - sliderWidth = node.width, - thumbWidth = SLIDER_THUMB_WIDTH, - ) - val thumbY = y + (node.height - SLIDER_THUMB_HEIGHT) / 2 - - val stateName = resolveSliderStateName(trackTheme, variant, enabled, hovered, dragging) - node.renderState = stateName - val trackState = trackTheme.getState(stateName, variant) - val thumbState = thumbTheme.getState(stateName, variant) - - val fillColor = if (enabled) 0xFF6BA8FF.toInt() else 0xFF5A5A5A.toInt() - - drawThemeState(trackState, x, y, node.width, node.height) - fill(trackStart, trackY, fillEnd, trackY + SLIDER_TRACK_HEIGHT, fillColor) - drawThemeState(thumbState, thumbX, thumbY, SLIDER_THUMB_WIDTH, SLIDER_THUMB_HEIGHT) - } - }, - ) - } -} - - - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Switch.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Switch.kt deleted file mode 100644 index 1ee370a11..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Switch.kt +++ /dev/null @@ -1,133 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.input - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import net.kernelpanicsoft.archie.gui.animation.AnimationSpec -import net.kernelpanicsoft.archie.gui.animation.Easings -import net.kernelpanicsoft.archie.gui.animation.animateInt -import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates -import net.kernelpanicsoft.archie.gui.composables.theme.WidgetState -import net.kernelpanicsoft.archie.gui.layout.BoxMeasurePolicy -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.Renderer -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.sizeIn -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.theme.LocalTheme -import net.kernelpanicsoft.archie.gui.theme.ThemeVariants -import net.kernelpanicsoft.archie.gui.util.extension.drawThemeState -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.minecraft.client.gui.GuiGraphics -import kotlin.time.Duration.Companion.milliseconds - -private const val SWITCH_MIN_WIDTH = 34 -private const val SWITCH_MIN_HEIGHT = 18 -private const val SWITCH_PADDING = 2 -private const val SWITCH_THUMB_SIZE = 14 - -/** Clamps a raw thumb x-offset so the thumb stays within the track, respecting [SWITCH_PADDING]. */ -internal fun resolveSwitchThumbOffset(thumbOffset: Int, trackWidth: Int): Int { - val minOffset = SWITCH_PADDING - val maxOffset = (trackWidth - SWITCH_THUMB_SIZE - SWITCH_PADDING).coerceAtLeast(minOffset) - return thumbOffset.coerceIn(minOffset, maxOffset) -} - -/** - * Low-level switch primitive exposing hover/press state and checked state to custom visuals. - */ -@Composable -fun SwitchCore( - checked: Boolean, - onCheckedChange: (Boolean) -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - content: @Composable (isHovered: Boolean, isPressed: Boolean, checked: Boolean) -> Unit, -) { - Clickable( - onClick = { onCheckedChange(!checked) }, - enabled = enabled, - modifier = modifier, - ) { hovered, pressed -> - content(hovered, pressed, checked) - } -} - -/** - * Simple styled switch control suitable for toggling boolean settings. - * - * Renders the themed [trackTexture] state - a combined checked+hovered state is used when - * both apply and the theme defines it - with the themed [thumbTexture] drawn on top, - * animating between its off/on positions. - * - * @param checked The current checked state. - * @param onCheckedChange Called with the new checked value when the user clicks. - * @param modifier Additional modifiers applied to the outer container. - * @param enabled When `false`, pointer events are ignored and the [TextureStates.DISABLED] state is shown. - * @param trackTexture The themed texture key for the track, looked up via [LocalTheme]. - * @param thumbTexture The themed texture key for the thumb, looked up via [LocalTheme]. - * @param variant The theme variant of both textures to use. See [ThemeVariants]. - */ -@Composable -fun Switch( - checked: Boolean, - onCheckedChange: (Boolean) -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - trackTexture: String = "switch_track", - thumbTexture: String = "switch_thumb", - variant: String = ThemeVariants.DEFAULT, -) { - val theme = LocalTheme.current - val trackTheme = theme.getComposableTheme(trackTexture) - val thumbTheme = theme.getComposableTheme(thumbTexture) - val measurePolicy = remember { BoxMeasurePolicy(Alignment.CenterStart) } - val sizeModifier = Modifier.sizeIn(minWidth = SWITCH_MIN_WIDTH, minHeight = SWITCH_MIN_HEIGHT) - val thumbOffset = animateInt( - targetValue = if (checked) SWITCH_MIN_WIDTH - SWITCH_THUMB_SIZE - SWITCH_PADDING else SWITCH_PADDING, - spec = AnimationSpec(durationMillis = 140.milliseconds, easing = Easings.OutCubic), - ) - - SwitchCore( - checked = checked, - onCheckedChange = onCheckedChange, - enabled = enabled, - modifier = sizeModifier.then(modifier), - ) { hovered, _, currentChecked -> - Layout( - name = "Switch", - measurePolicy = measurePolicy, - modifier = sizeModifier, - renderer = object : Renderer { - override fun render( - node: UINode, - x: Int, - y: Int, - guiGraphics: GuiGraphics, - mouseX: Int, - mouseY: Int, - partialTick: Float, - ) = guiGraphics { - val trackStateKey = WidgetState.resolve( - trackTheme, variant, - WidgetState.clicked(currentChecked), WidgetState.hovered(hovered), - enabled = enabled, - ) - node.renderState = trackStateKey - val trackState = trackTheme.getState(trackStateKey, variant) - val thumbState = thumbTheme.getState( - WidgetState.resolve(thumbTheme, variant, enabled = enabled), - variant - ) - - drawThemeState(trackState, x, y, node.width, node.height) - - val thumbX = x + resolveSwitchThumbOffset(thumbOffset, node.width) - val thumbY = y + ((node.height - SWITCH_THUMB_SIZE) / 2) - drawThemeState(thumbState, thumbX, thumbY, SWITCH_THUMB_SIZE, SWITCH_THUMB_SIZE) - } - }, - ) - } -} - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextField.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextField.kt deleted file mode 100644 index 0ff04b41d..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextField.kt +++ /dev/null @@ -1,221 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.input.textfield - -import androidx.compose.runtime.* -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.MeasureResult -import net.kernelpanicsoft.archie.gui.layout.Renderer -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.util.KColor -import net.kernelpanicsoft.archie.gui.util.extension.invoke -import net.kernelpanicsoft.archie.gui.util.extension.pose -import net.kernelpanicsoft.archie.gui.util.extension.scissor -import net.minecraft.client.Minecraft -import net.minecraft.client.gui.Font -import net.minecraft.client.gui.GuiGraphics -import net.minecraft.client.renderer.RenderType -import net.minecraft.resources.ResourceLocation -import net.minecraft.util.Mth -import kotlin.math.max -import kotlin.math.min - -private val TEXT_FIELD_SPRITE = ResourceLocation.withDefaultNamespace("widget/text_field") -private val TEXT_FIELD_HIGHLIGHTED = ResourceLocation.withDefaultNamespace("widget/text_field_highlighted") -private val SCROLLER_SPRITE = ResourceLocation.withDefaultNamespace("widget/scroller") -private const val BORDER_PADDING = 4 -private const val SCROLL_BAR_W = 8 - -/** - * A simple, controlled text field that uses a plain `String` as its state. - * - * This is a convenience wrapper around [TextField] that manages a [TextFieldValue] - * internally, converting to and from `String` for the [onValueChange] callback. - * - * @param value The current text string. - * @param onValueChange Called with the updated string on every edit. - * @param modifier Additional modifiers applied to the text field. - * @param enabled When `false`, input is ignored and the field appears disabled. - * @param readOnly When `true`, text can be selected and copied but not edited. - * @param textColor ARGB colour of the rendered text. - * @param cursorColor ARGB colour of the blinking cursor line. - * @param selectionColor ARGB colour of the text-selection highlight. - * @param font The [Font] used for rendering and measurement. - * @param singleLine When `true` the field occupies a single horizontal line. - * @param maxLength Maximum allowed character count. - * @param maxLines Maximum allowed line count (multi-line only). - */ -@Composable -fun BasicTextField( - value: String, - onValueChange: (String) -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - readOnly: Boolean = false, - textColor: KColor = KColor.ofRgb(0xE0E0E0), - cursorColor: KColor = KColor.ofRgb(0xFFD0D0D0.toInt()), - selectionColor: KColor = KColor.ofRgb(-16776961), - font: Font = Minecraft.getInstance().font, - singleLine: Boolean = true, - maxLength: Int = Int.MAX_VALUE, - maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, -) { - var tfv by remember(value) { mutableStateOf(TextFieldValue(value)) } - TextField( - value = tfv, - onValueChange = { tfv = it; onValueChange(it.text) }, - modifier = modifier, enabled = enabled, readOnly = readOnly, - textColor = textColor, cursorColor = cursorColor, selectionColor = selectionColor, - font = font, singleLine = singleLine, maxLength = maxLength, maxLines = maxLines, - ) -} - -/** - * A fully controlled text field composable with the default Minecraft widget appearance. - * - * Supports single-line and multi-line modes, cursor navigation, text selection, - * clipboard operations, and an optional scrollbar for multi-line overflow. - * - * Use [BasicTextField] if you only need a simple `String`-based API. Use this composable - * when you need full control over [TextFieldValue] (e.g. selection or IME state). - * - * @param value The current [TextFieldValue]. - * @param onValueChange Called on every edit with the new [TextFieldValue]. - * @param modifier Additional modifiers. - * @param enabled Whether the field accepts input. - * @param readOnly Whether the field permits editing. - * @param textColor Text colour. - * @param cursorColor Cursor colour. - * @param selectionColor Selection highlight colour. - * @param font The [Font] used for rendering. - * @param singleLine Single vs multi-line mode. - * @param maxLength Character cap. - * @param maxLines Line cap (multi-line only). - */ -@Composable -fun TextField( - value: TextFieldValue, - onValueChange: (TextFieldValue) -> Unit, - modifier: Modifier = Modifier, - enabled: Boolean = true, - readOnly: Boolean = false, - textColor: KColor = KColor.ofRgb(0xE0E0E0), - cursorColor: KColor = KColor.ofRgb(0xFFD0D0D0.toInt()), - selectionColor: KColor = KColor.ofRgb(-16776961), - font: Font = Minecraft.getInstance().font, - singleLine: Boolean = true, - maxLength: Int = Int.MAX_VALUE, - maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, -) { - TextFieldCore( - value = value, onValueChange = onValueChange, font = font, - modifier = modifier, enabled = enabled, readOnly = readOnly, - singleLine = singleLine, maxLength = maxLength, maxLines = maxLines, - ) { state -> - Layout( - name = "TextField", - measurePolicy = { _, _, constraints -> - val w = constraints.maxWidth - val h = if (singleLine) font.lineHeight + BORDER_PADDING * 2 else constraints.maxHeight - MeasureResult(w, h) {} - }, - renderer = object : Renderer { - override fun render(node: UINode, x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) = guiGraphics { - val (w, h) = state.layoutInfo - if (w <= 0 || h <= 0) return@guiGraphics - - val sprite = if (enabled && state.isFocused) TEXT_FIELD_HIGHLIGHTED else TEXT_FIELD_SPRITE - blitSprite(sprite, x, y, w, h) - - val cw = w - BORDER_PADDING * 2 - val ch = h - BORDER_PADDING * 2 - val cx = x + BORDER_PADDING - val cy = y + BORDER_PADDING - - scissor(cx, cy, cx + cw, cy + ch) { - pose { - translate(cx.toDouble(), cy.toDouble(), 0.0) - - if (singleLine) renderSingleLine( - value, font, state, cw, textColor.argb, - if (state.showCursor && state.isFocused) cursorColor.argb else 0, selectionColor.argb - ) - else - { - translate(0.0, -state.scrollY, 0.0) - renderMultiLine( - value, - font, - value.text.lines(), - if (state.showCursor && state.isFocused) cursorColor.argb else 0, - selectionColor.argb, - textColor.argb - ) - } - } - } - - if (!singleLine) { - val contentH = value.text.lines().size * font.lineHeight - if (contentH > ch) renderScrollBar(x + w - SCROLL_BAR_W, y, h, contentH, state.scrollY) - } - } - }, - ) - } -} - -// ── Rendering helpers ────────────────────────────────────────────────────── - -private fun GuiGraphics.renderSingleLine(value: TextFieldValue, font: Font, state: TextFieldState, width: Int, tc: Int, cc: Int, sc: Int) { - val text = value.text; val sel = value.selection - val visible = font.plainSubstrByWidth(text.substring(state.displayPos), width) - drawString(font, visible, 0, 0, tc) - if (sel.length > 0) { - val s = (sel.min - state.displayPos).coerceAtLeast(0) - val e = (sel.max - state.displayPos).coerceAtLeast(0) - val vp = text.substring(state.displayPos) - val sx = font.width(vp.take(s.coerceAtMost(vp.length))) - val ex = font.width(vp.take(e.coerceAtMost(vp.length))) - fill(RenderType.guiTextHighlight(), sx, -1, ex, font.lineHeight, sc) - } - if (cc != 0 && sel.isCollapsed && sel.start >= state.displayPos) { - val cx = font.width(text.substring(state.displayPos, sel.start)) - fill(cx, -1, cx + 1, font.lineHeight, cc) - } -} - -private fun GuiGraphics.renderMultiLine(value: TextFieldValue, font: Font, lines: List, cc: Int, sc: Int, tc: Int) { - val text = value.text; val sel = value.selection - var y = 0; var charIdx = 0 - for (line in lines) { - drawString(font, line, 0, y, tc) - if (sel.length > 0) { - val ls = charIdx; val le = ls + line.length - if (sel.min <= le && sel.max >= ls) { - val sil = max(sel.min, ls) - ls; val eil = min(sel.max, le) - ls - val sx = font.width(line.take(sil)); val ex = font.width(line.take(eil)) - fill(RenderType.guiTextHighlight(), sx, y, ex, y + font.lineHeight, sc) - } - } - y += font.lineHeight; charIdx += line.length + 1 - } - if (cc != 0 && sel.isCollapsed) { - val before = text.take(sel.start) - val li = before.count { it == '\n' } - val nl = before.lastIndexOf('\n') - val col = sel.start - (if (nl == -1) 0 else nl + 1) - if (li < lines.size) { - val curX = font.width(lines[li].substring(0, col.coerceAtMost(lines[li].length))) - val curY = li * font.lineHeight - fill(curX, curY, curX + 1, curY + font.lineHeight, cc) - } - } -} - -private fun GuiGraphics.renderScrollBar(x: Int, y: Int, nodeH: Int, contentH: Int, scrollY: Double) { - val innerH = nodeH - BORDER_PADDING * 2 - val thumbH = Mth.clamp((innerH * innerH) / contentH, 32, innerH) - val maxScroll = (contentH - innerH).coerceAtLeast(1) - val sby = y + BORDER_PADDING + Mth.clamp((scrollY * (innerH - thumbH)) / maxScroll, 0.0, (innerH - thumbH).toDouble()).toInt() - blitSprite(SCROLLER_SPRITE, x, sby, SCROLL_BAR_W, thumbH) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldCore.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldCore.kt deleted file mode 100644 index 93e3138fc..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldCore.kt +++ /dev/null @@ -1,313 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.input.textfield - -import androidx.compose.runtime.* -import kotlinx.coroutines.delay -import net.kernelpanicsoft.archie.gui.layout.Layout -import net.kernelpanicsoft.archie.gui.layout.LayoutNode -import net.kernelpanicsoft.archie.gui.layout.MeasureResult -import net.kernelpanicsoft.archie.gui.layout.Size -import net.kernelpanicsoft.archie.gui.modifiers.Constraints -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.input.* -import net.kernelpanicsoft.archie.util.minecraftClient -import net.minecraft.client.Minecraft -import net.minecraft.client.gui.Font -import net.minecraft.client.gui.screens.Screen -import net.minecraft.util.Mth -import net.minecraft.util.StringUtil -import kotlin.math.max - -private const val BORDER_PADDING = 4 -private const val CURSOR_BLINK_INTERVAL_MS = 300L - -/** - * Internal mutable state for a text field, tracking focus, scroll position, cursor blink, - * and layout dimensions. - * - * Obtain an instance via [rememberTextFieldState] and pass it to [TextFieldCore]. - */ -@Stable -class TextFieldState { - /** Horizontal scroll offset for single-line fields (index of the first visible character). */ - var displayPos by mutableStateOf(0) - /** Vertical scroll offset for multi-line fields, in pixels. */ - var scrollY by mutableStateOf(0.0) - /** Whether the field currently holds input focus. */ - var isFocused by mutableStateOf(false) - /** Whether the blinking cursor is currently visible. */ - var showCursor by mutableStateOf(false) - internal var lastBlink by mutableStateOf(0L) - /** Whether the user is dragging the multi-line scroll bar. */ - var isDraggingScrollbar by mutableStateOf(false) - internal var layoutInfo by mutableStateOf(Size(0, 0)) - - /** Updates focus state and resets the cursor blink timer on focus gain. */ - fun onFocusChange(focused: Boolean) { - if (isFocused != focused) { - isFocused = focused - if (focused) { lastBlink = System.currentTimeMillis(); showCursor = true } - else showCursor = false - } - } -} - -/** Creates and remembers a [TextFieldState] instance. */ -@Composable -fun rememberTextFieldState(): TextFieldState = remember { TextFieldState() } - -/** - * Core composable that handles all state, focus, and input logic for a text field while - * delegating visual rendering entirely to [content]. - * - * This is the lowest-level text field building block. Build higher-level components on top - * of it (as [TextField] and [BasicTextField] do) to add visual decorations. - * - * @param value The current [TextFieldValue]. - * @param onValueChange Called whenever the user modifies the text or cursor position. - * @param font The [Font] used for text measurement. - * @param modifier Additional modifiers applied to the invisible layout node. - * @param enabled When `false`, keyboard events are ignored. - * @param readOnly When `true`, the text can be selected and copied but not edited. - * @param singleLine When `true`, Enter inserts a newline; otherwise the field is single-line. - * @param maxLength Maximum permitted character count. - * @param maxLines Maximum permitted line count (only relevant when [singleLine] is `false`). - * @param content The visual content composable; receives the managed [TextFieldState]. - */ -@Composable -fun TextFieldCore( - value: TextFieldValue, - onValueChange: (TextFieldValue) -> Unit, - font: Font, - modifier: Modifier = Modifier, - enabled: Boolean = true, - readOnly: Boolean = false, - singleLine: Boolean = true, - maxLength: Int = Int.MAX_VALUE, - maxLines: Int = if (singleLine) 1 else Int.MAX_VALUE, - content: @Composable (state: TextFieldState) -> Unit, -) { - val state = rememberTextFieldState() - - // Cursor blink coroutine - LaunchedEffect(state.isFocused) { - if (state.isFocused) { - while (true) { - val t = System.currentTimeMillis() - if (t - state.lastBlink > CURSOR_BLINK_INTERVAL_MS) { state.showCursor = !state.showCursor; state.lastBlink = t } - delay(50) - } - } else state.showCursor = false - } - - val scrollToCursor = { - val (nodeWidth, nodeHeight) = state.layoutInfo - if (nodeWidth > 0 && nodeHeight > 0) { - if (singleLine) { - val innerWidth = nodeWidth - BORDER_PADDING * 2 - val visible = font.plainSubstrByWidth(value.text.substring(state.displayPos), innerWidth) - val endPos = visible.length + state.displayPos - if (value.selection.start > endPos) state.displayPos = value.selection.start - visible.length - else if (value.selection.start <= state.displayPos) state.displayPos = value.selection.start - state.displayPos = Mth.clamp(state.displayPos, 0, value.text.length) - } else { - val innerHeight = nodeHeight - BORDER_PADDING * 2 - val contentHeight = value.text.lines().size * font.lineHeight - val maxScroll = max(0, contentHeight - innerHeight) - val cursorLine = value.text.take(value.selection.start).count { it == '\n' } - val cursorY = cursorLine * font.lineHeight - if (cursorY < state.scrollY) state.scrollY = cursorY.toDouble() - if (cursorY + font.lineHeight > state.scrollY + innerHeight) state.scrollY = (cursorY + font.lineHeight - innerHeight).toDouble() - state.scrollY = Mth.clamp(state.scrollY, 0.0, maxScroll.toDouble()) - } - } - } - - val onValueChangeAndScroll: (TextFieldValue) -> Unit = { v -> - onValueChange(v); scrollToCursor(); state.showCursor = true; state.lastBlink = System.currentTimeMillis() - } - - Layout( - name = "TextFieldCore", - measurePolicy = { _, measurables, constraints -> - val w = constraints.maxWidth - val h = if (singleLine) font.lineHeight + BORDER_PADDING * 2 else constraints.maxHeight - state.layoutInfo = Size(w, h) - - val fixedConstraints = Constraints(minWidth = w, maxWidth = w, minHeight = h, maxHeight = h) - val placeables = measurables.map { it.measure(fixedConstraints) } - MeasureResult(w, h) { - placeables.forEach { it.placeAt(0, 0) } - } - }, - modifier = modifier - .onKeyEvent { _, event -> - if (!enabled || !state.isFocused) return@onKeyEvent - if (event.keyCode == 256) { state.onFocusChange(false); event.consume(true); return@onKeyEvent } - var handled = true - val result = when { - Screen.isSelectAll(event.keyCode) -> value.copy(selection = TextRange(0, value.text.length)) - Screen.isCopy(event.keyCode) -> { Minecraft.getInstance().keyboardHandler.clipboard = value.selectedText; value } - Screen.isPaste(event.keyCode) && !readOnly -> handlePaste(value, maxLength, maxLines, singleLine) - Screen.isCut(event.keyCode) && !readOnly -> { Minecraft.getInstance().keyboardHandler.clipboard = value.selectedText; deleteSelected(value) } - !singleLine && !readOnly && event.keyCode in listOf(257, 335) -> - if (value.text.lines().size < maxLines) insert(value, "\n") else value - else -> { val after = handleMovementKey(event, value, singleLine, readOnly); if (after == value) handled = false; after } - } - if (result != value) onValueChangeAndScroll(result) - if (handled || minecraftClient.options.keyInventory.matches(event.keyCode, 0)) event.consume(true) - } - .onCharTyped { _, event -> - if (enabled && !readOnly && state.isFocused && StringUtil.isAllowedChatCharacter(event.codePoint)) { - if (value.text.length - value.selection.length < maxLength) { - onValueChangeAndScroll(insert(value, event.codePoint.toString())); event.consume(true) - } - } - } - .onPointerEvent(PointerEventType.PRESS) { node, event -> - if (state.isFocused && !node.isBounded(event.mouseX.toInt(), event.mouseY.toInt())) - state.onFocusChange(false) - } - .onPointerEvent(PointerEventType.PRESS) { node, event -> - val (nX, _) = node.absoluteCoords - val scrollBarX = nX + state.layoutInfo.width - 8 - if (!singleLine && event.mouseX >= scrollBarX && event.mouseX < nX + state.layoutInfo.width) { - state.isDraggingScrollbar = true - } else { - state.onFocusChange(true) - val lX = event.mouseX - node.absoluteCoords.x - BORDER_PADDING - val lY = event.mouseY - node.absoluteCoords.y - BORDER_PADDING - val cur = findCursorPos(font, value.text, lX, lY, state, singleLine) - val sel = if (Screen.hasShiftDown()) TextRange(value.selection.end, cur) else TextRange(cur) - onValueChangeAndScroll(value.copy(selection = sel)) - } - event.consume() - } - .onPointerEvent(PointerEventType.RELEASE) { _, _ -> state.isDraggingScrollbar = false } - .onDrag { node, event -> - if (state.isDraggingScrollbar) { - val contentH = value.text.lines().size * font.lineHeight - val innerH = state.layoutInfo.height - BORDER_PADDING * 2 - val thumbH = Mth.clamp((innerH * innerH) / contentH, 32, innerH) - val maxScroll = (contentH - innerH).coerceAtLeast(1) - state.scrollY = Mth.clamp(state.scrollY + event.dragY * maxScroll.toDouble() / (innerH - thumbH), 0.0, maxScroll.toDouble()) - } else if (state.isFocused) { - val lX = event.mouseX - node.absoluteCoords.x - BORDER_PADDING - val lY = event.mouseY - node.absoluteCoords.y - BORDER_PADDING - val cur = findCursorPos(font, value.text, lX, lY, state, singleLine) - onValueChangeAndScroll(value.copy(selection = TextRange(value.selection.end, cur))) - } - event.consume() - } - .onScroll { _, event -> - if (singleLine && !state.isFocused) return@onScroll - val contentH = value.text.lines().size * font.lineHeight - val innerH = state.layoutInfo.height - BORDER_PADDING * 2 - val maxScroll = (contentH - innerH).coerceAtLeast(0) - state.scrollY = Mth.clamp(state.scrollY - event.scrollY * font.lineHeight / 2.0, 0.0, maxScroll.toDouble()) - event.consume() - }, - ) { content(state) } -} - -// ── Private helpers ──────────────────────────────────────────────────────── - -private fun findCursorPos(font: Font, text: String, x: Double, y: Double, state: TextFieldState, singleLine: Boolean): Int { - return if (singleLine) { - state.displayPos + font.plainSubstrByWidth(text.substring(state.displayPos), x.toInt().coerceAtLeast(0)).length - } else { - val scrolledY = y + state.scrollY - val lineIdx = Mth.floor(scrolledY / font.lineHeight).coerceIn(0, text.lines().size - 1) - val lineText = text.lines()[lineIdx] - val charIdx = font.plainSubstrByWidth(lineText, x.toInt().coerceAtLeast(0)).length - text.split('\n').take(lineIdx).sumOf { it.length + 1 } + charIdx - } -} - -private fun insert(value: TextFieldValue, text: String): TextFieldValue { - val new = value.text.take(value.selection.min) + text + value.text.substring(value.selection.max) - return TextFieldValue(new, TextRange(value.selection.min + text.length)) -} - -private fun deleteSelected(value: TextFieldValue): TextFieldValue { - if (value.selection.length == 0) return value - return TextFieldValue(value.text.take(value.selection.min) + value.text.substring(value.selection.max), TextRange(value.selection.min)) -} - -private fun handlePaste(value: TextFieldValue, maxLength: Int, maxLines: Int, singleLine: Boolean): TextFieldValue { - var clip = Minecraft.getInstance().keyboardHandler.clipboard - val avail = maxLength - (value.text.length - value.selection.length) - if (clip.length > avail) clip = clip.take(avail) - if (!singleLine) { - val currentLines = value.text.lines().size - val linesInSel = value.selectedText.count { it == '\n' } - val availLines = maxLines - (currentLines - linesInSel) - var nl = 0 - clip = buildString { for (c in clip) { if (c == '\n') { nl++; if (nl >= availLines) break }; append(c) } } - } - return insert(value, clip) -} - -private fun handleMovementKey(event: KeyEvent, value: TextFieldValue, singleLine: Boolean, readOnly: Boolean): TextFieldValue { - if (readOnly) return when (event.keyCode) { - 262, 263, 264, 265, 268, 269 -> moveKey(event, value, singleLine) - else -> value - } - return moveKey(event, value, singleLine) -} - -private fun moveKey(event: KeyEvent, value: TextFieldValue, singleLine: Boolean): TextFieldValue { - val shift = Screen.hasShiftDown(); val ctrl = Screen.hasControlDown() - val text = value.text; val sel = value.selection - return when (event.keyCode) { - 259 -> { // BACKSPACE - if (sel.length > 0) deleteSelected(value) - else if (sel.start == 0) value - else { val p = if (ctrl) findLastWord(text, sel.start) else sel.start - 1; TextFieldValue(text.take(p) + text.substring(sel.start), TextRange(p)) } - } - 261 -> { // DELETE - if (sel.length > 0) deleteSelected(value) - else if (sel.start == text.length) value - else { val p = if (ctrl) findNextWord(text, sel.start) else sel.start + 1; TextFieldValue(text.take(sel.start) + text.substring(p), TextRange(sel.start)) } - } - 263 -> { // LEFT - val p = if (ctrl) findLastWord(text, sel.start) else (sel.start - 1).coerceAtLeast(0) - value.copy(selection = if (shift) TextRange(sel.end, p) else TextRange(p)) - } - 262 -> { // RIGHT - val p = if (ctrl) findNextWord(text, sel.start) else (sel.start + 1).coerceAtMost(text.length) - value.copy(selection = if (shift) TextRange(sel.end, p) else TextRange(p)) - } - 265 -> if (!singleLine) moveVertical(value, -1, shift) else value // UP - 264 -> if (!singleLine) moveVertical(value, 1, shift) else value // DOWN - 268 -> { // HOME - val nl = text.take(sel.start).lastIndexOf('\n') - val ls = if (nl == -1) 0 else nl + 1 - value.copy(selection = if (shift) TextRange(sel.end, ls) else TextRange(ls)) - } - 269 -> { // END - val nl = text.indexOf('\n', sel.start) - val le = if (nl == -1) text.length else nl - value.copy(selection = if (shift) TextRange(sel.end, le) else TextRange(le)) - } - else -> value - } -} - -private fun moveVertical(value: TextFieldValue, delta: Int, select: Boolean): TextFieldValue { - val lines = value.text.lines(); val cur = value.selection.start - val curLine = value.text.take(cur).count { it == '\n' } - val target = (curLine + delta).coerceIn(0, lines.lastIndex) - if (curLine == target) return value - val lastNl = value.text.take(cur).lastIndexOf('\n') - val col = cur - (if (lastNl == -1) 0 else lastNl + 1) - val tStart = value.text.split('\n').take(target).sumOf { it.length + 1 } - val newPos = (tStart + col).coerceAtMost(tStart + lines[target].length) - return value.copy(selection = if (select) TextRange(value.selection.end, newPos) else TextRange(newPos)) -} - -private fun findNextWord(text: String, from: Int): Int { - var i = from; while (i < text.length && text[i] == ' ') i++; while (i < text.length && text[i] != ' ') i++; return i -} -private fun findLastWord(text: String, from: Int): Int { - var i = from - 1; while (i >= 0 && text[i] == ' ') i--; while (i >= 0 && text[i] != ' ') i--; return i + 1 -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldValue.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldValue.kt deleted file mode 100644 index c6ea64c64..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldValue.kt +++ /dev/null @@ -1,61 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.input.textfield - -import androidx.compose.runtime.Immutable -import kotlin.math.max -import kotlin.math.min - -/** - * Represents a half-open character range `[start, end)` within a text field's content string. - * - * When [start] == [end] the range is *collapsed* and represents a cursor position rather than - * a selection. [start] and [end] may be in either order; [min] and [max] always give the - * canonical inclusive/exclusive bounds regardless of direction. - * - * @property start The anchor end of the range (inclusive, 0-based character index). - * @property end The active end of the range. Defaults to [start] (collapsed cursor). - */ -@Immutable -data class TextRange(val start: Int, val end: Int = start) { - /** `true` when [start] and [end] are equal (cursor, no selection). */ - val isCollapsed: Boolean get() = start == end - - /** The number of characters in the selected range. */ - val length: Int get() = max(start, end) - min(start, end) - - /** The smaller of [start] and [end] — inclusive start of the selected region. */ - val min: Int get() = minOf(start, end) - - /** The larger of [start] and [end] — exclusive end of the selected region. */ - val max: Int get() = maxOf(start, end) - - companion object { - /** A collapsed [TextRange] positioned at the beginning of the string. */ - val Zero = TextRange(0) - } - - override fun toString(): String = "TextRange(start=$start, end=$end)" -} - -/** - * Immutable value holder for a [net.kernelpanicsoft.archie.gui.composables.input.textfield.TextField] - * or [BasicTextField]. - * - * Contains the full text, the current selection (or cursor position), and an optional IME - * composition range. Pass new instances to `onValueChange` to update the field. - * - * @property text The current text content. - * @property selection The current selection or cursor position within [text]. - * @property composition The active IME composition range, or `null` when no composition is in progress. - */ -@Immutable -data class TextFieldValue( - val text: String = "", - val selection: TextRange = TextRange(text.length), - val composition: TextRange? = null, -) { - /** The characters currently selected by the user (empty string when the selection is collapsed). */ - val selectedText: String get() = text.substring(selection.min, selection.max) - - override fun toString(): String = - "TextFieldValue(text='$text', selection=$selection, composition=$composition)" -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/ConfirmDialog.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/ConfirmDialog.kt deleted file mode 100644 index d942575c1..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/ConfirmDialog.kt +++ /dev/null @@ -1,108 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.modal - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import net.kernelpanicsoft.archie.gui.animation.AnimationSpec -import net.kernelpanicsoft.archie.gui.animation.Easings -import net.kernelpanicsoft.archie.gui.animation.animateInt -import net.kernelpanicsoft.archie.gui.composables.basic.Text -import net.kernelpanicsoft.archie.gui.composables.containers.Surface -import net.kernelpanicsoft.archie.gui.composables.input.Button -import net.kernelpanicsoft.archie.gui.layer.ModalScope -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.layout.Arrangement -import net.kernelpanicsoft.archie.gui.layout.Column -import net.kernelpanicsoft.archie.gui.layout.Row -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.position.margin -import net.kernelpanicsoft.archie.gui.modifiers.position.offset -import net.kernelpanicsoft.archie.gui.modifiers.position.padding -import net.kernelpanicsoft.archie.gui.modifiers.sizeIn -import net.kernelpanicsoft.archie.gui.theme.LocalTheme -import net.minecraft.network.chat.Component -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import kotlin.time.Duration.Companion.milliseconds - -private const val DIALOG_ANIMATION_MS = 180L - -/** - * A generic confirm/cancel modal with custom [content] and a slide/fade dismiss animation. - * - * Unlike [AlertDialog]/[PromptDialog]/[ChoiceDialog], [content] is fully custom rather than a - * fixed message layout. [net.kernelpanicsoft.archie.gui.layer.ModalScope.dismiss] is deferred until the close animation finishes so the modal - * doesn't disappear abruptly. - * - * @param title The dialog's header text. - * @param confirmText Label for the confirm button. - * @param cancelText Label for the cancel button. - * @param onConfirm Called immediately when the confirm button is pressed, before the close - * animation plays. - * @param onCancel Called immediately when the cancel button is pressed, before the close - * animation plays. - * @param content The dialog body, shown above the action row. - */ -@Composable -fun ModalScope.ConfirmDialog( - title: Component = Component.literal("Confirm Dialog"), - confirmText: Component = Component.literal("Confirm"), - cancelText: Component = Component.literal("Cancel"), - onConfirm: () -> Unit = {}, - onCancel: () -> Unit = {}, - content: @Composable () -> Unit -) -{ - val scope = rememberCoroutineScope() - var entered by remember { mutableStateOf(false) } - var closing by remember { mutableStateOf(false) } - LaunchedEffect(Unit) { entered = true } - - fun closeWithAnimation(action: () -> Unit) { - if (closing) return - closing = true - entered = false - action() - scope.launch { - delay(DIALOG_ANIMATION_MS.milliseconds) - dismiss() - } - } - - val offsetY = animateInt( - targetValue = if (entered) 0 else 8, - spec = AnimationSpec(durationMillis = DIALOG_ANIMATION_MS.milliseconds, easing = Easings.OutCubic), - ) - - Surface(modifier = Modifier.padding(4).offset(x = 0, y = offsetY)) { - Column(modifier = Modifier.margin(4)) { - Text( - text = title, - modifier = Modifier.margin(bottom = 4), - color = LocalTheme.current.darkTextColor, - dropShadow = false - ) - content() - Row( - modifier = Modifier.margin(top = 4), - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically - ) { - Button( - onClick = { closeWithAnimation(onConfirm) }, - enabled = !closing, - modifier = Modifier.sizeIn(minWidth = 50, minHeight = 20) - ) { Text(confirmText) } - Button( - onClick = { closeWithAnimation(onCancel) }, - enabled = !closing, - modifier = Modifier.sizeIn(minWidth = 50, minHeight = 20) - ) { Text(cancelText) } - } - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/DialogPrimitives.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/DialogPrimitives.kt deleted file mode 100644 index 5c9f56c76..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/DialogPrimitives.kt +++ /dev/null @@ -1,202 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.modal - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.setValue -import net.kernelpanicsoft.archie.gui.composables.basic.Text -import net.kernelpanicsoft.archie.gui.composables.containers.Surface -import net.kernelpanicsoft.archie.gui.composables.input.Button -import net.kernelpanicsoft.archie.gui.composables.input.textfield.BasicTextField -import net.kernelpanicsoft.archie.gui.layer.ModalScope -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.layout.Arrangement -import net.kernelpanicsoft.archie.gui.layout.Column -import net.kernelpanicsoft.archie.gui.layout.Row -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.position.margin -import net.kernelpanicsoft.archie.gui.modifiers.sizeIn -import net.kernelpanicsoft.archie.gui.modifiers.width -import net.kernelpanicsoft.archie.gui.theme.LocalTheme -import net.minecraft.network.chat.Component - -/** Value-label pair used by [ChoiceDialog]. */ -data class ModalChoice( - val value: T, - val label: Component, - val enabled: Boolean = true, -) - -/** Shared [Surface] layout (title, body, bottom action row) used by all built-in dialog composables. */ -@Composable -private fun ModalDialogScaffold( - title: Component, - modifier: Modifier = Modifier, - body: @Composable () -> Unit, - actions: @Composable () -> Unit, -) { - Surface(modifier = modifier) { - Column(modifier = Modifier.margin(4), verticalArrangement = Arrangement.spacedBy(4)) { - Text( - text = title, - color = LocalTheme.current.darkTextColor, - dropShadow = false, - ) - body() - Row( - horizontalArrangement = Arrangement.SpaceEvenly, - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.margin(top = 4), - ) { actions() } - } - } -} - -/** - * Simple one-action modal for acknowledgements and warnings. - * - * @param title The dialog's header text. - * @param message The body text explaining the alert. - * @param confirmText Label for the single dismiss button. - * @param onConfirm Called just before the modal dismisses itself. - */ -@Composable -fun ModalScope.AlertDialog( - title: Component, - message: Component, - confirmText: Component = Component.literal("OK"), - onConfirm: () -> Unit = {}, -) { - ModalDialogScaffold( - title = title, - modifier = Modifier.sizeIn(minWidth = 150, minHeight = 60), - body = { - Text(text = message, dropShadow = false, color = LocalTheme.current.darkTextColor) - }, - actions = { - Button(onClick = { - onConfirm() - dismiss() - }) { - Text(confirmText, dropShadow = false) - } - }, - ) -} - -/** - * Input modal with an inline text field and explicit confirm/cancel actions. - * - * @param title The dialog's header text. - * @param initialValue The text field's starting value. - * @param prompt Label text shown above the text field. - * @param confirmText Label for the confirm button. - * @param cancelText Label for the cancel button. - * @param validator The confirm button is only enabled while this returns `true` for the - * current field value. - * @param onConfirm Called with the field's value just before the modal dismisses itself. - * @param onCancel Called just before the modal dismisses itself via the cancel button. - */ -@Composable -fun ModalScope.PromptDialog( - title: Component, - initialValue: String = "", - prompt: Component = Component.literal("Enter a value:"), - confirmText: Component = Component.literal("Confirm"), - cancelText: Component = Component.literal("Cancel"), - validator: (String) -> Boolean = { true }, - onConfirm: (String) -> Unit, - onCancel: () -> Unit = {}, -) { - var value by remember(initialValue) { mutableStateOf(initialValue) } - - ModalDialogScaffold( - title = title, - modifier = Modifier.sizeIn(minWidth = 180, minHeight = 80), - body = { - Column(verticalArrangement = Arrangement.spacedBy(3)) { - Text(text = prompt, dropShadow = false, color = LocalTheme.current.darkTextColor) - BasicTextField( - value = value, - onValueChange = { value = it }, - modifier = Modifier.width(150), - ) - } - }, - actions = { - Button(onClick = { - onCancel() - dismiss() - }) { - Text(cancelText, dropShadow = false) - } - Button( - enabled = validator(value), - onClick = { - onConfirm(value) - dismiss() - }, - ) { - Text(confirmText, dropShadow = false) - } - }, - ) -} - -/** - * Multi-choice modal that maps each option in [choices] to its own button, plus one cancel - * action. - * - * @param title The dialog's header text. - * @param message Optional body text shown above the choice buttons. - * @param choices The selectable options, one button each, in order. - * @param cancelText Label for the cancel button. - * @param onSelected Called with the chosen value just before the modal dismisses itself. - * @param onCancel Called just before the modal dismisses itself via the cancel button. - */ -@Composable -fun ModalScope.ChoiceDialog( - title: Component, - message: Component? = null, - choices: List>, - cancelText: Component = Component.literal("Cancel"), - onSelected: (T) -> Unit, - onCancel: () -> Unit = {}, -) { - ModalDialogScaffold( - title = title, - modifier = Modifier.sizeIn(minWidth = 170, minHeight = 70), - body = { - Column(verticalArrangement = Arrangement.spacedBy(3)) { - if (message != null) { - Text(text = message, dropShadow = false, color = LocalTheme.current.darkTextColor) - } - Column(verticalArrangement = Arrangement.spacedBy(2)) { - choices.forEach { choice -> - Button( - enabled = choice.enabled, - modifier = Modifier.width(150), - onClick = { - onSelected(choice.value) - dismiss() - }, - ) { - Text(choice.label, dropShadow = false) - } - } - } - } - }, - actions = { - Button(onClick = { - onCancel() - dismiss() - }) { - Text(cancelText, dropShadow = false) - } - }, - ) -} - - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/TextureStates.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/TextureStates.kt deleted file mode 100644 index 26404864d..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/TextureStates.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.theme -import net.kernelpanicsoft.archie.gui.theme.ComposableTheme -import net.kernelpanicsoft.archie.gui.theme.ThemeState - -/** - * Constant keys used to look up [ThemeState] entries within a [ComposableTheme]'s state map. - * - * Composables use these keys to select the correct texture variant based on their current - * interactive state (e.g. hovered, pressed, disabled). - */ -object TextureStates { - /** The default idle state used when no other state applies. */ - const val DEFAULT = "default" - - /** Used when the composable is disabled and cannot be interacted with. */ - const val DISABLED = "disabled" - - /** Used when the mouse cursor is hovering over the composable. */ - const val HOVERED = "hovered" - - /** Used when the composable has been activated/checked/clicked (toggle state). */ - const val CLICKED = "clicked" - - /** Used when the composable is both activated and hovered simultaneously. */ - const val CLICKED_AND_HOVERED = "clicked_and_hovered" -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/WidgetState.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/WidgetState.kt deleted file mode 100644 index 962570da7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/WidgetState.kt +++ /dev/null @@ -1,84 +0,0 @@ -package net.kernelpanicsoft.archie.gui.composables.theme - -import net.kernelpanicsoft.archie.gui.theme.ComposableTheme - -/** - * Resolves a stateful composable's [TextureStates] key from an ordered set of independent - * boolean state axes (hovered, checked, pressed, ...), replacing the hand-written `when` chain - * every stateful composable (`Checkbox`, `Radio`, `Switch`, `Button`, `Slider`, `Tab`) used to - * maintain separately - which had drifted out of sync with each other (e.g. `Tab`'s combined - * hover state only fired via its `selected` axis, not its `pressed` axis, unlike every other - * component - see [resolve]'s "Tab" note). - * - * ### Example - * ```kotlin - * val stateKey = WidgetState.resolve( - * composableTheme, variant, - * WidgetState.clicked(checked), WidgetState.hovered(hovered), - * enabled = enabled, - * ) - * node.renderState = stateKey - * guiGraphics.drawThemeState(composableTheme.getState(stateKey, variant), x, y, node.width, node.height) - * ``` - */ -object WidgetState { - /** - * One named boolean axis of a widget's interaction state (e.g. "hovered" paired with - * whether the pointer currently is), in the priority order [resolve] should consider it - - * pass axes to [resolve] most-significant first (typically the "activated" axis - checked/ - * selected/pressed - before "hovered"). - */ - data class Axis(val name: String, val active: Boolean) - - /** An [Axis] for [TextureStates.HOVERED]. */ - fun hovered(active: Boolean) = Axis(TextureStates.HOVERED, active) - - /** An [Axis] for [TextureStates.CLICKED] - a checkbox/switch/radio's checked-or-selected state, a button/tab's pressed-or-selected state, or a slider's dragging state. */ - fun clicked(active: Boolean) = Axis(TextureStates.CLICKED, active) - - /** - * Resolves the [TextureStates] key for [theme]/[variant] given [enabled] and [axes] (in - * descending priority order - see [Axis]). - * - * - If [enabled] is `false`, returns [TextureStates.DISABLED] if [theme] defines it for - * [variant] (via [ComposableTheme.hasState]), otherwise falls through as if disabled - * weren't a factor - matching every existing chain's behavior of only branching on - * `!enabled` where a `disabled` theme state actually exists to show. - * - Otherwise, tries the most specific composite key first: every currently-active axis's - * [Axis.name], joined by `"_and_"` in priority order (e.g. `"clicked_and_hovered"` for - * [clicked]+[hovered] both active). If [theme] doesn't define that combination, falls - * back one axis at a time - by priority, i.e. trying each individual active axis's own - * key alone, highest priority first - stopping at the first one [theme] defines. - * - Returns [TextureStates.DEFAULT] if no active axis (alone or combined) has a defined - * state, or if no axis is active at all. - * - * This graceful per-axis fallback (rather than jumping straight from the full composite to - * [TextureStates.DEFAULT]) generalizes what `Button`'s chain alone used to do by hand - * (falling through a missing "clicked" state to "hovered" - `button.json` defines no - * "clicked" state at all) - every caller gets it for free, without needing its own - * `hasState` check. - * - * **Tab note:** `TabContainer.kt`'s old chain computed its "clicked" axis from - * `selected || isPressed`, but only paired it with `hovered` into the combined state when - * specifically `selected` was true - a pressed-but-unselected-and-hovered tab silently lost - * its hover visual. Callers migrating to this resolver should pass a single `clicked` axis - * (`selected || isPressed`) and a separate `hovered` axis as normal; [resolve] then treats - * both uniformly like every other component, which is a deliberate behavior fix, not an - * incidental one. - */ - fun resolve(theme: ComposableTheme, variant: String, vararg axes: Axis, enabled: Boolean = true): String { - if (!enabled && theme.hasState(TextureStates.DISABLED, variant)) return TextureStates.DISABLED - - val active = axes.filter { it.active } - if (active.isEmpty()) return TextureStates.DEFAULT - - val compositeKey = active.joinToString("_and_") { it.name } - if (theme.hasState(compositeKey, variant)) return compositeKey - - active.forEach { axis -> - if (theme.hasState(axis.name, variant)) return axis.name - } - - return TextureStates.DEFAULT - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemContainerMenu.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemContainerMenu.kt deleted file mode 100644 index 087e5aee9..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemContainerMenu.kt +++ /dev/null @@ -1,147 +0,0 @@ -package net.kernelpanicsoft.archie.gui.item - -import kotlinx.serialization.KSerializer -import net.kernelpanicsoft.archie.gui.ComposeContainerMenuBase -import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacket -import net.kernelpanicsoft.archie.gui.blockentity.toSerializedValue -import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel -import net.kernelpanicsoft.archie.serialization.NBTHolder -import net.minecraft.server.level.ServerPlayer -import net.minecraft.world.entity.player.Inventory -import net.minecraft.world.entity.player.Player -import net.minecraft.world.inventory.MenuType - -/** - * Base class for [net.minecraft.world.item.ItemStack]-backed Compose container menus - e.g. a - * backpack/bag with its own GUI. The [ComposeBlockContainerMenu][net.kernelpanicsoft.archie.gui.ComposeBlockContainerMenu] - * equivalent for items. - * - * See [ComposeContainerMenuBase] for slot pre-registration/positioning behavior, shared with - * [net.kernelpanicsoft.archie.gui.ComposeBlockContainerMenu] - this class only adds the - * item-specific pieces: locating the backing stack via [itemAccess], the [itemState] sync path, - * and periodic validity checking via [ItemStateManager]. - * - * ### Subclassing - * ```kotlin - * class MyBackpackMenu(id: Int, inventory: Inventory, access: ItemContainerAccess) : - * ComposeItemContainerMenu(MY_MENU_TYPE, id, inventory, access) { - * - * @Sync - * var progress by holder.intField() - * - * override fun registerSlotHandlers() { - * handler("inventory", holder.itemField(27)) - * } - * } - * ``` - * - * @param SELF The concrete menu subclass (self-referential for the [MenuType]). - * @param type The registered [MenuType] for this menu. - * @param id The container id assigned by the server. - * @param playerInventory The opening player's inventory. - * @param itemAccess Locates the backing [net.minecraft.world.item.ItemStack] and reports whether - * this menu should stay open. - */ -abstract class ComposeItemContainerMenu>( - type: MenuType, - id: Int, - playerInventory: Inventory, - protected val itemAccess: ItemContainerAccess, -) : ComposeContainerMenuBase(type, id, playerInventory), SyncedItemHolder { - - /** Client- and server-side sync state for this menu's own `@Sync`-annotated [holder] fields. */ - val itemState: ComposeItemState = ComposeItemState(containerId) - - /** - * An [NBTHolder] view of the backing stack, captured **once** at construction - mirroring - * [net.kernelpanicsoft.archie.gui.ComposeBlockContainerMenu]'s `tile` (stable for this menu's - * lifetime, not re-resolved per access). Declare this menu's own `@Sync`-annotated scalar - * fields against it, e.g. `@Sync var progress by holder.intField()`. - * - * Unlike [itemAccess]'s own `getStack()` (re-resolved fresh every call, since slot *contents* - * must always reflect the live inventory slot), this menu's own bookkeeping fields behave the - * same way [net.kernelpanicsoft.archie.gui.ComposeBlockContainerMenu]'s fields do against - * `tile` - captured once, not defended against the backing stack reference being swapped out - * from under an already-open menu. That's an unusual scenario that isn't defended against for - * block-entity-backed menus either (`tile` is captured the same way there). - */ - protected val holder: NBTHolder = NBTHolder.item(itemAccess.getStack()) - - /** Property names changed since the last [tickSync], with their serialized values ready to send. */ - private val dirtyUpdates = mutableMapOf() - - init - { - // Must run here, in this class's own init - not from ComposeContainerMenuBase's, which - // would dispatch into onMenuOpened() before `itemAccess` (this class's own constructor - // property) is actually assigned. See ComposeContainerMenuBase.onMenuOpened's KDoc. - onMenuOpened() - } - - override fun onMenuOpened() - { - if (!level.isClientSide) - ItemStateManager.register(this) - } - - override fun onMenuClosed(player: Player) - { - if (!level.isClientSide) - ItemStateManager.unregister(this) - } - - @Suppress("UNCHECKED_CAST") - override fun registerSyncedProperty(name: String, serializer: KSerializer) - { - // Runs on both sides, at field-declaration time - independent of observeItemProperty(), - // which only ever runs client-side inside a composable. Without this, the server never - // learns a serializer for `name` at all unless a fresh delegate's own initial-value write - // happens to fire onSyncedPropertyChanged first (which it doesn't for a property whose - // value already exists on an already-populated stack). - itemState.propertySerializers[name] = serializer as KSerializer - } - - override fun onSyncedPropertyChanged(name: String, serializer: KSerializer, value: T) - { - dirtyUpdates[name] = value.toSerializedValue(serializer) - } - - /** - * Applies a client-sent [ItemUpdatePacket] edit: a raw, low-level write straight into - * [holder]'s stored data (bypassing whatever property setter owns [name], the same way - * [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacketRegistry]'s serverbound - * handler calls `blockEntity.updateProperty(...)` rather than going through the property - * setter) - re-entering through [onSyncedPropertyChanged] here would only mark [name] dirty - * without ever actually persisting the new value, since that method is a notification hook, - * not a write path. Marks [name] dirty directly afterward so [tickSync] re-broadcasts it. - */ - internal fun applyRemoteUpdate(name: String, serializer: KSerializer, value: T) - { - holder.updateProperty(name, serializer, value) - dirtyUpdates[name] = value.toSerializedValue(serializer) - } - - /** - * Called once per server tick by [ItemStateManager]: force-closes this menu if [itemAccess] - * reports it's no longer valid, otherwise sends any accumulated [dirtyUpdates] as a single - * [ItemStatePacket]. - */ - internal fun tickSync(currentTick: Long) - { - if (!itemAccess.stillValid(player)) - { - player.closeContainer() - return - } - if (dirtyUpdates.isEmpty()) return - val packet = ItemStatePacket(containerId, dirtyUpdates.toMap(), currentTick) - dirtyUpdates.clear() - (player as? ServerPlayer)?.let { ArchieNetworkChannel.toPlayers(listOf(it), packet) } - } - - override fun stillValid(player: Player): Boolean = itemAccess.stillValid(player) - - /** Freezes the backpack's own slot in the player's inventory while its GUI is open - see [ComposeContainerMenuBase.isPlayerSlotExcluded]. */ - override fun isPlayerSlotExcluded(index: Int): Boolean = - (itemAccess as? PlayerInventoryItemAccess)?.slot == index -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemState.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemState.kt deleted file mode 100644 index 1153b9514..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemState.kt +++ /dev/null @@ -1,145 +0,0 @@ -package net.kernelpanicsoft.archie.gui.item - -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.mutableStateOf -import kotlinx.serialization.KSerializer -import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacket -import net.kernelpanicsoft.archie.gui.blockentity.deserialize -import net.kernelpanicsoft.archie.gui.blockentity.toSerializedValue -import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel - -/** - * Client- and server-side state holder for a [ComposeItemContainerMenu]'s synchronized - * properties. The [ComposeBlockEntityState][net.kernelpanicsoft.archie.gui.blockentity.ComposeBlockEntityState] - * equivalent for item-backed menus, keyed by [containerId] instead of a [net.minecraft.core.BlockPos]. - * - * Unlike block entities - which can be watched by multiple players simultaneously, needing a - * position-keyed global registry on both sides - an item-backed menu is inherently 1:1 with a - * single player's currently-open session. Client and server each already have exactly one live - * instance of "my currently open menu" (this one, owned directly by the [ComposeItemContainerMenu] - * itself), so no equivalent client-side registry is needed here. - * - * @param containerId The owning menu's vanilla [net.minecraft.world.inventory.AbstractContainerMenu.containerId] - - * used purely as a staleness guard against a stray packet arriving after this player closed one - * item menu and opened another, not as a lookup key. - */ -class ComposeItemState( - val containerId: Int, -) { - /** Map of property names to their Compose state values */ - val propertyStates = mutableMapOf>() - - /** Serializers used to encode/decode each observed property, keyed by property name. */ - val propertySerializers = mutableMapOf>() - - @Suppress("UNCHECKED_CAST") - private fun anySerializer(serializer: KSerializer): KSerializer = serializer as KSerializer - - @Suppress("UNCHECKED_CAST") - private fun typedSerializer(propertyName: String): KSerializer? = propertySerializers[propertyName] as? KSerializer - - @Suppress("UNCHECKED_CAST") - private fun getOrCreateState(propertyName: String, initialValue: T?): MutableState { - return propertyStates.computeIfAbsent(propertyName) { - PropertyState(this, propertyName, mutableStateOf(initialValue)) as MutableState - } as MutableState - } - - /** - * Gets or creates a Compose state for a property with a specific type. - * - * @param propertyName The name of the property. - * @param initialValue The initial value (optional, defaults to null). - * @param T The expected type of the property. - * @return A [MutableState] of type T that can be observed in composables. - */ - fun observeProperty( - propertyName: String, - serializer: KSerializer, - initialValue: T? = null, - ): MutableState { - propertySerializers[propertyName] = anySerializer(serializer) - return getOrCreateState(propertyName, initialValue) - } - - /** - * A [MutableState] delegate that forwards writes to [ComposeItemState.sendUpdatedProperty], - * so setting [value] from a composable both updates local state and pushes the change to the server. - */ - class PropertyState(private val state: ComposeItemState, private val propertyName: String, internal val mutableState: MutableState) : MutableState by mutableState - { - override var value: T - get() = mutableState.value - set(value) - { - mutableState.value = value - state.sendUpdatedProperty(propertyName, value) - } - } - - /** - * Updates a property value from a network packet. - * - * If the property doesn't exist yet, it will be created. - * - * @param propertyName The name of the property. - * @param value The new serialized value from the network packet. - */ - fun updateProperty(propertyName: String, value: BlockEntityStatePacket.SerializedValue) { - val deserializedValue = value.deserialize(propertySerializers[propertyName]) - // Must go through getOrCreateState(), not a separate computeIfAbsent - otherwise a - // property whose first appearance is a packet (not observeProperty()) gets stuck with a - // bare state that never forwards writes back to the server. - getOrCreateState(propertyName, deserializedValue).value = deserializedValue - } - - /** - * Updates a property value and sends the change to the server. - * - * This method should be called when a client-side interaction changes a property. - * - * @param propertyName The name of the property. - * @param value The new value. - */ - fun sendUpdatedProperty(propertyName: String, value: T) { - val serializer = typedSerializer(propertyName) ?: run { - println("No serializer found for property $propertyName. Cannot send update to server.") - return - } - - val serializedValue = value.toSerializedValue(serializer) - val packet = ItemUpdatePacket.singleUpdate(containerId, propertyName, serializedValue) - ArchieNetworkChannel.toServer(packet) - } - - /** - * Gets the current value of a property. - * - * @param propertyName The name of the property. - * @return The property value, or null if not tracked. - */ - fun getProperty(propertyName: String): Any? { - return propertyStates[propertyName]?.value - } - - /** - * Gets the current value of a property with type casting. - * - * @param propertyName The name of the property. - * @param T The expected type. - * @return The property value cast to T, or null if not found/wrong type. - */ - @Suppress("UNCHECKED_CAST") - fun getPropertyTyped(propertyName: String): T? { - return propertyStates[propertyName]?.value as? T - } - - /** - * Gets all currently tracked properties. - * - * @return A map of property names to their current values. - */ - fun getAllProperties(): Map { - return propertyStates.mapValues { (_, state) -> state.value } - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemContainerAccess.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemContainerAccess.kt deleted file mode 100644 index ce38c1bb8..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemContainerAccess.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.kernelpanicsoft.archie.gui.item - -import net.minecraft.world.entity.player.Player -import net.minecraft.world.item.Item -import net.minecraft.world.item.ItemStack - -/** - * Locates the [ItemStack] backing a [ComposeItemContainerMenu] and reports whether it's still - * valid to keep the menu open. - */ -interface ItemContainerAccess -{ - /** - * Resolves the current backing [ItemStack]. Must be re-resolved fresh on every call, not - * cached - the underlying stack reference can be swapped out from under the menu (e.g. by - * another mod replacing the inventory slot's stack wholesale), and a cached reference would - * silently go stale rather than reflect that. - */ - fun getStack(): ItemStack - - /** Whether [player] should still be allowed to keep this menu open. */ - fun stillValid(player: Player): Boolean -} - -/** - * An [ItemContainerAccess] for an item sitting in [player]'s own inventory at [slot] (vanilla - * [net.minecraft.world.entity.player.Inventory] numbering: hotbar 0-8, main 9-35). - * - * @param expectedItem Guards [stillValid] against the slot's contents having been swapped out - * for a different item entirely (e.g. dropped and something else picked up into the same - * slot index) while the menu was open. - */ -class PlayerInventoryItemAccess( - private val player: Player, - val slot: Int, - private val expectedItem: Item, -) : ItemContainerAccess -{ - override fun getStack(): ItemStack = player.inventory.getItem(slot) - - override fun stillValid(player: Player): Boolean = - player === this.player && getStack().`is`(expectedItem) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateComposables.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateComposables.kt deleted file mode 100644 index f35ea05ef..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateComposables.kt +++ /dev/null @@ -1,49 +0,0 @@ -package net.kernelpanicsoft.archie.gui.item - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.MutableState -import androidx.compose.runtime.compositionLocalOf -import kotlinx.serialization.serializer - -/** - * Provides the current [ComposeItemContainerMenu]'s state to composables in the composition - * tree - the [ComposeItemState] equivalent of - * [net.kernelpanicsoft.archie.gui.blockentity.LocalBlockEntityState]. `null` when the current - * screen's menu isn't item-backed. - * - * Use with `LocalItemState.current` to access the state, or use the [observeItemProperty] helper - * for convenience. - */ -val LocalItemState = compositionLocalOf { null } - -/** - * Observes a property on the current [ComposeItemContainerMenu] in the current composition - * context. The [net.kernelpanicsoft.archie.gui.blockentity.observeProperty] equivalent for - * item-backed menus. - * - * Returns a [MutableState] that automatically triggers recomposition when the property changes. - * Must be called where [LocalItemState] has been provided with a non-null value (i.e. inside an - * item-backed menu's screen composition) - otherwise it throws. - * - * ### Example - * ```kotlin - * @Composable - * fun MyComponent() { - * val progressState = observeItemProperty("progress") - * Text("Progress: ${progressState.value}") - * } - * ``` - * - * @param propertyName The name of the property to observe. - * @param T The expected type of the property. - * @return A [MutableState] of type T reflecting the property's current value. - * @throws RuntimeException if no [ComposeItemState] is available in the current composition. - */ -@Composable -inline fun observeItemProperty( - propertyName: String, - initialValue: T? = null, -): MutableState { - val state = LocalItemState.current ?: throw RuntimeException("No item container state available in composition") - return state.observeProperty(propertyName, serializer(), initialValue) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateManager.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateManager.kt deleted file mode 100644 index 072da51c7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateManager.kt +++ /dev/null @@ -1,53 +0,0 @@ -package net.kernelpanicsoft.archie.gui.item - -import dev.architectury.event.events.common.TickEvent -import org.slf4j.LoggerFactory -import java.util.concurrent.CopyOnWriteArraySet - -private val LOGGER = LoggerFactory.getLogger(ItemStateManager::class.java) - -/** - * Server-side manager for currently-open [ComposeItemContainerMenu]s: drives dirty-property sync - * packets each tick (mirroring [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager]), - * and force-closes a menu whose [ItemContainerAccess] reports it's no longer valid - e.g. the - * backing item was consumed/dropped while the GUI was passively open. Vanilla's own `stillValid` - * polling only fires reactively on player-initiated clicks otherwise, so without this a stale - * menu could sit open indefinitely against a stack that no longer exists. - * - * Much lighter than [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager]: an - * item-backed menu is inherently 1:1 with one player's session (unlike a block entity, which can - * be watched by multiple players simultaneously), so there's no position-keyed registry or - * per-menu tracked-player set needed - just the set of currently-open menus themselves. - */ -object ItemStateManager { - private val openMenus: MutableSet> = CopyOnWriteArraySet() - - /** - * Registers the server tick listener that drives per-tick syncing/validity checks. - * - * Must be called once during mod init. - */ - fun init() { - TickEvent.SERVER_POST.register { - val currentTick = it.tickCount.toLong() - // One menu's tickSync() throwing shouldn't abort the loop for every other open menu. - openMenus.forEach { menu -> - try { - menu.tickSync(currentTick) - } catch (e: Exception) { - LOGGER.error("Error syncing item container menu $menu", e) - } - } - } - } - - /** Registers [menu] for tick-driven syncing. Called from [ComposeItemContainerMenu.onMenuOpened]. */ - fun register(menu: ComposeItemContainerMenu<*>) { - openMenus += menu - } - - /** Unregisters [menu]. Called from [ComposeItemContainerMenu.onMenuClosed]. */ - fun unregister(menu: ComposeItemContainerMenu<*>) { - openMenus -= menu - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacket.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacket.kt deleted file mode 100644 index dd8a2eaea..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacket.kt +++ /dev/null @@ -1,38 +0,0 @@ -package net.kernelpanicsoft.archie.gui.item - -import kotlinx.serialization.Serializable -import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacket - -/** - * A network packet that carries [ComposeItemContainerMenu] state changes from server to client - - * the item-backed-menu equivalent of [BlockEntityStatePacket]. [containerId] addresses the - * player's currently open menu directly rather than acting as a lookup key: an item-backed menu - * is inherently 1:1 with one player's session, so there's no position-keyed registry to look - * anything up in, unlike a block entity that can be watched by multiple players at once. It's - * checked purely as a staleness guard against a stray packet arriving after this player closed - * one item menu and opened another. - * - * @property containerId The owning menu's vanilla [net.minecraft.world.inventory.AbstractContainerMenu.containerId]. - * @property updates A map of property names to their serialized values. - * @property timestamp Server tick when this packet was created (for ordering/deduplication). - */ -@Serializable -data class ItemStatePacket( - val containerId: Int, - val updates: Map = emptyMap(), - val timestamp: Long = 0, -) { - companion object { - /** Creates a new packet with a single property update. */ - fun singleUpdate( - containerId: Int, - propertyName: String, - value: BlockEntityStatePacket.SerializedValue, - timestamp: Long = 0, - ): ItemStatePacket = ItemStatePacket( - containerId = containerId, - updates = mapOf(propertyName to value), - timestamp = timestamp, - ) - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacketRegistry.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacketRegistry.kt deleted file mode 100644 index 0145956d8..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacketRegistry.kt +++ /dev/null @@ -1,42 +0,0 @@ -package net.kernelpanicsoft.archie.gui.item - -import kotlinx.serialization.KSerializer -import net.kernelpanicsoft.archie.gui.blockentity.deserialize -import net.kernelpanicsoft.archie.networking.ArchieNetworkChannel - -/** - * Registers [ItemStatePacket]/[ItemUpdatePacket] handlers with [ArchieNetworkChannel] - the - * [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacketRegistry] equivalent for - * item-backed menus. - * - * Unlike the block-entity path, routing needs no position-keyed lookup on either side - an - * item-backed menu is inherently 1:1 with one player's session, so "the current menu" is always - * just `context.player.containerMenu` (true on both sides: [net.minecraft.world.entity.player.Player] - * has exactly one open [net.minecraft.world.inventory.AbstractContainerMenu] at a time). The - * `containerId` on each packet is checked purely as a staleness guard against a stray packet - * arriving after the player closed one item menu and opened another - a mismatch is silently - * dropped, not an error. - */ -object ItemStatePacketRegistry { - /** Registers the clientbound and serverbound packet handlers described above. */ - fun register() { - ArchieNetworkChannel.clientbound { packet, context -> - val menu = context.player.containerMenu as? ComposeItemContainerMenu<*> ?: return@clientbound - if (menu.containerId != packet.containerId) return@clientbound - packet.updates.forEach { (propertyName, value) -> - menu.itemState.updateProperty(propertyName, value) - } - } - - ArchieNetworkChannel.serverbound { packet, context -> - val menu = context.player.containerMenu as? ComposeItemContainerMenu<*> ?: return@serverbound - if (menu.containerId != packet.containerId) return@serverbound - packet.updates.forEach { (propertyName, serializedValue) -> - val serializer = menu.itemState.propertySerializers[propertyName] ?: return@forEach - val deserializedValue = serializedValue.deserialize(serializer) - @Suppress("UNCHECKED_CAST") - menu.applyRemoteUpdate(propertyName, serializer as KSerializer, deserializedValue) - } - } - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemUpdatePacket.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemUpdatePacket.kt deleted file mode 100644 index ca00f99b3..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemUpdatePacket.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.kernelpanicsoft.archie.gui.item - -import kotlinx.serialization.Serializable -import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacket - -/** - * A network packet that carries [ComposeItemContainerMenu] state updates from client to server - - * the item-backed-menu equivalent of [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityUpdatePacket]. - * See [ItemStatePacket] for what [containerId] is used for. - * - * @property containerId The owning menu's vanilla [net.minecraft.world.inventory.AbstractContainerMenu.containerId]. - * @property updates A map of property names to their serialized values. - */ -@Serializable -data class ItemUpdatePacket( - val containerId: Int, - val updates: Map, -) { - companion object { - /** Creates a new packet with a single property update. */ - fun singleUpdate( - containerId: Int, - propertyName: String, - value: BlockEntityStatePacket.SerializedValue, - ): ItemUpdatePacket = ItemUpdatePacket( - containerId = containerId, - updates = mapOf(propertyName to value), - ) - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/SyncedItemHolder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/SyncedItemHolder.kt deleted file mode 100644 index e89d4a89b..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/SyncedItemHolder.kt +++ /dev/null @@ -1,33 +0,0 @@ -package net.kernelpanicsoft.archie.gui.item - -import kotlinx.serialization.KSerializer - -/** - * Implemented by whatever owns an [net.kernelpanicsoft.archie.serialization.NBTHolder.item]-backed - * holder that wants its `@Sync`-annotated fields to actually push updates somewhere, mirroring - * what [net.minecraft.world.level.block.entity.BlockEntity] gets automatically via - * [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager]. - * - * [net.kernelpanicsoft.archie.serialization.ItemStackNBTHolderImpl] checks for this on its - * `thisRef` the same way it checks `thisRef is BlockEntity` for the block-entity-backed - * implementation - so any `NBTHolder.item(stack)`-delegated property declared directly on a type - * implementing this interface gets [registerSyncedProperty]/[onSyncedPropertyChanged] calls - * automatically. - */ -interface SyncedItemHolder -{ - /** - * Called once, at property-declaration time, for every `@Sync`-annotated `NBTHolder.item`- - * delegated property named [name] - independent of whether its value has ever actually been - * written. Needed so a serializer is available to decode an incoming edit even for a property - * whose value came from an *existing* stack's already-populated data (where the delegate's own - * initial-value write, which [onSyncedPropertyChanged] would otherwise piggyback on, never - * runs) - mirrors why [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateContainer] - * has its own separate `setPropertySerializer` call distinct from `updateProperty`. No-op by - * default for implementations that don't need it. - */ - fun registerSyncedProperty(name: String, serializer: KSerializer) {} - - /** Called on every write to a `@Sync`-annotated `NBTHolder.item`-delegated property named [name]. */ - fun onSyncedPropertyChanged(name: String, serializer: KSerializer, value: T) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/Layer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/Layer.kt deleted file mode 100644 index e2e031c30..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/Layer.kt +++ /dev/null @@ -1,53 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layer - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Composition -import androidx.compose.runtime.CompositionContext -import net.kernelpanicsoft.archie.gui.layout.LayoutNode -import net.kernelpanicsoft.archie.gui.nodes.LayoutNodeApplier -import java.util.* - -/** - * A self-contained UI layer with its own independent [LayoutNode] tree and [Composition]. - * - * Layers are used to implement overlapping UI surfaces such as modals, dropdowns, and - * tooltips. Each layer has its own root node that is measured and rendered separately - * from the base screen content. - * - * Layers are managed by [LayerStackManager]. Do not create or dispose [Layer] instances - * directly; use [LayerStackManager.push] or [LayerStackManager.modal] instead. - * - * @property id Unique identifier for this layer, used for removal. - * @property rootNode The root [LayoutNode] of this layer's composition tree. - * @property composition The Compose [Composition] backing this layer. - */ -class Layer( - val id: UUID = UUID.randomUUID(), - depth: Int, - parentComposition: CompositionContext, - content: @Composable () -> Unit, -) { - val rootNode = LayoutNode("Root").apply { layer = depth } - - /** The `"RootContainer"` node under [rootNode], if one has been composed. */ - val rootContainerNode by lazy { rootNode.findNode("RootContainer") } - - /** Finds a descendant of [rootNode] by name. See [LayoutNode.findNode]. */ - fun findNode(name: String): LayoutNode? = rootNode.findNode(name) - - - - val composition = Composition(LayoutNodeApplier(rootNode), parentComposition) - - init { - composition.setContent(content) - } - - /** - * Disposes the Compose [Composition] associated with this layer, releasing all - * remembered state and coroutines. - * - * Called automatically by [LayerStackManager.pop] and [LayerStackManager.popById]. - */ - fun dispose() = composition.dispose() -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/LayerStackManager.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/LayerStackManager.kt deleted file mode 100644 index ffd26cd2f..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/LayerStackManager.kt +++ /dev/null @@ -1,382 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layer - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.Composition -import androidx.compose.runtime.CompositionContext -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.LaunchedEffect -import androidx.compose.runtime.compositionLocalOf -import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateListOf -import androidx.compose.runtime.mutableStateOf -import androidx.compose.runtime.remember -import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue -import net.kernelpanicsoft.archie.gui.animation.AnimationSpec -import net.kernelpanicsoft.archie.gui.animation.Easing -import net.kernelpanicsoft.archie.gui.animation.Easings -import net.kernelpanicsoft.archie.gui.animation.animateFloat -import net.kernelpanicsoft.archie.gui.animation.animateInt -import net.kernelpanicsoft.archie.gui.composables.containers.RootContainer -import net.kernelpanicsoft.archie.gui.composables.modal.AlertDialog -import net.kernelpanicsoft.archie.gui.composables.modal.ChoiceDialog -import net.kernelpanicsoft.archie.gui.composables.modal.ConfirmDialog -import net.kernelpanicsoft.archie.gui.composables.modal.ModalChoice -import net.kernelpanicsoft.archie.gui.composables.modal.PromptDialog -import net.kernelpanicsoft.archie.gui.layout.Box -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.layout.IntCoordinates -import net.kernelpanicsoft.archie.gui.layout.Size -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.appearance.background -import net.kernelpanicsoft.archie.gui.modifiers.fillMaxSize -import net.kernelpanicsoft.archie.gui.modifiers.input.PointerEventType -import net.kernelpanicsoft.archie.gui.modifiers.input.onPointerEvent -import net.kernelpanicsoft.archie.gui.modifiers.position.offset -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.minecraft.network.chat.Component -import java.util.* -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch -import net.kernelpanicsoft.archie.gui.modifiers.position.zIndex -import kotlin.math.max -import kotlin.math.min -import kotlin.math.roundToInt -import kotlin.time.Duration -import kotlin.time.Duration.Companion.milliseconds - -/** - * Provides the nearest [LayerStackManager] to composables inside a [net.kernelpanicsoft.archie.gui.ComposeScreen] - * or [net.kernelpanicsoft.archie.gui.ComposeContainerScreen]. - * - * Access via `LocalLayerManager.current` to push new overlay layers. - */ -val LocalLayerManager = compositionLocalOf { - error("No LayerManager provided. Are you inside a ComposeScreen?") -} - -/** The depth index of the currently composed layer (base layer is `0`). */ -val LocalLayerDepth = compositionLocalOf { 0 } - -/** - * Receiver scope for modal layer content, exposing a way to close the modal. - */ -interface ModalScope { - /** - * Dismisses (removes) the modal layer that owns this scope. - */ - fun dismiss() -} - -/** Transition defaults applied to every modal pushed through [LayerStackManager.modal]. */ -data class ModalTransitionSpec( - val durationMillis: Duration = 180.milliseconds, - val easing: Easing = Easings.OutCubic, - val enterOffsetY: Int = 8, - val maxBackdropAlpha: Int = 132, -) - -/** - * Manages an ordered stack of [Layer]s for a single screen. - * - * The stack determines the rendering order (bottom to top) and input-dispatch priority - * (top layer receives events first). Overlays such as dialogs, dropdowns, and tooltips - * are each their own layer on top of the base screen content. - * - * Obtain an instance via the [LocalLayerManager] composition local. - * - * @param parentComposition The [CompositionContext] from the host screen, required - * when creating child [Composition]s for each layer. - */ -class LayerStackManager(private val parentComposition: CompositionContext) { - - /** The ordered list of active layers. Layers are rendered bottom-to-top. */ - val layers = mutableStateListOf() - - /** - * Represents the total size of the screen, calculated based on the dimensions of all active layers. - * - * This property computes the maximum width and height among all the root container nodes - * from the layers managed by the containing class. It aggregates these dimensions by traversing - * the active layers and comparing their widths and heights. - * - * If a layer does not have a root container node, it is skipped in the calculation. - * - * @return A [Size] object representing the combined width and height required to encapsulate - * all visible layers. - */ - val screenSize: Size - get() = layers.fold(Size(0, 0)) { acc, layer -> - val node = layer.rootContainerNode ?: return@fold acc - Size(max(node.width, acc.width), max(node.height, acc.height)) - } - - /** - * Represents the top-left position of the screen, calculated based on the root container - * nodes of all active layers within the layer stack. - * - * The result aggregates the minimum x and y coordinates across all layers. If no root - * container nodes are found, the default position is (0, 0). - * - * The position is determined by folding over all layers and comparing the x and y positions - * of their root container nodes, if present. The computation ensures that the resulting - * coordinates account for the smallest bounds of the visible layers in the stack. - */ - val screenPos: IntCoordinates - get() = layers.fold(null) { acc, layer -> - val node = layer.rootContainerNode ?: return@fold acc - IntCoordinates(min(acc?.x ?: node.x, node.x), min(acc?.y ?: node.y, node.y)) - } ?: IntCoordinates(0, 0) - - /** - * Pushes a new generic layer onto the stack. - * - * The content lambda receives a `dismiss` function it can call to remove itself - * from the stack. This overload is suitable for persistent overlays and custom - * layer types. - * - * @param layerContent The composable content for the new layer. - * @return A dismiss handle; call it to imperatively remove the layer. - */ - fun push(layerContent: @Composable (dismiss: () -> Unit) -> Unit): () -> Unit { - val layerId = UUID.randomUUID() - val layerDepth = layers.size - val layer = Layer(id = layerId, parentComposition = parentComposition, depth = layerDepth) { - CompositionLocalProvider(LocalLayerDepth provides layerDepth) { - layerContent { popById(layerId) } - } - } - layers.add(layer) - return { popById(layerId) } - } - - /** - * Pushes a new modal layer onto the stack. - * - * Modals are opinionated, input-blocking overlays ideal for dialogs and confirmation - * prompts. A click outside the modal content area triggers [onDismissRequest] and, - * when [dismissOnClickOutside] is `true`, automatically removes the layer. - * - * @param alignment Alignment of the modal within the full screen. Default [Alignment.Center]. - * @param dismissOnClickOutside Whether clicking outside the modal content closes it. - * @param onDismissRequest Optional callback invoked when the modal is dismissed. - * @param content The modal UI, a composable lambda with [ModalScope] receiver. - */ - fun modal( - alignment: Alignment = Alignment.Center, - dismissOnClickOutside: Boolean = true, - transitionSpec: ModalTransitionSpec = ModalTransitionSpec(), - onDismissRequest: () -> Unit = {}, - content: @Composable ModalScope.() -> Unit, - ) { - push { popLayer -> - var entered by remember { mutableStateOf(false) } - var closing by remember { mutableStateOf(false) } - val closeScope = rememberCoroutineScope() - val progress = animateFloat( - targetValue = if (entered) 1f else 0f, - spec = AnimationSpec(durationMillis = transitionSpec.durationMillis, easing = transitionSpec.easing), - ) - - fun requestDismiss() { - if (closing) return - closing = true - entered = false - onDismissRequest() - closeScope.launch { - delay(transitionSpec.durationMillis) - popLayer() - } - } - - val scope = object : ModalScope { - override fun dismiss() = requestDismiss() - } - - LaunchedEffect(Unit) { - entered = true - } - - ModalLayout( - alignment = alignment, - dismissOnClickOutside = dismissOnClickOutside, - onDismissRequest = ::requestDismiss, - transitionSpec = transitionSpec, - transitionProgress = progress, - content = { scope.content() }, - ) - } - } - - /** - * Pushes a modal presenting a [ConfirmDialog] with confirm/cancel actions. The modal - * animates out and dismisses itself after either action runs. - * - * @param onConfirm Invoked when the user confirms. - * @param onCancel Invoked when the user cancels. - * @param content Additional body content shown above the actions. - */ - fun confirmDialog( - title: Component = Component.literal("Confirm Dialog"), - confirmText: Component = Component.literal("Confirm"), - cancelText: Component = Component.literal("Cancel"), - onConfirm: () -> Unit = {}, - onCancel: () -> Unit = {}, - content: @Composable () -> Unit - ) { - modal( - dismissOnClickOutside = false - ) { - - ConfirmDialog( - title = title, - confirmText = confirmText, - cancelText = cancelText, - onConfirm = onConfirm, - onCancel = onCancel, - content = content - ) - } - } - - /** - * Pushes a modal presenting an [AlertDialog] with a single acknowledgement action. - * - * @param onConfirm Invoked when the user acknowledges the alert. - */ - fun alertDialog( - title: Component = Component.literal("Alert"), - message: Component, - confirmText: Component = Component.literal("OK"), - onConfirm: () -> Unit = {}, - ) { - modal(dismissOnClickOutside = false) { - AlertDialog( - title = title, - message = message, - confirmText = confirmText, - onConfirm = onConfirm, - ) - } - } - - /** - * Pushes a modal presenting a [PromptDialog] for single-line text input. - * - * @param initialValue Text prefilled in the input field. - * @param validator Predicate controlling whether the confirm action is enabled. - * @param onConfirm Invoked with the entered text when the user confirms. - * @param onCancel Invoked when the user cancels. - */ - fun promptDialog( - title: Component = Component.literal("Enter Value"), - initialValue: String = "", - prompt: Component = Component.literal("Enter a value:"), - confirmText: Component = Component.literal("Confirm"), - cancelText: Component = Component.literal("Cancel"), - validator: (String) -> Boolean = { true }, - onConfirm: (String) -> Unit, - onCancel: () -> Unit = {}, - ) { - modal(dismissOnClickOutside = false) { - PromptDialog( - title = title, - initialValue = initialValue, - prompt = prompt, - confirmText = confirmText, - cancelText = cancelText, - validator = validator, - onConfirm = onConfirm, - onCancel = onCancel, - ) - } - } - - /** - * Pushes a modal presenting a [ChoiceDialog] listing [choices] for the user to pick from. - * - * @param choices The selectable options. - * @param onSelected Invoked with the chosen value's [ModalChoice.value] when a choice is picked. - * @param onCancel Invoked when the user cancels without choosing. - */ - fun choiceDialog( - title: Component = Component.literal("Choose an Option"), - message: Component? = null, - choices: List>, - cancelText: Component = Component.literal("Cancel"), - onSelected: (T) -> Unit, - onCancel: () -> Unit = {}, - ) { - modal(dismissOnClickOutside = false) { - ChoiceDialog( - title = title, - message = message, - choices = choices, - cancelText = cancelText, - onSelected = onSelected, - onCancel = onCancel, - ) - } - } - - /** - * Removes and disposes the topmost layer. - */ - fun pop() = layers.removeLastOrNull()?.dispose() - - /** - * Removes and disposes the layer identified by [id]. - * - * Does nothing if no layer with that id exists. - * - * @param id The [UUID] of the layer to remove. - */ - fun popById(id: UUID) { - val layer = layers.find { it.id == id } ?: return - layer.dispose() - layers.remove(layer) - } - - /** - * The topmost (most recently pushed) layer, which receives input events first. - * `null` if the stack is empty. - */ - val top: Layer? get() = layers.lastOrNull() - - @Composable - private fun ModalLayout( - alignment: Alignment, - onDismissRequest: () -> Unit, - dismissOnClickOutside: Boolean, - transitionSpec: ModalTransitionSpec, - transitionProgress: Float, - content: @Composable () -> Unit, - ) { - val alpha = animateInt( - targetValue = (transitionSpec.maxBackdropAlpha * transitionProgress.coerceIn(0f, 1f)).roundToInt(), - spec = AnimationSpec(durationMillis = transitionSpec.durationMillis, easing = transitionSpec.easing), - ) - val offsetY = animateInt( - targetValue = ((1f - transitionProgress.coerceIn(0f, 1f)) * transitionSpec.enterOffsetY).roundToInt(), - spec = AnimationSpec(durationMillis = transitionSpec.durationMillis, easing = transitionSpec.easing), - ) - var rootModifier = Modifier.fillMaxSize() - .background((alpha.coerceIn(0, 255) shl 24)) - if (dismissOnClickOutside) { - rootModifier = rootModifier.onPointerEvent(PointerEventType.PRESS) { _, event -> - onDismissRequest() - event.consume() - } - } - Box(modifier = rootModifier, contentAlignment = alignment) { - RootContainer( - modifier = Modifier - .offset(x = 0, y = offsetY) - .onPointerEvent(PointerEventType.PRESS) { _, event -> event.consume() } - .zIndex(1f) - ) { - content() - } - } - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Alignment.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Alignment.kt deleted file mode 100644 index 1708e411c..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Alignment.kt +++ /dev/null @@ -1,275 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.kernelpanicsoft.archie.gui.layout - -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import kotlin.math.roundToInt - -/** - * An interface to calculate the position of a sized box inside an available space. [Alignment] is - * often used to define the alignment of a layout inside a parent layout. - * - * @see AbsoluteAlignment - * @see BiasAlignment - * @see BiasAbsoluteAlignment - */ -@Stable -fun interface Alignment { - /** - * Calculates the position of a box of size [size] relative to the top left corner of an area - * of size [space]. The returned offset can be negative or larger than `space - size`, - * meaning that the box will be positioned partially or completely outside the area. - */ - fun align(size: IntSize, space: IntSize, layoutDirection: LayoutDirection): IntOffset - - /** - * An interface to calculate the position of box of a certain width inside an available width. - * [Alignment.Horizontal] is often used to define the horizontal alignment of a layout inside a - * parent layout. - */ - @Stable - fun interface Horizontal { - /** - * Calculates the horizontal position of a box of width [size] relative to the left - * side of an area of width [space]. The returned offset can be negative or larger than - * `space - size` meaning that the box will be positioned partially or completely outside - * the area. - */ - fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int - } - - /** - * An interface to calculate the position of a box of a certain height inside an available - * height. [Alignment.Vertical] is often used to define the vertical alignment of a - * layout inside a parent layout. - */ - @Stable - fun interface Vertical { - /** - * Calculates the vertical position of a box of height [size] relative to the top edge of - * an area of height [space]. The returned offset can be negative or larger than - * `space - size` meaning that the box will be positioned partially or completely outside - * the area. - */ - fun align(size: Int, space: Int): Int - } - - /** - * A collection of common [Alignment]s aware of layout direction. - */ - companion object { - // 2D Alignments. - @Stable - val TopStart: Alignment = BiasAlignment(-1f, -1f) - @Stable - val TopCenter: Alignment = BiasAlignment(0f, -1f) - @Stable - val TopEnd: Alignment = BiasAlignment(1f, -1f) - @Stable - val CenterStart: Alignment = BiasAlignment(-1f, 0f) - @Stable - val Center: Alignment = BiasAlignment(0f, 0f) - @Stable - val CenterEnd: Alignment = BiasAlignment(1f, 0f) - @Stable - val BottomStart: Alignment = BiasAlignment(-1f, 1f) - @Stable - val BottomCenter: Alignment = BiasAlignment(0f, 1f) - @Stable - val BottomEnd: Alignment = BiasAlignment(1f, 1f) - - // 1D Alignment.Verticals. - @Stable - val Top: Vertical = BiasAlignment.Vertical(-1f) - @Stable - val CenterVertically: Vertical = BiasAlignment.Vertical(0f) - @Stable - val Bottom: Vertical = BiasAlignment.Vertical(1f) - - // 1D Alignment.Horizontals. - @Stable - val Start: Horizontal = BiasAlignment.Horizontal(-1f) - @Stable - val CenterHorizontally: Horizontal = BiasAlignment.Horizontal(0f) - @Stable - val End: Horizontal = BiasAlignment.Horizontal(1f) - } -} - -/** - * A collection of common [Alignment]s unaware of the layout direction. - */ -object AbsoluteAlignment { - // 2D AbsoluteAlignments. - @Stable - val TopLeft: Alignment = BiasAbsoluteAlignment(-1f, -1f) - - @Stable - val TopRight: Alignment = BiasAbsoluteAlignment(1f, -1f) - - @Stable - val CenterLeft: Alignment = BiasAbsoluteAlignment(-1f, 0f) - - @Stable - val CenterRight: Alignment = BiasAbsoluteAlignment(1f, 0f) - - @Stable - val BottomLeft: Alignment = BiasAbsoluteAlignment(-1f, 1f) - - @Stable - val BottomRight: Alignment = BiasAbsoluteAlignment(1f, 1f) - - // 1D BiasAbsoluteAlignment.Horizontals. - @Stable - val Left: Alignment.Horizontal = BiasAbsoluteAlignment.Horizontal(-1f) - - @Stable - val Right: Alignment.Horizontal = BiasAbsoluteAlignment.Horizontal(1f) -} - -/** - * An [Alignment] specified by bias: for example, a bias of -1 represents alignment to the - * start/top, a bias of 0 will represent centering, and a bias of 1 will represent end/bottom. - * Any value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained - * alignment will position the aligned size fully inside the available space, while outside the - * range it will the aligned size will be positioned partially or completely outside. - * - * @see BiasAbsoluteAlignment - * @see Alignment - */ -@Immutable -data class BiasAlignment( - val horizontalBias: Float, - val verticalBias: Float -) : Alignment { - override fun align( - size: IntSize, - space: IntSize, - layoutDirection: LayoutDirection - ): IntOffset { - // Convert to Px first and only round at the end, to avoid rounding twice while calculating - // the new positions - val centerX = (space.width - size.width).toFloat() / 2f - val centerY = (space.height - size.height).toFloat() / 2f - val resolvedHorizontalBias = if (layoutDirection == LayoutDirection.Ltr) { - horizontalBias - } else { - -1 * horizontalBias - } - - val x = centerX * (1 + resolvedHorizontalBias) - val y = centerY * (1 + verticalBias) - return IntOffset(x.roundToInt(), y.roundToInt()) - } - - /** - * An [Alignment.Horizontal] specified by bias: for example, a bias of -1 represents alignment - * to the start, a bias of 0 will represent centering, and a bias of 1 will represent end. - * Any value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained - * alignment will position the aligned size fully inside the available space, while outside the - * range it will the aligned size will be positioned partially or completely outside. - * - * @see BiasAbsoluteAlignment.Horizontal - * @see Vertical - */ - @Immutable - data class Horizontal(private val bias: Float) : Alignment.Horizontal { - override fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int { - // Convert to Px first and only round at the end, to avoid rounding twice while - // calculating the new positions - val center = (space - size).toFloat() / 2f - val resolvedBias = if (layoutDirection == LayoutDirection.Ltr) bias else -1 * bias - return (center * (1 + resolvedBias)).roundToInt() - } - } - - /** - * An [Alignment.Vertical] specified by bias: for example, a bias of -1 represents alignment - * to the top, a bias of 0 will represent centering, and a bias of 1 will represent bottom. - * Any value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained - * alignment will position the aligned size fully inside the available space, while outside the - * range it will the aligned size will be positioned partially or completely outside. - * - * @see Horizontal - */ - @Immutable - data class Vertical(private val bias: Float) : Alignment.Vertical { - override fun align(size: Int, space: Int): Int { - // Convert to Px first and only round at the end, to avoid rounding twice while - // calculating the new positions - val center = (space - size).toFloat() / 2f - return (center * (1 + bias)).roundToInt() - } - } -} - -/** - * An [Alignment] specified by bias: for example, a bias of -1 represents alignment to the - * left/top, a bias of 0 will represent centering, and a bias of 1 will represent right/bottom. - * Any value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained - * alignment will position the aligned size fully inside the available space, while outside the - * range it will the aligned size will be positioned partially or completely outside. - * - * @see AbsoluteAlignment - * @see Alignment - */ -@Immutable -data class BiasAbsoluteAlignment( - private val horizontalBias: Float, - private val verticalBias: Float -) : Alignment { - /** - * Returns the position of a 2D point in a container of a given size, according to this - * [BiasAbsoluteAlignment]. The position will not be mirrored in Rtl context. - */ - override fun align(size: IntSize, space: IntSize, layoutDirection: LayoutDirection): IntOffset { - // Convert to Px first and only round at the end, to avoid rounding twice while calculating - // the new positions - val remaining = IntSize(space.width - size.width, space.height - size.height) - val centerX = remaining.width.toFloat() / 2f - val centerY = remaining.height.toFloat() / 2f - - val x = centerX * (1 + horizontalBias) - val y = centerY * (1 + verticalBias) - return IntOffset(x.roundToInt(), y.roundToInt()) - } - - /** - * An [Alignment.Horizontal] specified by bias: for example, a bias of -1 represents alignment - * to the left, a bias of 0 will represent centering, and a bias of 1 will represent right. - * Any value can be specified to obtain an alignment. Inside the [-1, 1] range, the obtained - * alignment will position the aligned size fully inside the available space, while outside the - * range it will the aligned size will be positioned partially or completely outside. - * - * @see BiasAlignment.Horizontal - */ - @Immutable - data class Horizontal(private val bias: Float) : Alignment.Horizontal { - /** - * Returns the position of a 2D point in a container of a given size, - * according to this [BiasAbsoluteAlignment.Horizontal]. This position will not be - * mirrored in Rtl context. - */ - override fun align(size: Int, space: Int, layoutDirection: LayoutDirection): Int { - // Convert to Px first and only round at the end, to avoid rounding twice while - // calculating the new positions - val center = (space - size).toFloat() / 2f - return (center * (1 + bias)).roundToInt() - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Arrangement.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Arrangement.kt deleted file mode 100644 index 5fc9a9c3e..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Arrangement.kt +++ /dev/null @@ -1,689 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layout - -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable -import kotlin.math.min -import kotlin.math.roundToInt - -/** - * Used to specify the arrangement of the layout's children in layouts like [Row] or [Column] in - * the main axis direction (horizontal and vertical, respectively). - * - * Below is an illustration of different horizontal arrangements in [Row]s: - * ![Row arrangements](https://developer.android.com/images/reference/androidx/compose/foundation/layout/row_arrangement_visualization.gif) - * - * Different vertical arrangements in [Column]s: - * ![Column arrangements](https://developer.android.com/images/reference/androidx/compose/foundation/layout/column_arrangement_visualization.gif) - */ -@Immutable -object Arrangement { - /** - * Used to specify the horizontal arrangement of the layout's children in layouts like [Row]. - */ - @Stable - interface Horizontal { - /** - * Spacing that should be added between any two adjacent layout children. - */ - val spacing get() = 0 - - /** - * Horizontally places the layout children. - * - * @param totalSize Available space that can be occupied by the children, in pixels. - * @param sizes An array of sizes of all children, in pixels. - * @param layoutDirection A layout direction, left-to-right or right-to-left, of the parent - * layout that should be taken into account when determining positions of the children. - * @param outPositions An array of the size of [sizes] that returns the calculated - * positions relative to the left, in pixels. - */ - fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) - } - - /** - * Used to specify the vertical arrangement of the layout's children in layouts like [Column]. - */ - @Stable - interface Vertical { - /** - * Spacing that should be added between any two adjacent layout children. - */ - val spacing get() = 0.dp - - /** - * Vertically places the layout children. - * - * @param totalSize Available space that can be occupied by the children, in pixels. - * @param sizes An array of sizes of all children, in pixels. - * @param outPositions An array of the size of [sizes] that returns the calculated - * positions relative to the top, in pixels. - */ - fun arrange( - totalSize: Int, - sizes: IntArray, - outPositions: IntArray - ) - } - - /** - * Used to specify the horizontal arrangement of the layout's children in horizontal layouts - * like [Row], or the vertical arrangement of the layout's children in vertical layouts like - * [Column]. - */ - @Stable - interface HorizontalOrVertical : Horizontal, Vertical { - /** - * Spacing that should be added between any two adjacent layout children. - */ - override val spacing: Dp get() = 0.dp - } - - /** - * Place children horizontally such that they are as close as possible to the beginning of the - * horizontal axis (left if the layout direction is LTR, right otherwise). - * Visually: 123#### for LTR and ####321. - */ - @Stable - val Start = object : Horizontal { - override fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) = if (layoutDirection == LayoutDirection.Ltr) { - placeLeftOrTop(sizes, outPositions, reverseInput = false) - } else { - placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = true) - } - - override fun toString() = "Arrangement#Start" - } - - /** - * Place children horizontally such that they are as close as possible to the end of the main - * axis. - * Visually: ####123 for LTR and 321#### for RTL. - */ - @Stable - val End = object : Horizontal { - override fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) = if (layoutDirection == LayoutDirection.Ltr) { - placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = false) - } else { - placeLeftOrTop(sizes, outPositions, reverseInput = true) - } - - override fun toString() = "Arrangement#End" - } - - /** - * Place children vertically such that they are as close as possible to the top of the main - * axis. - * Visually: (top) 123#### (bottom) - */ - @Stable - val Top = object : Vertical { - override fun arrange( - totalSize: Int, - sizes: IntArray, - outPositions: IntArray - ) = placeLeftOrTop(sizes, outPositions, reverseInput = false) - - override fun toString() = "Arrangement#Top" - } - - /** - * Place children vertically such that they are as close as possible to the bottom of the main - * axis. - * Visually: (top) ####123 (bottom) - */ - @Stable - val Bottom = object : Vertical { - override fun arrange( - totalSize: Int, - sizes: IntArray, - outPositions: IntArray - ) = placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = false) - - override fun toString() = "Arrangement#Bottom" - } - - /** - * Place children such that they are as close as possible to the middle of the main axis. - * Visually: ##123## for LTR and ##321## for RTL. - */ - @Stable - val Center = object : HorizontalOrVertical { - override val spacing = 0.dp - - override fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) = if (layoutDirection == LayoutDirection.Ltr) { - placeCenter(totalSize, sizes, outPositions, reverseInput = false) - } else { - placeCenter(totalSize, sizes, outPositions, reverseInput = true) - } - - override fun arrange( - totalSize: Int, - sizes: IntArray, - outPositions: IntArray - ) = placeCenter(totalSize, sizes, outPositions, reverseInput = false) - - override fun toString() = "Arrangement#Center" - } - - /** - * Place children such that they are spaced evenly across the main axis, including free - * space before the first child and after the last child. - * Visually: #1#2#3# for LTR and #3#2#1# for RTL. - */ - @Stable - val SpaceEvenly = object : HorizontalOrVertical { - override val spacing = 0.dp - - override fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) = if (layoutDirection == LayoutDirection.Ltr) { - placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = false) - } else { - placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = true) - } - - override fun arrange( - totalSize: Int, - sizes: IntArray, - outPositions: IntArray - ) = placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = false) - - override fun toString() = "Arrangement#SpaceEvenly" - } - - /** - * Place children such that they are spaced evenly across the main axis, without free - * space before the first child or after the last child. - * Visually: 1##2##3 for LTR or 3##2##1 for RTL. - */ - @Stable - val SpaceBetween = object : HorizontalOrVertical { - override val spacing = 0.dp - - override fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) = if (layoutDirection == LayoutDirection.Ltr) { - placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = false) - } else { - placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = true) - } - - override fun arrange( - totalSize: Int, - sizes: IntArray, - outPositions: IntArray - ) = placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = false) - - override fun toString() = "Arrangement#SpaceBetween" - } - - /** - * Place children such that they are spaced evenly across the main axis, including free - * space before the first child and after the last child, but half the amount of space - * existing otherwise between two consecutive children. - * Visually: #1##2##3# for LTR and #3##2##1# for RTL - */ - @Stable - val SpaceAround = object : HorizontalOrVertical { - override val spacing = 0.dp - - override fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) = if (layoutDirection == LayoutDirection.Ltr) { - placeSpaceAround(totalSize, sizes, outPositions, reverseInput = false) - } else { - placeSpaceAround(totalSize, sizes, outPositions, reverseInput = true) - } - - override fun arrange( - totalSize: Int, - sizes: IntArray, - outPositions: IntArray - ) = placeSpaceAround(totalSize, sizes, outPositions, reverseInput = false) - - override fun toString() = "Arrangement#SpaceAround" - } - - /** - * Place children such that each two adjacent ones are spaced by a fixed [space] distance across - * the main axis. The spacing will be subtracted from the available space that the children - * can occupy. The [space] can be negative, in which case children will overlap. - * - * To change alignment of the spaced children horizontally or vertically, use [spacedBy] - * overloads with `alignment` parameter. - * - * @param space The space between adjacent children. - */ - @Stable - fun spacedBy(space: Dp): HorizontalOrVertical = - SpacedAligned(space, true) { size, layoutDirection -> - Alignment.Start.align(0, size, layoutDirection) - } - - /** - * Place children horizontally such that each two adjacent ones are spaced by a fixed [space] - * distance. The spacing will be subtracted from the available width that the children - * can occupy. An [alignment] can be specified to align the spaced children horizontally - * inside the parent, in case there is empty width remaining. The [space] can be negative, - * in which case children will overlap. - * - * @param space The space between adjacent children. - * @param alignment The alignment of the spaced children inside the parent. - */ - @Stable - fun spacedBy(space: Dp, alignment: Alignment.Horizontal): Horizontal = - SpacedAligned(space, true) { size, layoutDirection -> - alignment.align(0, size, layoutDirection) - } - - /** - * Place children vertically such that each two adjacent ones are spaced by a fixed [space] - * distance. The spacing will be subtracted from the available height that the children - * can occupy. An [alignment] can be specified to align the spaced children vertically - * inside the parent, in case there is empty height remaining. The [space] can be negative, - * in which case children will overlap. - * - * @param space The space between adjacent children. - * @param alignment The alignment of the spaced children inside the parent. - */ - @Stable - fun spacedBy(space: Dp, alignment: Alignment.Vertical): Vertical = - SpacedAligned(space, false) { size, _ -> alignment.align(0, size) } - - /** - * Place children horizontally one next to the other and align the obtained group - * according to an [alignment]. - * - * @param alignment The alignment of the children inside the parent. - */ - @Stable - fun aligned(alignment: Alignment.Horizontal): Horizontal = - SpacedAligned(0.dp, true) { size, layoutDirection -> - alignment.align(0, size, layoutDirection) - } - - /** - * Place children vertically one next to the other and align the obtained group - * according to an [alignment]. - * - * @param alignment The alignment of the children inside the parent. - */ - @Stable - fun aligned(alignment: Alignment.Vertical): Vertical = - SpacedAligned(0.dp, false) { size, _ -> alignment.align(0, size) } - - @Immutable - object Absolute { - /** - * Place children horizontally such that they are as close as possible to the left edge of - * the [Row]. - * - * Unlike [Arrangement.Start], when the layout direction is RTL, the children will not be - * mirrored and as such children will appear in the order they are composed inside the [Row]. - * - * Visually: 123#### - */ - @Stable - val Left = object : Horizontal { - override fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) = placeLeftOrTop(sizes, outPositions, reverseInput = false) - - override fun toString() = "AbsoluteArrangement#Left" - } - - /** - * Place children such that they are as close as possible to the middle of the [Row]. - * - * Unlike [Arrangement.Center], when the layout direction is RTL, the children will not be - * mirrored and as such children will appear in the order they are composed inside the [Row]. - * - * Visually: ##123## - */ - @Stable - val Center = object : Horizontal { - override fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) = placeCenter(totalSize, sizes, outPositions, reverseInput = false) - - override fun toString() = "AbsoluteArrangement#Center" - } - - /** - * Place children horizontally such that they are as close as possible to the right edge of - * the [Row]. - * - * Unlike [Arrangement.End], when the layout direction is RTL, the children will not be - * mirrored and as such children will appear in the order they are composed inside the [Row]. - * - * Visually: ####123 - */ - @Stable - val Right = object : Horizontal { - override fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) = placeRightOrBottom(totalSize, sizes, outPositions, reverseInput = false) - - override fun toString() = "AbsoluteArrangement#Right" - } - - /** - * Place children such that they are spaced evenly across the main axis, without free - * space before the first child or after the last child. - * - * Unlike [Arrangement.SpaceBetween], when the layout direction is RTL, the children will not be - * mirrored and as such children will appear in the order they are composed inside the [Row]. - * - * Visually: 1##2##3 - */ - @Stable - val SpaceBetween = object : Horizontal { - override fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) = placeSpaceBetween(totalSize, sizes, outPositions, reverseInput = false) - - override fun toString() = "AbsoluteArrangement#SpaceBetween" - } - - /** - * Place children such that they are spaced evenly across the main axis, including free - * space before the first child and after the last child. - * - * Unlike [Arrangement.SpaceEvenly], when the layout direction is RTL, the children will not be - * mirrored and as such children will appear in the order they are composed inside the [Row]. - * - * Visually: #1#2#3# - */ - @Stable - val SpaceEvenly = object : Horizontal { - override fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) = placeSpaceEvenly(totalSize, sizes, outPositions, reverseInput = false) - - override fun toString() = "AbsoluteArrangement#SpaceEvenly" - } - - /** - * Place children such that they are spaced evenly horizontally, including free - * space before the first child and after the last child, but half the amount of space - * existing otherwise between two consecutive children. - * - * Unlike [Arrangement.SpaceAround], when the layout direction is RTL, the children will not be - * mirrored and as such children will appear in the order they are composed inside the [Row]. - * - * Visually: #1##2##3##4# - */ - @Stable - val SpaceAround = object : Horizontal { - override fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) = placeSpaceAround(totalSize, sizes, outPositions, reverseInput = false) - - override fun toString() = "AbsoluteArrangement#SpaceAround" - } - - /** - * Place children such that each two adjacent ones are spaced by a fixed [space] distance across - * the main axis. The spacing will be subtracted from the available space that the children - * can occupy. - * - * Unlike [Arrangement.spacedBy], when the layout direction is RTL, the children will not be - * mirrored and as such children will appear in the order they are composed inside the [Row]. - * - * @param space The space between adjacent children. - */ - @Stable - fun spacedBy(space: Dp): HorizontalOrVertical = - SpacedAligned(space, false, null) - - /** - * Place children horizontally such that each two adjacent ones are spaced by a fixed [space] - * distance. The spacing will be subtracted from the available width that the children - * can occupy. An [alignment] can be specified to align the spaced children horizontally - * inside the parent, in case there is empty width remaining. - * - * Unlike [Arrangement.spacedBy], when the layout direction is RTL, the children will not be - * mirrored and as such children will appear in the order they are composed inside the [Row]. - * - * @param space The space between adjacent children. - * @param alignment The alignment of the spaced children inside the parent. - */ - @Stable - fun spacedBy(space: Dp, alignment: Alignment.Horizontal): Horizontal = - SpacedAligned(space, false) { size, layoutDirection -> - alignment.align(0, size, layoutDirection) - } - - /** - * Place children vertically such that each two adjacent ones are spaced by a fixed [space] - * distance. The spacing will be subtracted from the available height that the children - * can occupy. An [alignment] can be specified to align the spaced children vertically - * inside the parent, in case there is empty height remaining. - * - * Unlike [Arrangement.spacedBy], when the layout direction is RTL, the children will not be - * mirrored and as such children will appear in the order they are composed inside the [Row]. - * - * @param space The space between adjacent children. - * @param alignment The alignment of the spaced children inside the parent. - */ - @Stable - fun spacedBy(space: Dp, alignment: Alignment.Vertical): Vertical = - SpacedAligned(space, false) { size, _ -> alignment.align(0, size) } - - /** - * Place children horizontally one next to the other and align the obtained group - * according to an [alignment]. - * - * Unlike [Arrangement.aligned], when the layout direction is RTL, the children will not be - * mirrored and as such children will appear in the order they are composed inside the [Row]. - * - * @param alignment The alignment of the children inside the parent. - */ - @Stable - fun aligned(alignment: Alignment.Horizontal): Horizontal = - SpacedAligned(0.dp, false) { size, layoutDirection -> - alignment.align(0, size, layoutDirection) - } - } - - /** - * Arrangement with spacing between adjacent children and alignment for the spaced group. - * Should not be instantiated directly, use [spacedBy] instead. - */ - @Immutable - internal data class SpacedAligned( - val space: Dp, - val rtlMirror: Boolean, - val alignment: ((Int, LayoutDirection) -> Int)? - ) : HorizontalOrVertical { - - override val spacing = space - - override fun arrange( - totalSize: Int, - sizes: IntArray, - layoutDirection: LayoutDirection, - outPositions: IntArray - ) { - if (sizes.isEmpty()) return - val spacePx = space - - var occupied = 0 - var lastSpace = 0 - val reversed = rtlMirror && layoutDirection == LayoutDirection.Rtl - sizes.forEachIndexed(reversed) { index, it -> - outPositions[index] = min(occupied, totalSize - it) - lastSpace = min(spacePx, totalSize - outPositions[index] - it) - occupied = outPositions[index] + it + lastSpace - } - occupied -= lastSpace - - if (alignment != null && occupied < totalSize) { - val groupPosition = alignment.invoke(totalSize - occupied, layoutDirection) - for (index in outPositions.indices) { - outPositions[index] += groupPosition - } - } - } - - override fun arrange( - totalSize: Int, - sizes: IntArray, - outPositions: IntArray - ) = arrange(totalSize, sizes, LayoutDirection.Ltr, outPositions) - - override fun toString() = - "${if (rtlMirror) "" else "Absolute"}Arrangement#spacedAligned($space, $alignment)" - } - - internal fun placeRightOrBottom( - totalSize: Int, - size: IntArray, - outPosition: IntArray, - reverseInput: Boolean - ) { - val consumedSize = size.fold(0) { a, b -> a + b } - var current = totalSize - consumedSize - size.forEachIndexed(reverseInput) { index, it -> - outPosition[index] = current - current += it - } - } - - internal fun placeLeftOrTop(size: IntArray, outPosition: IntArray, reverseInput: Boolean) { - var current = 0 - size.forEachIndexed(reverseInput) { index, it -> - outPosition[index] = current - current += it - } - } - - internal fun placeCenter( - totalSize: Int, - size: IntArray, - outPosition: IntArray, - reverseInput: Boolean - ) { - val consumedSize = size.fold(0) { a, b -> a + b } - var current = (totalSize - consumedSize).toFloat() / 2 - size.forEachIndexed(reverseInput) { index, it -> - outPosition[index] = current.roundToInt() - current += it.toFloat() - } - } - - internal fun placeSpaceEvenly( - totalSize: Int, - size: IntArray, - outPosition: IntArray, - reverseInput: Boolean - ) { - val consumedSize = size.fold(0) { a, b -> a + b } - val gapSize = (totalSize - consumedSize).toFloat() / (size.size + 1) - var current = gapSize - size.forEachIndexed(reverseInput) { index, it -> - outPosition[index] = current.roundToInt() - current += it.toFloat() + gapSize - } - } - - internal fun placeSpaceBetween( - totalSize: Int, - size: IntArray, - outPosition: IntArray, - reverseInput: Boolean - ) { - if (size.isEmpty()) return - - val consumedSize = size.fold(0) { a, b -> a + b } - val noOfGaps = maxOf(size.lastIndex, 1) - val gapSize = (totalSize - consumedSize).toFloat() / noOfGaps - - var current = 0f - if (reverseInput && size.size == 1) { - // If the layout direction is right-to-left and there is only one gap, - // we start current with the gap size. That forces the single item to be right-aligned. - current = gapSize - } - size.forEachIndexed(reverseInput) { index, it -> - outPosition[index] = current.roundToInt() - current += it.toFloat() + gapSize - } - } - - internal fun placeSpaceAround( - totalSize: Int, - size: IntArray, - outPosition: IntArray, - reverseInput: Boolean - ) { - val consumedSize = size.fold(0) { a, b -> a + b } - val gapSize = if (size.isNotEmpty()) { - (totalSize - consumedSize).toFloat() / size.size - } else { - 0f - } - var current = gapSize / 2 - size.forEachIndexed(reverseInput) { index, it -> - outPosition[index] = current.roundToInt() - current += it.toFloat() + gapSize - } - } - - private inline fun IntArray.forEachIndexed(reversed: Boolean, action: (Int, Int) -> Unit) { - if (!reversed) { - forEachIndexed(action) - } else { - for (i in (size - 1) downTo 0) { - action(i, get(i)) - } - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Box.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Box.kt deleted file mode 100644 index e69c5ccb6..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Box.kt +++ /dev/null @@ -1,60 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layout - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import net.kernelpanicsoft.archie.gui.modifiers.Constraints -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.position.MarginModifier -import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier -import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingValues - -/** - * A layout composable that stacks its children on top of each other, aligned within its bounds. - * - * Each child is independently aligned using [contentAlignment]. Children are drawn in - * declaration order (first child at the bottom, last child on top). - * - * ### Example - * ```kotlin - * Box(contentAlignment = Alignment.Center, modifier = Modifier.size(100, 60)) { - * // background fills the box - * Spacer(modifier = Modifier.fillMaxSize().background(KColor.DARK_GRAY)) - * Text(Component.literal("Centered")) - * } - * ``` - * - * @param modifier Modifiers applied to the outer Box node. - * @param contentAlignment How children are positioned within the box. Default [Alignment.TopStart]. - * @param content The child composables to stack. - */ -@Composable -fun Box( - modifier: Modifier = Modifier, - contentAlignment: Alignment = Alignment.TopStart, - content: @Composable () -> Unit -) { - val measurePolicy = remember(contentAlignment) { BoxMeasurePolicy(contentAlignment) } - Layout( - name = "Box", - measurePolicy, - modifier = modifier, - content = content - ) -} - -internal data class BoxMeasurePolicy( - private val alignment: Alignment, -) : RowColumnMeasurePolicy() { - - override fun placeChildren(scope: MeasureScope, measurables: List, placeables: List, width: Int, height: Int): MeasureResult { - return MeasureResult(width, height) { - val inset = (scope as? LayoutNode)?.get()?.padding - ?: PaddingValues() - var accumulatedOutset = 0 - for ((index, child) in placeables.withIndex()) { - child.placeAt(alignment.align(child.size, IntSize(width, height), LayoutDirection.Ltr) + inset.getOffset()) - (measurables[index] as? LayoutNode)?.get()?.let { accumulatedOutset += it.horizontal + it.vertical } - } - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Column.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Column.kt deleted file mode 100644 index 835506c31..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Column.kt +++ /dev/null @@ -1,87 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layout - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.position.MarginModifier -import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier -import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingValues - -/** - * A layout composable that arranges its children in a vertical sequence from top to bottom. - * - * Children are measured sequentially and their heights subtracted from the available space. - * Use [verticalArrangement] to control spacing and placement along the main axis, and - * [horizontalAlignment] to align children along the cross axis. - * - * ### Example - * ```kotlin - * Column( - * verticalArrangement = Arrangement.spacedBy(8), - * horizontalAlignment = Alignment.CenterHorizontally, - * ) { - * Text(Component.literal("Title")) - * Text(Component.literal("Subtitle")) - * } - * ``` - * - * @param modifier Modifiers applied to the Column node. - * @param verticalArrangement Controls spacing and placement along the vertical axis. - * @param horizontalAlignment Controls alignment of children along the horizontal axis. - * @param content The child composables to lay out in a column. - */ -@Composable -fun Column( - modifier: Modifier = Modifier, - verticalArrangement: Arrangement.Vertical = Arrangement.Top, - horizontalAlignment: Alignment.Horizontal = Alignment.Start, - content: @Composable () -> Unit -) { - val measurePolicy = remember(verticalArrangement, horizontalAlignment) { - ColumnMeasurePolicy( - verticalArrangement, - horizontalAlignment - ) - } - Layout( - name = "Column", - measurePolicy, - modifier = modifier, - content = content - ) -} - -private data class ColumnMeasurePolicy( - private val verticalArrangement: Arrangement.Vertical, - private val horizontalAlignment: Alignment.Horizontal, -) : RowColumnMeasurePolicy( - sumHeight = true, - arrangementSpacing = verticalArrangement.spacing -) { - override fun placeChildren(scope: MeasureScope, measurables: List, placeables: List, width: Int, height: Int): MeasureResult { - val childCount = placeables.size - val positions = IntArray(childCount) - val sizes = IntArray(childCount) - for (index in 0 until childCount) { - sizes[index] = placeables[index].height - } - - verticalArrangement.arrange( - totalSize = height, - sizes = sizes, - outPositions = positions - ) - - return MeasureResult(width, height) { - val inset = (scope as? LayoutNode)?.get()?.padding - ?: PaddingValues() - var accumulatedOutset = 0 - - for (index in 0 until childCount) { - val child = placeables[index] - child.placeAt(horizontalAlignment.align(child.width, width, LayoutDirection.Ltr) + inset.left, positions[index] + accumulatedOutset + inset.top) - (measurables[index] as? LayoutNode)?.get()?.let { accumulatedOutset += it.vertical } - } - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Helpers.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Helpers.kt deleted file mode 100644 index 52487f5ee..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Helpers.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layout - -/** - * A density-independent pixel unit used throughout the layout system. Currently an alias for - * [Int] since GUI measurements map 1:1 to Minecraft GUI pixels (no separate density scaling). - */ -typealias Dp = Int - -/** - * Converts this [Int] to a [Dp] value. Provided so measurements read naturally at call sites, - * e.g. `16.dp`, mirroring Compose's `Dp` API even though no unit conversion currently happens. - */ -inline val Int.dp: Int get() = this \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntCoordinates.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntCoordinates.kt deleted file mode 100644 index 04d912239..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntCoordinates.kt +++ /dev/null @@ -1,55 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layout - -import kotlinx.serialization.Serializable - -/** - * A 2D integer coordinate pair, packed into a single [Long] (`x` in the high 32 bits, `y` in the - * low 32 bits) to avoid boxing allocations. Also aliased as [IntOffset] when used to represent a - * relative displacement rather than an absolute position. - */ -@JvmInline -@Serializable -value class IntCoordinates(val pair: Long) { - val x get() = (pair shr 32).toInt() - val y get() = pair.toInt() - - operator fun component1() = x - operator fun component2() = y - - constructor(x: Int, y: Int) : this((x.toLong() shl 32) or y.toLong()) - - override fun toString(): String = "($x, $y)" - - operator fun plus(other: IntCoordinates) = IntCoordinates(x + other.x, y + other.y) - operator fun minus(other: IntCoordinates) = IntCoordinates(x - other.x, y - other.y) -} - -/** An [IntCoordinates] used to represent a relative displacement rather than an absolute position. */ -typealias IntOffset = IntCoordinates - -/** - * An integer width/height pair, packed into a single [Long] (`width` in the high 32 bits, - * `height` in the low 32 bits) to avoid boxing allocations. - */ -@JvmInline -@Serializable -value class IntSize(val pair: Long) { - val width get() = (pair shr 32).toInt() - val height get() = pair.toInt() - - operator fun component1() = width - operator fun component2() = height - - constructor(width: Int, height: Int) : this((width.toLong() shl 32) or height.toLong()) - - override fun toString(): String = "($width, $height)" -} - -/** Creates an [IntCoordinates] at the given [x], [y] position. */ -fun pos(x: Int, y: Int) = IntCoordinates(x, y) - -/** Creates an [IntOffset] with the given [x], [y] displacement, defaulting to zero. */ -fun offset(x: Int = 0, y: Int = 0) = IntOffset(x, y) - -/** Creates an [IntSize] with the given [width] and [height]. */ -fun size(width: Int, height: Int) = IntSize(width, height) \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntRect.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntRect.kt deleted file mode 100644 index 86a083c37..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntRect.kt +++ /dev/null @@ -1,44 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layout - -import kotlinx.serialization.Serializable - -/** - * An axis-aligned integer rectangle described by its min/max bounds along each axis, rather than - * an origin and a size. Used for hit-testing and clip/overlap calculations (e.g. scissor regions). - */ -@Serializable -data class IntRect( - val minX: Int, - val minY: Int, - val maxX: Int, - val maxY: Int, -) { - val width: Int get() = maxX - minX - val height: Int get() = maxY - minY - - /** Returns `true` if this rect has zero or negative width/height. */ - fun isEmpty(): Boolean = width <= 0 || height <= 0 - - /** - * Returns the overlapping region between this rect and [other], or `null` if they don't - * overlap. - */ - fun intersect(other: IntRect): IntRect? { - val ix = maxOf(minX, other.minX) - val iy = maxOf(minY, other.minY) - val ax = minOf(maxX, other.maxX) - val ay = minOf(maxY, other.maxY) - return if (ax <= ix || ay <= iy) null else IntRect(ix, iy, ax, ay) - } - - operator fun div(other: IntRect): IntRect? = intersect(other) - - companion object { - /** A rect with zero bounds on every side. */ - val EMPTY: IntRect = IntRect(0, 0, 0, 0) - - /** Builds an [IntRect] from a top-left [position] and a [size]. */ - fun fromPositionAndSize(position: IntCoordinates, size: Size): IntRect = - IntRect(position.x, position.y, position.x + size.width, position.y + size.height) - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Layout.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Layout.kt deleted file mode 100644 index 924503fe3..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Layout.kt +++ /dev/null @@ -1,60 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layout - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.ComposeNode -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.nodes.LayoutNodeApplier - -/** - * The fundamental building block for creating custom Compose-based UI elements in Archie. - * - * [Layout] is the lowest-level composable: it emits a single [UINode] into the composition - * tree and wires up measurement, rendering, and modifier behaviour via the provided policies. - * Higher-level composables such as [Box], [Row], [Column], and all built-in widgets are - * implemented in terms of [Layout]. - * - * ### Creating a custom composable - * ```kotlin - * @Composable - * fun MyBox(modifier: Modifier = Modifier) { - * Layout( - * measurePolicy = { measurables, constraints -> - * val placeables = measurables.map { it.measure(constraints) } - * MeasureResult(constraints.maxWidth, constraints.maxHeight) { - * placeables.forEach { it.placeAt(0, 0) } - * } - * }, - * renderer = object : Renderer { - * override fun render(node, x, y, guiGraphics, mouseX, mouseY, partialTick) { - * guiGraphics.fill(x, y, x + node.width, y + node.height, 0xFFFF0000.toInt()) - * } - * }, - * modifier = modifier, - * ) - * } - * ``` - * - * @param measurePolicy Defines how this node and its children are measured and placed. - * @param renderer Defines how this node renders itself. Defaults to [EmptyRenderer]. - * @param modifier [Modifier] chain applied to this node. - * @param content Child composables emitted inside this node. - */ -@Composable -inline fun Layout( - name: String, - measurePolicy: MeasurePolicy, - renderer: Renderer = EmptyRenderer, - modifier: Modifier = Modifier, - content: @Composable () -> Unit = {} -) { - ComposeNode( - factory = { LayoutNode(name) }, - update = { - set(measurePolicy) { this.measurePolicy = it } - set(renderer) { this.renderer = it } - set(modifier) { this.modifier = it } - }, - content = content, - ) -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutDirection.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutDirection.kt deleted file mode 100644 index 24385572e..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutDirection.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layout - - -/** - * A class for defining layout directions. - * - * A layout direction can be left-to-right (LTR) or right-to-left (RTL). - */ -enum class LayoutDirection { - /** - * Horizontal layout direction is from Left to Right. - */ - Ltr, - - /** - * Horizontal layout direction is from Right to Left. - */ - Rtl -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutNode.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutNode.kt deleted file mode 100644 index cba70c335..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutNode.kt +++ /dev/null @@ -1,401 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layout - -import net.kernelpanicsoft.archie.gui.ComposeContainerScreen -import net.minecraft.client.Minecraft -import net.minecraft.client.gui.GuiGraphics -import net.minecraft.network.chat.Component -import net.kernelpanicsoft.archie.gui.modifiers.* -import net.kernelpanicsoft.archie.gui.modifiers.position.MarginModifier -import net.kernelpanicsoft.archie.gui.modifiers.position.ZIndexModifier -import net.kernelpanicsoft.archie.gui.modifiers.DebugModifier -import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.util.extension.drawRectOutline -import kotlin.reflect.KClass - -// ARGB debug overlay colours -private const val COMPONENT_OUTLINE = 0xFF00FFFF.toInt() -private const val DEBUG_OUTLINE = 0xFF000000.toInt() -private const val DEBUG_FILL = 0xA7000000.toInt() -private const val DEBUG_TEXT = 0xFFFFFFFF.toInt() -private const val OUTSET_FILL = 0x80800080.toInt() -private const val INSET_FILL = 0x80FF0000.toInt() -private const val LINE_SPACING = 2 -private const val COLUMN_SPACING = 6 - -/** - * The concrete node type that forms Archie's UI scene graph. - * - * Every composable in the Archie GUI framework ultimately creates one [LayoutNode]. - * It handles measurement, draw-chain rendering (including [DrawModifier] wrapping), - * z-index sorting, input hit-testing, and the Ctrl+Shift debug overlay. - * - * Do not instantiate directly — use [Layout] and higher-level composables instead. - * - * @param nodeName A human-readable label shown in the debug overlay for this node. - */ -class LayoutNode( - private val nodeName: String = "LayoutNode", -) : Measurable, Placeable, UINode, MeasureScope { - - override var measurePolicy: MeasurePolicy = ChildMeasurePolicy - override var renderer: Renderer = EmptyRenderer - - /** Mutable list of child LayoutNodes managed by the Compose applier. */ - val children = mutableListOf() - - var layer: Int = 0 - get() = parent?.layer ?: field - set(value) = parent?.let { it.layer = value } ?: run { field = value } - - private var childrenAscendingZCache: List? = null - private var childrenDescendingZCache: List? = null - - /** This node's human-readable label, as shown in the debug overlay and used by [findNode]/[findAllNodes]. */ - val name: String get() = nodeName - - /** Recursively searches this subtree for a descendant node whose [nodeName] equals [name]. */ - fun findNode(name: String): LayoutNode? { - val snapshot = children.toList() - return snapshot.find { it.nodeName == name } ?: snapshot.firstNotNullOfOrNull { it.findNode(name) } - } - - /** Recursively searches this subtree for every descendant node whose [nodeName] equals [name], in depth-first order. */ - fun findAllNodes(name: String): List = children.toList().flatMap { child -> - if (child.nodeName == name) listOf(child) + child.findAllNodes(name) else child.findAllNodes(name) - } - - /** This subtree (this node plus every descendant), in depth-first pre-order. */ - fun flatten(): List = listOf(this) + children.toList().flatMap { it.flatten() } - - override var modifier: Modifier = Modifier - set(value) { - val previousZ = zIndex - field = value - // Rebuild processed-modifier map (merged by type) - processedModifier = modifier.foldIn(mutableMapOf()) { acc, element -> - val existing = acc[element::class] - acc[element::class] = if (existing != null) existing.unsafeMergeWith(element) else element - acc - } - drawModifiers = modifier.foldIn(mutableListOf()) { acc, element -> - if (element is DrawModifier) acc.add(element) - acc - } - layoutChangingModifiers = modifier.foldIn(mutableListOf()) { acc, element -> - if (element is LayoutChangingModifier) acc.add(element) - acc - } - - if (previousZ != zIndex) { - parent?.invalidateChildrenZCache() - } - } - - /** Processed modifier map keyed by element type for O(1) lookup. */ - var processedModifier = mapOf>, Modifier.Element<*>>() - private set - - /** Ordered list of [DrawModifier]s extracted from [modifier]. */ - var drawModifiers: List = emptyList() - private set - - /** Ordered list of [LayoutChangingModifier]s extracted from [modifier]. */ - var layoutChangingModifiers: List = emptyList() - private set - - /** Retrieves the merged [Modifier.Element] of type [T] from [processedModifier], or `null`. */ - inline fun > get(): T? = processedModifier[T::class] as? T - - /** The parent [LayoutNode] in the scene graph, or `null` for root nodes. */ - var parent: LayoutNode? = null - - override var width: Int = 0 - override var height: Int = 0 - override var x: Int = 0 - override var y: Int = 0 - override var renderState: String? = null - - /** The effective z-index for this node, used for draw and input ordering. */ - val zIndex: Float get() = get()?.zIndex ?: 0f - - /** This node's absolute z-depth, combining its [layer]'s base z with all ancestor [zIndex]es. */ - val effectiveZ: Float get() = effectiveZ(ComposeContainerScreen.layerBaseZ(layer)) - - /** Computes the maximum effective z-depth in this subtree, adding [layerOffset]. */ - fun getMaxZ(layerOffset: Float): Float { - val myZ = effectiveZ(layerOffset) - return maxOf(myZ, children.toList().maxOfOrNull { it.getMaxZ(layerOffset) } ?: myZ) - } - - internal fun invalidateChildrenZCache() { - childrenAscendingZCache = null - childrenDescendingZCache = null - } - - internal fun childrenAscendingZ(): List { - val cached = childrenAscendingZCache - if (cached != null) return cached - return children.toList().sortedBy { it.zIndex }.also { sorted -> - childrenAscendingZCache = sorted - childrenDescendingZCache = sorted.asReversed() - } - } - - internal fun childrenDescendingZ(): List { - val cached = childrenDescendingZCache - if (cached != null) return cached - return children.toList().sortedByDescending { it.zIndex }.also { sorted -> - childrenDescendingZCache = sorted - childrenAscendingZCache = sorted.asReversed() - } - } - - private fun effectiveZ(layerOffset: Float): Float = - (parent?.effectiveZ(layerOffset) ?: layerOffset) + zIndex - - /** - * Absolute on-screen coordinates, accumulating parent offsets up the scene graph. - */ - val absoluteCoords: IntCoordinates - get() { - var coords = IntCoordinates(x, y) - var p = parent - while (p != null) { coords += IntCoordinates(p.x, p.y); p = p.parent } - return coords - } - - /** The topmost ancestor [LayoutNode] (the root of this subtree). */ - val rootNode: LayoutNode get() = parent?.rootNode ?: this - - /** - * Whether the debug overlay is active. Setting this on a child propagates to the root. - */ - var debug: Boolean = false - get() = parent?.debug ?: field - set(value) = parent?.let { it.debug = value } ?: run { field = value } - - /** - * Whether the extended modifier info is shown in the debug overlay. Setting this propagates to the root. - */ - var extraDebug: Boolean = false - get() = parent?.extraDebug ?: field - set(value) = parent?.let { it.extraDebug = value } ?: run { field = value } - - // ── Measurement ─────────────────────────────────────────────────────── - - override fun measure(constraints: Constraints): Placeable { - // Snapshot once - Compose's Recomposer applies structural changes (LayoutNodeApplier - // insert/remove/move) from its own recompose+apply coroutine, which isn't necessarily - // synchronized with whatever thread is measuring, so iterating the live `children` list - // directly here (as this used to) could throw ConcurrentModificationException if a - // recomposition mutates it mid-measure. - val childrenSnapshot = children.toList() - - // Collect outset (margin) from children - val outset = childrenSnapshot.fold(listOf()) { acc, child -> - acc + child.modifier.getAll() - } - val horizontal = outset.sumOf { it.horizontal } - val vertical = outset.sumOf { it.vertical } - - val innerConstraints = layoutChangingModifiers.fold(constraints) { c, m -> m.modifyInnerConstraints(c) } - val result = measurePolicy.measure(this, childrenSnapshot, innerConstraints) - - // Account for padding inset - val inset = get() - val insetH = inset?.horizontal ?: 0 - val insetV = inset?.vertical ?: 0 - - val newWidth = result.width + horizontal + insetH - val newHeight = result.height + vertical + insetV - - if (width != newWidth || height != newHeight) { - get()?.onSizeChanged?.invoke(Size(newWidth, newHeight)) - } - width = newWidth - height = newHeight - - val layoutConstraints = layoutChangingModifiers.fold(constraints) { c, m -> - m.modifyLayoutConstraints(IntSize(newWidth, newHeight), c) - } - width = width.coerceIn(layoutConstraints.minWidth..layoutConstraints.maxWidth) - height = height.coerceIn(layoutConstraints.minHeight..layoutConstraints.maxHeight) - - result.placer.placeChildren() - - return object : Placeable by this { - override var width: Int = this@LayoutNode.width - override var height: Int = this@LayoutNode.height - } - } - - override fun placeAt(x: Int, y: Int) { - val offset = layoutChangingModifiers.fold(IntOffset(x, y)) { acc, m -> m.modifyPosition(acc) } - this.x = offset.x - this.y = offset.y - get()?.onGloballyPositioned?.invoke(absoluteCoords) - } - - // ── Rendering ───────────────────────────────────────────────────────── - - override fun render(x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { - render(x, y, guiGraphics, mouseX, mouseY, partialTick, 0f) - } - - /** - * Renders this node and its entire subtree, with z-index translation, draw-modifier - * wrapping, and the optional debug overlay. - */ - fun render(x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, zOffset: Float) { - if (parent == null) { - guiGraphics.pose().pushPose() - guiGraphics.pose().translate(0.0, 0.0, zOffset.toDouble()) - renderRecursive(x, y, guiGraphics, mouseX, mouseY, partialTick, zOffset) - - if (rootNode.debug) { - guiGraphics.pose().pushPose() - guiGraphics.pose().translate(0.0, 0.0, 1000.0 + zOffset) - renderDebug(x, y, guiGraphics, mouseX, mouseY, partialTick) - guiGraphics.pose().popPose() - } - - guiGraphics.pose().popPose() - } - } - - private fun renderRecursive(x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, zOffset: Float) { - val dx = this.x + x - val dy = this.y + y - - guiGraphics.pose().pushPose() - guiGraphics.pose().translate(0.0, 0.0, zIndex.toDouble()) - - // Build the draw chain from innermost (content) outward through DrawModifiers - val contentDrawer: () -> Unit = { - renderer.render(this, dx, dy, guiGraphics, mouseX, mouseY, partialTick) - childrenAscendingZ().forEach { it.renderRecursive(dx, dy, guiGraphics, mouseX, mouseY, partialTick, zOffset) } - renderer.renderAfterChildren(this, dx, dy, guiGraphics, mouseX, mouseY, partialTick) - } - - val drawChain = drawModifiers.reversed().fold(contentDrawer) { acc, mod -> - { - val scope = object : ContentDrawScope { - override val guiGraphics = guiGraphics - override val width = this@LayoutNode.width - override val height = this@LayoutNode.height - override val x = dx - override val y = dy - override fun drawContent() = acc() - } - with(mod) { scope.draw() } - } - } - drawChain() - - guiGraphics.pose().popPose() - } - - // ── Debug overlay ───────────────────────────────────────────────────── - - private fun renderDebug(x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) { - val dx = this.x + x - val dy = this.y + y - - val hoveredChildren = children.toList().filter { it.isBounded(mouseX, mouseY) } - if (hoveredChildren.isNotEmpty()) { - hoveredChildren.forEach { it.renderDebug(dx, dy, guiGraphics, mouseX, mouseY, partialTick) } - return - } - if (!isBounded(mouseX, mouseY)) return - - guiGraphics.drawRectOutline(dx, dy, width, height, COMPONENT_OUTLINE) - - // Margin (outset) visualisation - (processedModifier[MarginModifier::class] as? MarginModifier)?.let { mod -> - with(mod.margin) { - if (top != 0) guiGraphics.fill(dx, dy - top, dx + width, dy, OUTSET_FILL) - if (bottom != 0) guiGraphics.fill(dx, dy + height, dx + width, dy + height + bottom, OUTSET_FILL) - if (left != 0) guiGraphics.fill(dx - left, dy, dx, dy + height, OUTSET_FILL) - if (right != 0) guiGraphics.fill(dx + width, dy, dx + width + right, dy + height, OUTSET_FILL) - } - } - - // Padding (inset) visualisation - (processedModifier[PaddingModifier::class] as? PaddingModifier)?.let { mod -> - with(mod.padding) { - if (top != 0) guiGraphics.fill(dx + left, dy, dx + width - right, dy + top, INSET_FILL) - if (bottom != 0) guiGraphics.fill(dx + left, dy + height - bottom, dx + width - right, dy + height, INSET_FILL) - if (left != 0) guiGraphics.fill(dx, dy + top, dx + left, dy + height - bottom, INSET_FILL) - if (right != 0) guiGraphics.fill(dx + width - right, dy + top, dx + width, dy + height - bottom, INSET_FILL) - } - } - - // Tooltip panel - val font = Minecraft.getInstance().font - var tooltipY = dy + height + 1 - - val debugLines: List> = buildList { - add(listOf(Component.literal(nodeName))) - add(listOf( - Component.literal("X:").apply { append(Component.literal("$dx").withColor(0x00FFFF)); append(", Y:"); append(Component.literal("$dy").withColor(0x32CD32)); append(", Z:"); append(Component.literal("$effectiveZ").withColor(0xFF66FF)) }, - Component.literal("W:").apply { append(Component.literal("$width").withColor(0xFFA500)); append(", H:"); append(Component.literal("$height").withColor(0x87CEEB)); append(", L:"); append(Component.literal("$layer").withColor(0x87CEEB)) }, - )) - if (extraDebug) { - val mods = mutableListOf() - modifier.all { mod -> - if (mod is DebugModifier) mods.addAll(0, mod.toComponents()) - else mods.add(mod.toComponent()) - true - } - if (mods.isNotEmpty()) { - add(listOf(Component.literal("Modifiers:"))) - mods.forEach { add(listOf(it)) } - } - } - } - - val lineWidths = debugLines.map { line -> line.sumOf { font.width(it) } + (line.size - 1) * COLUMN_SPACING } - val maxLineWidth = (lineWidths.maxOrNull() ?: 0) + 4 - val panelHeight = debugLines.size * (font.lineHeight + LINE_SPACING) - LINE_SPACING + 2 - - if (tooltipY + panelHeight > guiGraphics.guiHeight()) tooltipY -= height + panelHeight + 2 - - guiGraphics.drawRectOutline(dx + 1, tooltipY, maxLineWidth, panelHeight, DEBUG_OUTLINE) - guiGraphics.fill(dx + 1, tooltipY, dx + 1 + maxLineWidth, tooltipY + panelHeight, DEBUG_FILL) - - debugLines.forEachIndexed { row, line -> - var colX = dx + 3 - val textY = tooltipY + row * (font.lineHeight + LINE_SPACING) + 1 - line.forEachIndexed { col, text -> - guiGraphics.drawString(font, text, colX, textY, DEBUG_TEXT) - if (col < line.size - 1) colX += font.width(text) + COLUMN_SPACING - } - } - } - - // ── Hit testing ─────────────────────────────────────────────────────── - - /** - * Returns `true` if ([mouseX], [mouseY]) falls within this node's absolute screen bounds. - */ - fun isBounded(mouseX: Int, mouseY: Int): Boolean { - val (ax, ay) = absoluteCoords - return mouseX in ax until (ax + width) && mouseY in ay until (ay + height) - } - - override fun toString() = children.toList().run { if (isNotEmpty()) joinToString(prefix = "$nodeName {\n", separator = "\n", postfix = "\n}") { "\t$it" } else "$nodeName()" } - - internal companion object { - val ChildMeasurePolicy = MeasurePolicy { _, measurables, constraints -> - val placeables = measurables.map { it.measure(constraints) } - MeasureResult( - placeables.maxOfOrNull { it.width } ?: 0, - placeables.maxOfOrNull { it.height } ?: 0, - ) { placeables.forEach { it.placeAt(0, 0) } } - } - } -} - -/** A [Renderer] that performs no drawing — the default for layout-only nodes. */ -val EmptyRenderer = object : Renderer {} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/MeasurePolicy.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/MeasurePolicy.kt deleted file mode 100644 index dd8b7b9a8..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/MeasurePolicy.kt +++ /dev/null @@ -1,123 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layout - -import androidx.compose.runtime.Stable -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.modifiers.Constraints -import net.minecraft.client.gui.GuiGraphics - -/** - * Marker interface implemented by [LayoutNode] and passed as the first argument to - * [MeasurePolicy.measure]. Measure policies may cast this to [LayoutNode] to access - * node-level properties such as padding or margin modifiers during layout. - */ -interface MeasureScope - -/** - * The result of a [MeasurePolicy.measure] call, containing the intrinsic dimensions of the - * node and a [Placer] that positions child nodes within those bounds. - * - * @property width The measured width in pixels. - * @property height The measured height in pixels. - * @property placer The [Placer] that executes child placement when called. - */ -data class MeasureResult( - val width: Int, - val height: Int, - val placer: Placer, -) - -/** - * Defines how a [LayoutNode] measures itself and its children. - * - * The `scope` parameter is the [LayoutNode] currently being measured, allowing - * measure policies to read node properties (e.g. padding) during layout. - */ -@Stable -fun interface MeasurePolicy { - /** - * Measures [measurables] within [constraints] and returns a [MeasureResult]. - * - * @param scope The [LayoutNode] currently being measured (implements [MeasureScope]). - * @param measurables The child nodes to measure. - * @param constraints The size constraints imposed by the parent. - */ - fun measure(scope: MeasureScope, measurables: List, constraints: Constraints): MeasureResult -} - -/** - * A deferred child-placement action returned inside a [MeasureResult]. - * - * The [placeChildren] function is invoked by [LayoutNode] after measurement is complete to - * call [Placeable.placeAt] on each child. - */ -@Stable -fun interface Placer { - /** Executes all [Placeable.placeAt] calls for this layout pass. */ - fun placeChildren() -} - -/** - * Defines the rendering behaviour of a [LayoutNode]. - * - * Both [render] and [renderAfterChildren] have no-op defaults so implementors only - * override what they need. - */ -@Stable -interface Renderer { - /** - * Called before the node's children are rendered. - * - * Use this for backgrounds, borders, or content that should appear *below* children. - */ - fun render( - node: UINode, x: Int, y: Int, - guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, - ) {} - - /** - * Called after all children have been rendered. - * - * Use this for overlays or post-process effects that should appear *above* children. - */ - fun renderAfterChildren( - node: UINode, x: Int, y: Int, - guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float, - ) {} -} - -/** - * A node that can participate in a layout pass by returning a [Placeable]. - */ -interface Measurable { - /** - * Measures this node within [constraints] and returns a [Placeable] for placement. - * - * @param constraints The size constraints imposed by the parent. - */ - fun measure(constraints: Constraints): Placeable -} - -/** - * The result of measuring a node, which can subsequently be positioned via [placeAt]. - */ -interface Placeable { - /** The measured width in pixels. */ - var width: Int - - /** The measured height in pixels. */ - var height: Int - - /** - * Places this node at the given screen coordinates. - * - * @param x Absolute x position in screen pixels. - * @param y Absolute y position in screen pixels. - */ - fun placeAt(x: Int, y: Int) - - /** Places this node using an [IntOffset] convenience type. */ - fun placeAt(offset: IntOffset) = placeAt(offset.x, offset.y) - - /** The measured size as an [IntSize] value. */ - val size: IntSize get() = IntSize(width, height) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Row.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Row.kt deleted file mode 100644 index 264077141..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Row.kt +++ /dev/null @@ -1,80 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layout - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.modifiers.position.MarginModifier -import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier -import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingValues - -/** - * A layout composable that arranges its children in a horizontal sequence from left to right. - * - * Children are measured sequentially and their widths subtracted from the available space. - * Use [horizontalArrangement] to control spacing and alignment along the main axis, and - * [verticalAlignment] to align children along the cross axis. - * - * ### Example - * ```kotlin - * Row( - * horizontalArrangement = Arrangement.spacedBy(8), - * verticalAlignment = Alignment.CenterVertically, - * ) { - * Icon(...) - * Text(Component.literal("Label")) - * } - * ``` - * - * @param modifier Modifiers applied to the Row node. - * @param horizontalArrangement Controls spacing and placement along the horizontal axis. - * @param verticalAlignment Controls alignment of children along the vertical axis. - * @param content The child composables to lay out in a row. - */ -@Composable -fun Row( - modifier: Modifier = Modifier, - horizontalArrangement: Arrangement.Horizontal = Arrangement.Start, - verticalAlignment: Alignment.Vertical = Alignment.Top, - content: @Composable () -> Unit -) { - val measurePolicy = remember(horizontalArrangement, verticalAlignment) { - RowMeasurePolicy( - horizontalArrangement, - verticalAlignment - ) - } - Layout( - name = "Row", - measurePolicy, - modifier = modifier, - content = content - ) -} - -private data class RowMeasurePolicy( - private val horizontalArrangement: Arrangement.Horizontal, - private val verticalAlignment: Alignment.Vertical, -) : RowColumnMeasurePolicy(sumWidth = true, arrangementSpacing = horizontalArrangement.spacing) { - override fun placeChildren(scope: MeasureScope, measurables: List, placeables: List, width: Int, height: Int): MeasureResult { - val childCount = placeables.size - val positions = IntArray(childCount) - val sizes = IntArray(childCount) - for (index in 0 until childCount) { - sizes[index] = placeables[index].width - } - - horizontalArrangement.arrange(totalSize = width, sizes = sizes, layoutDirection = LayoutDirection.Ltr, outPositions = positions) - - return MeasureResult(width, height) { - val inset = (scope as? LayoutNode)?.get()?.padding - ?: PaddingValues() - var accumulatedOutset = 0 - - for (index in 0 until childCount) { - val child = placeables[index] - child.placeAt(positions[index] + accumulatedOutset + inset.left, verticalAlignment.align(child.height, height) + inset.top) - (measurables[index] as? LayoutNode)?.get()?.let { accumulatedOutset += it.horizontal } - } - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/RowColumnMeasurePolicy.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/RowColumnMeasurePolicy.kt deleted file mode 100644 index d607375b0..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/RowColumnMeasurePolicy.kt +++ /dev/null @@ -1,71 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layout - -import net.kernelpanicsoft.archie.gui.modifiers.Constraints -import kotlin.math.max - -/** - * Base [MeasurePolicy] for [Row] and [Column] layouts. - * - * Handles sequential measurement (subtracting consumed space when [sumWidth] or [sumHeight] - * is `true`) and delegates child placement to [placeChildren]. - * - * @param sumWidth When `true`, each child's width is subtracted from the remaining - * max-width before the next child is measured (Row behaviour). - * @param sumHeight When `true`, each child's height is subtracted from the remaining - * max-height before the next child is measured (Column behaviour). - * @param arrangementSpacing Additional pixels added between siblings by the arrangement. - */ -abstract class RowColumnMeasurePolicy( - val sumWidth: Boolean = false, - val sumHeight: Boolean = false, - val arrangementSpacing: Int = 0, -) : MeasurePolicy { - - override fun measure(scope: MeasureScope, measurables: List, constraints: Constraints): MeasureResult { - var remaining = constraints.copy(minWidth = 0, minHeight = 0) - val placeables = ArrayList(measurables.size) - var widthValue = 0 - var heightValue = 0 - - for (index in measurables.indices) { - val measured = measurables[index].measure(remaining) - placeables += measured - - if (sumWidth) widthValue += measured.width else widthValue = max(widthValue, measured.width) - if (sumHeight) heightValue += measured.height else heightValue = max(heightValue, measured.height) - - remaining = remaining.copy( - maxWidth = if (sumWidth) (remaining.maxWidth - measured.width).coerceAtLeast(0) else remaining.maxWidth, - maxHeight = if (sumHeight) (remaining.maxHeight - measured.height).coerceAtLeast(0) else remaining.maxHeight, - ) - } - - val extraSpacing = (arrangementSpacing * (placeables.size - 1)).coerceAtLeast(0) - val width = if (sumWidth) widthValue + extraSpacing else widthValue - val height = if (sumHeight) heightValue + extraSpacing else heightValue - - return placeChildren( - scope, measurables, placeables, - max(width, constraints.minWidth), - max(height, constraints.minHeight), - ) - } - - /** - * Positions all measured [placeables] within [width] × [height] and returns the - * [MeasureResult]. - * - * @param scope The measuring [LayoutNode]. - * @param measurables The original measurables (for modifier access). - * @param placeables The measured placeables to position. - * @param width The resolved container width. - * @param height The resolved container height. - */ - abstract fun placeChildren( - scope: MeasureScope, - measurables: List, - placeables: List, - width: Int, - height: Int, - ): MeasureResult -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Size.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Size.kt deleted file mode 100644 index 2ac00c7ad..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Size.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.kernelpanicsoft.archie.gui.layout - -import androidx.compose.runtime.Immutable -import kotlinx.serialization.Serializable - -/** - * An integer width/height pair. Unlike [IntSize], this is a regular [data class][Size] (not an - * inline value class), which makes it convenient where a boxed, nullable, or default-constructed - * size is needed, e.g. component configuration. - */ -@Immutable -@Serializable -data class Size( - val width: Int = 0, - val height: Int = 0 -) \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Constraints.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Constraints.kt deleted file mode 100644 index 95c2bbb47..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Constraints.kt +++ /dev/null @@ -1,80 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.kernelpanicsoft.archie.gui.modifiers - -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.Stable - -@Immutable -/** - * Immutable size constraints passed from a parent layout to its children during measurement. - * - * A child must produce a size whose width is in `[minWidth, maxWidth]` and whose height is - * in `[minHeight, maxHeight]`. Use [copy] to derive a modified copy with some dimensions changed, - * and [offset] to shrink the available space by a fixed amount (e.g. for padding). - * - * @property minWidth Minimum allowed width in pixels (inclusive). - * @property maxWidth Maximum allowed width in pixels (inclusive). - * @property minHeight Minimum allowed height in pixels (inclusive). - * @property maxHeight Maximum allowed height in pixels (inclusive). - */ -class Constraints( - val minWidth: Int = 0, - val maxWidth: Int = Int.MAX_VALUE, - val minHeight: Int = 0, - val maxHeight: Int = Int.MAX_VALUE -) { - fun copy( - minWidth: Int = this.minWidth, - maxWidth: Int = this.maxWidth, - minHeight: Int = this.minHeight, - maxHeight: Int = this.maxHeight - ) = Constraints( - minWidth.coerceAtMost(maxWidth), - maxWidth.coerceAtLeast(minWidth), - minHeight.coerceAtMost(maxHeight), - maxHeight.coerceAtLeast(minHeight) - ) - - override fun toString(): String - { - return "Constraints(minWidth=$minWidth, maxWidth=$maxWidth, minHeight=$minHeight, maxHeight=$maxHeight)" - } -} - -/** - * Returns a copy of these [Constraints] expanded or shrunk by [horizontal] pixels on each - * horizontal side and [vertical] pixels on each vertical side. - * - * Negative values shrink the available space (useful for padding). - * [Constraints.maxWidth] and [Constraints.maxHeight] are never reduced below zero. - */ -@Stable -fun Constraints.offset(horizontal: Int = 0, vertical: Int = 0) = Constraints( - (minWidth + horizontal).coerceAtLeast(0), - addMaxWithMinimum(maxWidth, horizontal), - (minHeight + vertical).coerceAtLeast(0), - addMaxWithMinimum(maxHeight, vertical) -) - -private fun addMaxWithMinimum(max: Int, value: Int): Int { - return if (max == Int.MAX_VALUE) { - max - } else { - (max + value).coerceAtLeast(0) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DebugModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DebugModifier.kt deleted file mode 100644 index eaf33a45c..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DebugModifier.kt +++ /dev/null @@ -1,55 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers - -import androidx.compose.runtime.Stable -import net.minecraft.network.chat.Component - -/** - * A [Modifier.Element] that attaches arbitrary debug information to a composable node. - * - * Debug information is only visible when the debug overlay is active (toggle with - * **Ctrl + Shift** while the screen is open). When [net.kernelpanicsoft.archie.gui.layout.LayoutNode.extraDebug] - * is enabled (hold Shift in debug mode), the attached strings and components are rendered - * inside the debug tooltip alongside node dimensions and coordinates. - * - * Multiple [DebugModifier] elements on the same node are merged by concatenation. - * - * @property strs Plain-text debug strings. - * @property comps Formatted [Component] debug labels. - */ -data class DebugModifier( - val strs: List = emptyList(), - val comps: List = emptyList(), -) : Modifier.Element { - - override fun mergeWith(other: DebugModifier): DebugModifier = - DebugModifier(strs = strs + other.strs, comps = comps + other.comps) - - override fun toString(): String = strs.joinToString(", ").ifEmpty { super.toString() } - - override fun toComponent(): Component = Component.empty().apply { - strs.map { Component.literal(it) }.forEach { append(it) } - comps.forEach { append(it) } - }.takeIf { it != Component.empty() } ?: Component.literal(super.toString()) - - /** Returns the debug information as a list of individual [Component]s, one per item. */ - fun toComponents(): List = - (strs.map { Component.literal(it) } + comps).ifEmpty { listOf(Component.literal(super.toString())) } -} - -/** - * Attaches one or more plain-text debug strings to the composable. - * - * The strings are displayed in the debug overlay when debug mode is active. - * - * @param strs The strings to attach. - */ -@Stable -fun Modifier.debug(vararg strs: String): Modifier = this then DebugModifier(strs = strs.toList()) - -/** - * Attaches one or more formatted [Component] debug labels to the composable. - * - * @param comps The components to attach. - */ -@Stable -fun Modifier.debug(vararg comps: Component): Modifier = this then DebugModifier(comps = comps.toList()) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DrawModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DrawModifier.kt deleted file mode 100644 index 6e7e1d1fe..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DrawModifier.kt +++ /dev/null @@ -1,51 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers - -import net.minecraft.client.gui.GuiGraphics - -/** - * A [Modifier] element that can participate in the draw chain for a composable node. - * - * [DrawModifier]s wrap the node's normal render call, allowing effects to be drawn - * **before** (e.g. a background fill) or **after** (e.g. an overlay) the node's own - * content. The modifier calls [ContentDrawScope.drawContent] to trigger the wrapped render. - * - * Implement [draw] inside the modifier to define the drawing logic. - */ -interface DrawModifier { - /** - * Performs custom drawing for this modifier. - * - * Call [ContentDrawScope.drawContent] at the desired point to render the wrapped content. - * Omitting the call suppresses the node's normal rendering entirely. - */ - fun ContentDrawScope.draw() -} - -/** - * Receiver scope provided to [DrawModifier.draw] containing everything needed to render - * and position content. - */ -interface ContentDrawScope { - /** The current [GuiGraphics] context. */ - val guiGraphics: GuiGraphics - - /** The width of the node being drawn, in pixels. */ - val width: Int - - /** The height of the node being drawn, in pixels. */ - val height: Int - - /** The absolute x coordinate of the node's top-left corner on screen. */ - val x: Int - - /** The absolute y coordinate of the node's top-left corner on screen. */ - val y: Int - - /** - * Renders the wrapped content (the node's own renderer and all child nodes). - * - * Call this at any point inside [DrawModifier.draw] to position the content - * relative to any surrounding draw operations. - */ - fun drawContent() -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/LayoutChangingModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/LayoutChangingModifier.kt deleted file mode 100644 index 819c1e3b3..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/LayoutChangingModifier.kt +++ /dev/null @@ -1,47 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers - -import net.kernelpanicsoft.archie.gui.layout.IntOffset -import net.kernelpanicsoft.archie.gui.layout.IntSize - -/** - * A [Modifier.Element] that can alter a node's position and the [Constraints] seen by the - * node and its children. - * - * Implement this interface alongside [Modifier.Element] when a modifier needs to change - * where the node is placed (e.g. [net.kernelpanicsoft.archie.gui.modifiers.position.OffsetModifier], - * [net.kernelpanicsoft.archie.gui.modifiers.position.MarginModifier]) or the space available - * to the node's subtree (e.g. [net.kernelpanicsoft.archie.gui.modifiers.SizeModifier], - * [net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier]). - */ -interface LayoutChangingModifier { - /** - * Shifts the node's placement by transforming the parent-supplied [offset]. - * - * @param offset The position computed by the parent layout. - * @return The adjusted position for this node. - */ - fun modifyPosition(offset: IntOffset): IntOffset = offset - - /** - * Adjusts the [Constraints] as seen by the **parent** when it lays out this node. - * - * Use this to report a different effective size to the parent (e.g. after accounting for - * margin space that the parent should reserve). - * - * @param measuredSize The actual measured size of this node. - * @param constraints The constraints originally passed by the parent. - * @return The constraints the parent should use when accounting for this node's footprint. - */ - fun modifyLayoutConstraints(measuredSize: IntSize, constraints: Constraints): Constraints = - modifyInnerConstraints(constraints) - - /** - * Adjusts the [Constraints] passed **into** this node for measuring its children. - * - * Use this to reduce the available space before measuring children (e.g. padding). - * - * @param constraints The constraints supplied by this node's parent. - * @return The constraints to use when measuring this node's children. - */ - fun modifyInnerConstraints(constraints: Constraints): Constraints = constraints -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Modifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Modifier.kt deleted file mode 100644 index 8dbe4f75a..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Modifier.kt +++ /dev/null @@ -1,153 +0,0 @@ -/* - * Copyright 2019 The Android Open Source Project - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package net.kernelpanicsoft.archie.gui.modifiers - -import net.minecraft.network.chat.Component - -/** - * An ordered, immutable collection of [modifier elements][Modifier.Element] that decorate or add - * behavior to Compose UI elements. For example, backgrounds, padding and click event listeners - * decorate or add behavior to rows, text or buttons. - * - * This class is taken from the androidx Jetpack Compose UI library so as to avoid extra dependencies. - */ -interface Modifier { - - /** - * Accumulates a value starting with [initial] and applying [operation] to the current value - * and each element from outside in. - * - * Elements wrap one another in a chain from left to right; an [Element] that appears to the - * left of another in a `+` expression or in [operation]'s parameter order affects all - * of the elements that appear after it. [foldIn] may be used to accumulate a value starting - * from the parent or head of the modifier chain to the final wrapped child. - */ - fun foldIn(initial: R, operation: (R, Element<*>) -> R): R - - /** - * Accumulates a value starting with [initial] and applying [operation] to the current value - * and each element from inside out. - * - * Elements wrap one another in a chain from left to right; an [Element] that appears to the - * left of another in a `+` expression or in [operation]'s parameter order affects all - * of the elements that appear after it. [foldOut] may be used to accumulate a value starting - * from the child or tail of the modifier chain up to the parent or head of the chain. - */ - fun foldOut(initial: R, operation: (Element<*>, R) -> R): R - - /** - * Returns `true` if [predicate] returns true for any [Element] in this [Modifier]. - */ - fun any(predicate: (Element<*>) -> Boolean): Boolean - - /** - * Returns `true` if [predicate] returns true for all [Element]s in this [Modifier] or if - * this [Modifier] contains no [Element]s. - */ - fun all(predicate: (Element<*>) -> Boolean): Boolean - - /** - * Concatenates this modifier with another. - * - * Returns a [Modifier] representing this modifier followed by [other] in sequence. - */ - infix fun then(other: Modifier): Modifier = - if (other === Modifier) this else CombinedModifier(this, other) - - /** - * A single element contained within a [Modifier] chain. - */ - interface Element> : Modifier { - override fun foldIn(initial: R, operation: (R, Element<*>) -> R): R = - operation(initial, this) - - override fun foldOut(initial: R, operation: (Element<*>, R) -> R): R = - operation(this, initial) - - override fun any(predicate: (Element<*>) -> Boolean): Boolean = predicate(this) - - override fun all(predicate: (Element<*>) -> Boolean): Boolean = predicate(this) - - fun mergeWith(other: Self): Self - - @Suppress("UNCHECKED_CAST") - private fun castSelf(other: Element<*>): Self = other as Self - - fun unsafeMergeWith(other: Element<*>) = mergeWith(castSelf(other)) - - /** - * Converts this modifier element to a debug [Component] representation. - */ - fun toComponent(): Component = Component.literal(toString()) - } - - /** - * The companion object `Modifier` is the empty, default, or starter [Modifier] - * that contains no [elements][Element]. Use it to create a new [Modifier] using - * modifier extension factory functions. - */ - // The companion object implements `Modifier` so that it may be used as the start of a - // modifier extension factory expression. - companion object : Modifier { - override fun foldIn(initial: R, operation: (R, Element<*>) -> R): R = initial - override fun foldOut(initial: R, operation: (Element<*>, R) -> R): R = initial - override fun any(predicate: (Element<*>) -> Boolean): Boolean = false - override fun all(predicate: (Element<*>) -> Boolean): Boolean = true - override infix fun then(other: Modifier): Modifier = other - override fun toString() = "Modifier" - } -} - -/** - * A node in a [Modifier] chain. A CombinedModifier always contains at least two elements; - * a Modifier [outer] that wraps around the Modifier [inner]. - */ -class CombinedModifier( - private val outer: Modifier, - private val inner: Modifier -) : Modifier { - override fun foldIn(initial: R, operation: (R, Modifier.Element<*>) -> R): R = - inner.foldIn(outer.foldIn(initial, operation), operation) - - override fun foldOut(initial: R, operation: (Modifier.Element<*>, R) -> R): R = - outer.foldOut(inner.foldOut(initial, operation), operation) - - override fun any(predicate: (Modifier.Element<*>) -> Boolean): Boolean = - outer.any(predicate) || inner.any(predicate) - - override fun all(predicate: (Modifier.Element<*>) -> Boolean): Boolean = - outer.all(predicate) && inner.all(predicate) - - override fun equals(other: Any?): Boolean = - other is CombinedModifier && outer == other.outer && inner == other.inner - - override fun hashCode(): Int = outer.hashCode() + 31 * inner.hashCode() - - override fun toString() = "[" + foldIn("") { acc, element -> - if (acc.isEmpty()) element.toString() else "$acc, $element" - } + "]" -} - -/** - * Collects all [Modifier.Element] instances of type [T] from this modifier chain. - * - * @return A list of all modifier elements matching type [T], in declaration order. - */ -inline fun > Modifier.getAll(): List = - foldIn(mutableListOf()) { acc, element -> - if (element is T) acc.apply { add(element) } else acc - } \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnGloballyPositionedModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnGloballyPositionedModifier.kt deleted file mode 100644 index 2a377a67b..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnGloballyPositionedModifier.kt +++ /dev/null @@ -1,33 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers - -import net.kernelpanicsoft.archie.gui.layout.IntCoordinates - -/** - * A [Modifier.Element] that invokes [onGloballyPositioned] with the node's absolute on-screen - * coordinates whenever it is placed by [net.kernelpanicsoft.archie.gui.layout.LayoutNode.placeAt]. - * - * Multiple [OnGloballyPositionedModifier] elements on the same node are merged so that every - * callback in the chain fires, in declaration order, for each placement. - * - * @property merged Internal flag marking whether this instance already wraps other merged - * callbacks; set automatically by [mergeWith], not intended to be passed by callers. - * @property onGloballyPositioned Invoked with the node's absolute [IntCoordinates] on placement. - */ -class OnGloballyPositionedModifier( - val merged: Boolean = false, - val onGloballyPositioned: (IntCoordinates) -> Unit -) : Modifier.Element -{ - override fun mergeWith(other: OnGloballyPositionedModifier): OnGloballyPositionedModifier = OnGloballyPositionedModifier(merged = true) { position -> - if (!other.merged) - onGloballyPositioned(position) - other.onGloballyPositioned(position) - } - -} - -/** - * Registers [onGloballyPositioned] to be called with the node's absolute screen coordinates - * every time it is placed (e.g. on layout changes). - */ -fun Modifier.onGloballyPositioned(onGloballyPositioned: (IntCoordinates) -> Unit) = this then OnGloballyPositionedModifier(onGloballyPositioned = onGloballyPositioned) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnSizeChangedModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnSizeChangedModifier.kt deleted file mode 100644 index 23f63c8cf..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnSizeChangedModifier.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers - -import net.kernelpanicsoft.archie.gui.layout.Size - -/** - * A [Modifier.Element] that invokes [onSizeChanged] whenever the node's measured [Size] changes - * between layout passes. - * - * Multiple [OnSizeChangedModifier] elements on the same node are merged so that every callback - * in the chain fires, in declaration order, for each size change. - * - * @property merged Internal flag marking whether this instance already wraps other merged - * callbacks; set automatically by [mergeWith], not intended to be passed by callers. - * @property onSizeChanged Invoked with the node's new measured [Size]. - */ -class OnSizeChangedModifier( - val merged: Boolean = false, - val onSizeChanged: (Size) -> Unit -) : Modifier.Element { - override fun mergeWith(other: OnSizeChangedModifier) = OnSizeChangedModifier(merged = true) { size -> - if (!other.merged) - onSizeChanged(size) - other.onSizeChanged(size) - } -} - -/** Notifies callback of any size changes to element. */ -fun Modifier.onSizeChanged(onSizeChanged: (Size) -> Unit) = this then OnSizeChangedModifier(onSizeChanged = onSizeChanged) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/SizeModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/SizeModifier.kt deleted file mode 100644 index 2227ba2a7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/SizeModifier.kt +++ /dev/null @@ -1,132 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers - -import androidx.compose.runtime.Stable -import kotlin.math.roundToInt - -/** - * A [Modifier.Element] that constrains the intrinsic size of a composable node by clamping - * the [Constraints] passed to it during measurement. - * - * Multiple [SizeModifier] elements on the same node are merged by intersecting their ranges, - * so the resulting constraints satisfy all modifiers simultaneously. - * - * Prefer the extension functions ([size], [sizeIn], [width], [height]) over constructing - * this class directly. - * - * @property constraints The [Constraints] to enforce. - */ -data class SizeModifier( - val constraints: Constraints -) : Modifier.Element, LayoutChangingModifier { - override fun mergeWith(other: SizeModifier) = with(constraints) { - SizeModifier( - Constraints( - other.constraints.minWidth.coerceIn(minWidth, maxWidth), - other.constraints.maxWidth.coerceIn(minWidth, maxWidth), - other.constraints.minHeight.coerceIn(minHeight, maxHeight), - other.constraints.maxHeight.coerceIn(minHeight, maxHeight), - ) - ) - } - - override fun modifyInnerConstraints(constraints: Constraints): Constraints { - return SizeModifier(constraints).mergeWith(this).constraints - } -} - -/** - * A [LayoutChangingModifier] that forces the node to fill a [percent] fraction of the - * available horizontal space. - * - * @property percent Fraction of available width to fill (0.0–1.0, default 1.0 = full width). - */ -data class HorizontalFillModifier( - val percent: Double -) : Modifier.Element, LayoutChangingModifier { - override fun mergeWith(other: HorizontalFillModifier) = other - - override fun modifyInnerConstraints(constraints: Constraints): Constraints { - val fillWidth = (constraints.minWidth + percent * (constraints.maxWidth - constraints.minWidth)).roundToInt() - return constraints.copy( - minWidth = fillWidth, - maxWidth = fillWidth - ) - } -} - -/** - * A [LayoutChangingModifier] that forces the node to fill a [percent] fraction of the - * available vertical space. - * - * @property percent Fraction of available height to fill (0.0–1.0, default 1.0 = full height). - */ -data class VerticalFillModifier( - val percent: Double -) : Modifier.Element, LayoutChangingModifier { - override fun mergeWith(other: VerticalFillModifier) = other - - override fun modifyInnerConstraints(constraints: Constraints): Constraints { - val fillHeight = - (constraints.minHeight + percent * (constraints.maxHeight - constraints.minHeight)).roundToInt() - return constraints.copy( - minHeight = fillHeight, - maxHeight = fillHeight - ) - } -} - -/** - * Forces the node to fill [percent] of the maximum available width. - * - * @param percent Fraction of available width (0.0–1.0). Default `1.0` fills all available width. - */ -@Stable -fun Modifier.fillMaxWidth(percent: Double = 1.0) = then(HorizontalFillModifier(percent)) - -/** - * Forces the node to fill [percent] of the maximum available height. - * - * @param percent Fraction of available height (0.0–1.0). Default `1.0` fills all available height. - */ -@Stable -fun Modifier.fillMaxHeight(percent: Double = 1.0) = then(VerticalFillModifier(percent)) - -/** - * Forces the node to fill [percent] of both the available width and height. - * - * @param percent Fraction of available space (0.0–1.0). Default `1.0` fills all available space. - */ -@Stable -fun Modifier.fillMaxSize(percent: Double = 1.0) = then(HorizontalFillModifier(percent)).then(VerticalFillModifier(percent)) - -/** - * Constrains the node's width and height to be within the given min/max bounds. - * - * @param minWidth Minimum width in pixels. - * @param maxWidth Maximum width in pixels. - * @param minHeight Minimum height in pixels. - * @param maxHeight Maximum height in pixels. - */ -@Stable -fun Modifier.sizeIn( - minWidth: Int = 0, - maxWidth: Int = Integer.MAX_VALUE, - minHeight: Int = 0, - maxHeight: Int = Integer.MAX_VALUE, -) = then(SizeModifier(Constraints(minWidth, maxWidth, minHeight, maxHeight))) - -/** Sets an exact fixed size of [width] × [height] pixels. */ -@Stable -fun Modifier.size(width: Int, height: Int) = sizeIn(width, width, height, height) - -/** Sets an exact fixed square size of [size] × [size] pixels. */ -@Stable -fun Modifier.size(size: Int) = size(size, size) - -/** Sets an exact fixed width of [width] pixels (height unconstrained). */ -@Stable -fun Modifier.width(width: Int) = sizeIn(width, width, 0, Integer.MAX_VALUE) - -/** Sets an exact fixed height of [height] pixels (width unconstrained). */ -@Stable -fun Modifier.height(height: Int) = sizeIn(0, Integer.MAX_VALUE, height, height) \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BackgroundModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BackgroundModifier.kt deleted file mode 100644 index 01e41c0b6..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BackgroundModifier.kt +++ /dev/null @@ -1,97 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers.appearance - -import androidx.compose.runtime.Stable -import net.kernelpanicsoft.archie.gui.modifiers.ContentDrawScope -import net.kernelpanicsoft.archie.gui.modifiers.DrawModifier -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.util.KColor -import net.kernelpanicsoft.archie.gui.util.extension.fillGradient -import net.kernelpanicsoft.archie.gui.util.extension.invoke - -/** - * The direction along which a background gradient transitions. - */ -enum class GradientDirection { - TOP_TO_BOTTOM, - RIGHT_TO_LEFT, - LEFT_TO_RIGHT, - BOTTOM_TO_TOP, -} - -/** - * A [DrawModifier] that fills a composable's background with a solid colour or a two-stop - * linear gradient. - * - * When [startColor] and [endColor] are equal the fill is solid; otherwise the two colours - * are interpolated across the node bounds in [gradientDirection]. - * - * @property startColor ARGB packed start colour. - * @property endColor ARGB packed end colour. - * @property gradientDirection The direction of the gradient transition. - */ -data class BackgroundModifier( - val startColor: Int, - val endColor: Int, - val gradientDirection: GradientDirection = GradientDirection.TOP_TO_BOTTOM, -) : Modifier.Element, DrawModifier { - - override fun ContentDrawScope.draw() { - guiGraphics { - val (topLeft, topRight, bottomLeft, bottomRight) = when (gradientDirection) - { - GradientDirection.TOP_TO_BOTTOM -> listOf(startColor, startColor, endColor, endColor) - GradientDirection.BOTTOM_TO_TOP -> listOf(endColor, endColor, startColor, startColor) - GradientDirection.LEFT_TO_RIGHT -> listOf(startColor, endColor, startColor, endColor) - GradientDirection.RIGHT_TO_LEFT -> listOf(endColor, startColor, endColor, startColor) - } - fillGradient(x, y, width, height, topLeft, topRight, bottomLeft, bottomRight) - } - drawContent() - } - - override fun mergeWith(other: BackgroundModifier): BackgroundModifier = other - - override fun toString(): String = - if (startColor == endColor) - "BackgroundModifier(color=#${String.format("%08X", startColor)})" - else - "BackgroundModifier(startColor=#${String.format("%08X", startColor)}, endColor=#${String.format("%08X", endColor)}, direction=$gradientDirection)" -} - -/** - * Fills the composable's background with a solid [color]. - */ -@Stable fun Modifier.background(color: KColor): Modifier = - this then BackgroundModifier(color.argb, color.argb) - -/** - * Fills the composable's background with a gradient from [startColor] to [endColor] - * going top-to-bottom. - */ -@Stable fun Modifier.background(startColor: KColor, endColor: KColor): Modifier = - this then BackgroundModifier(startColor.argb, endColor.argb) - -/** - * Fills the composable's background with a gradient from [startColor] to [endColor] - * in the given [gradientDirection]. - */ -@Stable fun Modifier.background( - startColor: KColor, - endColor: KColor, - gradientDirection: GradientDirection = GradientDirection.TOP_TO_BOTTOM, -): Modifier = this then BackgroundModifier(startColor.argb, endColor.argb, gradientDirection) - -/** Fills the composable's background with a solid ARGB integer [color]. */ -@Stable fun Modifier.background(color: Int): Modifier = - this then BackgroundModifier(color, color) - -/** Fills the composable's background with a gradient between two ARGB integer colours. */ -@Stable fun Modifier.background(startColor: Int, endColor: Int): Modifier = - this then BackgroundModifier(startColor, endColor) - -/** Fills the composable's background with a directional gradient between two ARGB integer colours. */ -@Stable fun Modifier.background( - startColor: Int, - endColor: Int, - gradientDirection: GradientDirection = GradientDirection.TOP_TO_BOTTOM, -): Modifier = this then BackgroundModifier(startColor, endColor, gradientDirection) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BorderModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BorderModifier.kt deleted file mode 100644 index d2d76b076..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BorderModifier.kt +++ /dev/null @@ -1,53 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers.appearance - -import androidx.compose.runtime.Stable -import net.kernelpanicsoft.archie.gui.modifiers.ContentDrawScope -import net.kernelpanicsoft.archie.gui.modifiers.DrawModifier -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.kernelpanicsoft.archie.gui.util.KColor -import net.kernelpanicsoft.archie.gui.util.extension.drawRectOutline - -/** - * A [DrawModifier] that draws a rectangular border around a composable. - * - * The border is rendered **before** the composable's own content so that it appears - * underneath any child nodes. - * - * @property color ARGB packed border colour. - * @property thickness Border stroke width in pixels. - */ -data class BorderModifier( - val color: Int, - val thickness: Int, -) : Modifier.Element, DrawModifier { - - override fun mergeWith(other: BorderModifier): BorderModifier = other - - override fun ContentDrawScope.draw() { - guiGraphics.drawRectOutline(x, y, width, height, color, thickness) - drawContent() - } - - override fun toString(): String = - "BorderModifier(width=$thickness, color=#${String.format("%08X", color)})" -} - -/** - * Adds a border of [thickness] pixels and [color] to the composable. - * - * @param color The border colour. - * @param thickness The border stroke width in pixels (default 1). - */ -@Stable -fun Modifier.border(color: KColor, thickness: Int = 1): Modifier = - this then BorderModifier(color.argb, thickness) - -/** - * Adds a border using a raw ARGB integer [color]. - * - * @param color ARGB packed colour. - * @param thickness The border stroke width in pixels (default 1). - */ -@Stable -fun Modifier.border(color: Int, thickness: Int = 1): Modifier = - this then BorderModifier(color, thickness) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TextureModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TextureModifier.kt deleted file mode 100644 index 043eae1b8..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TextureModifier.kt +++ /dev/null @@ -1,26 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers.appearance - -import androidx.compose.runtime.Stable -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.minecraft.resources.ResourceLocation - -/** - * A [Modifier.Element] that overrides the texture used by certain theme-aware composables - * (such as [net.kernelpanicsoft.archie.gui.Slot]). - * - * Only the last applied [TextureModifier] on a node takes effect. - * - * @property texture The [ResourceLocation] of the replacement texture. - */ -data class TextureModifier(val texture: ResourceLocation) : Modifier.Element { - override fun mergeWith(other: TextureModifier): TextureModifier = - throw UnsupportedOperationException("TextureModifier cannot be merged; only one texture can be active at a time.") -} - -/** - * Overrides the texture of theme-aware composables with the given [ResourceLocation]. - * - * @param texture The replacement texture resource location. - */ -@Stable -fun Modifier.texture(texture: ResourceLocation): Modifier = this then TextureModifier(texture) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TooltipModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TooltipModifier.kt deleted file mode 100644 index 3cfe840ba..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TooltipModifier.kt +++ /dev/null @@ -1,29 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers.appearance - -import androidx.compose.runtime.Stable -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.minecraft.world.inventory.tooltip.TooltipComponent - -/** - * A [Modifier.Element] that attaches one or more [TooltipComponent]s to a composable. - * - * Multiple [TooltipModifier] elements on the same node are merged by concatenating their - * tooltip lists. - * - * @property tooltips The list of tooltip components to display. - */ -data class TooltipModifier(val tooltips: List) : Modifier.Element { - override fun mergeWith(other: TooltipModifier): TooltipModifier = - TooltipModifier(tooltips + other.tooltips) -} - -/** - * Attaches one or more [TooltipComponent]s to this composable. - * - * The tooltips are merged with any existing [TooltipModifier] on the node. - * - * @param tooltips The tooltip components to attach. - */ -@Stable -fun Modifier.tooltip(vararg tooltips: TooltipComponent): Modifier = - this then TooltipModifier(tooltips.toList()) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/InputEvent.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/InputEvent.kt deleted file mode 100644 index bc64e163f..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/InputEvent.kt +++ /dev/null @@ -1,112 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers.input - -/** - * Base class for all events dispatched through the composable input system. - * - * Events propagate through the node tree from the innermost child outward. Once - * [consume] is called the propagation stops for most event types. The [bypassSuper] - * flag additionally controls whether the originating [net.minecraft.client.gui.screens.Screen] - * method forwards the event to its Minecraft `super` implementation. - */ -sealed class InputEvent { - /** - * Whether this event has been consumed by a handler. - * - * Consumed events do not continue propagating to outer nodes. - */ - internal var isConsumed: Boolean = false - private set - - /** - * When `true`, the screen method that triggered this event will bypass the Minecraft - * `super` call (i.e. return `true` to absorb the input at the screen level). - */ - internal var bypassSuper: Boolean = false - private set - - /** - * Marks this event as consumed, stopping further propagation. - * - * @param bypassSuperCall When `true`, the corresponding screen method will return `true` - * instead of delegating to the `super` implementation. Use this when the UI has fully - * handled a keyboard or mouse event and the vanilla logic should be suppressed. - */ - fun consume(bypassSuperCall: Boolean = false) { - isConsumed = true - bypassSuper = bypassSuperCall - } -} - -/** - * Base class for all pointer (mouse) events. - * - * @property type The specific kind of pointer interaction. - * @property mouseX The current cursor x position in screen pixels. - * @property mouseY The current cursor y position in screen pixels. - */ -sealed class PointerEvent( - val type: PointerEventType, - val mouseX: Double, - val mouseY: Double, -) : InputEvent() - -/** A basic pointer event carrying position and type information. */ -class BasicPointerEvent( - type: PointerEventType, - mouseX: Double, - mouseY: Double, -) : PointerEvent(type, mouseX, mouseY) - -/** - * A pointer event generated by mouse wheel movement. - * - * @property scrollX Horizontal scroll delta. - * @property scrollY Vertical scroll delta. - */ -class ScrollEvent( - type: PointerEventType = PointerEventType.SCROLL, - mouseX: Double, - mouseY: Double, - val scrollX: Double, - val scrollY: Double, -) : PointerEvent(type, mouseX, mouseY) - -/** - * A pointer event generated by click-dragging the mouse. - * - * @property button The mouse button held during the drag (0 = left, 1 = right, 2 = middle). - * @property dragX Horizontal drag delta since the last frame. - * @property dragY Vertical drag delta since the last frame. - */ -class DragEvent( - type: PointerEventType = PointerEventType.DRAG, - mouseX: Double, - mouseY: Double, - val button: Int, - val dragX: Double, - val dragY: Double, -) : PointerEvent(type, mouseX, mouseY) - -/** - * An event generated by a keyboard key press. - * - * @property keyCode The GLFW key code. - * @property scanCode The platform-specific scan code. - * @property modifiers Bitmask of active modifier keys (Shift / Ctrl / Alt). - */ -data class KeyEvent( - val keyCode: Int, - val scanCode: Int, - val modifiers: Int, -) : InputEvent() - -/** - * An event generated when a printable character is typed. - * - * @property codePoint The typed character as a Unicode code point. - * @property modifiers Bitmask of active modifier keys. - */ -data class CharEvent( - val codePoint: Char, - val modifiers: Int, -) : InputEvent() diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnCharTypedModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnCharTypedModifier.kt deleted file mode 100644 index afb4bb0db..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnCharTypedModifier.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers.input - -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.modifiers.Modifier - -/** - * A [Modifier.Element] that registers a character-typed handler on a composable node. - * - * Multiple [OnCharTypedModifier] elements on the same node are **chained**: earlier handlers - * fire first, and later handlers fire only if the event was not yet consumed. - * - * @property onEvent The callback invoked with (node, [CharEvent]) when a character is typed. - */ -data class OnCharTypedModifier( - val onEvent: (UINode, CharEvent) -> Unit, -) : Modifier.Element { - - override fun mergeWith(other: OnCharTypedModifier): OnCharTypedModifier = - OnCharTypedModifier { node, event -> - onEvent(node, event) - if (!event.isConsumed) other.onEvent(node, event) - } -} - -/** - * Registers a character-typed handler on this composable. - * - * Called when the user types a printable character while the node (or a descendant) - * participates in key event dispatch. - * - * @param onEvent Callback invoked with (node, [CharEvent]). - */ -fun Modifier.onCharTyped(onEvent: (UINode, CharEvent) -> Unit): Modifier = - this then OnCharTypedModifier(onEvent) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnKeyEventModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnKeyEventModifier.kt deleted file mode 100644 index c6a19df7c..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnKeyEventModifier.kt +++ /dev/null @@ -1,36 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers.input - -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.modifiers.Modifier - -/** - * A [Modifier.Element] that registers a keyboard key-press handler on a composable node. - * - * Multiple [OnKeyEventModifier] elements on the same node are **chained**: the earlier - * handler fires first, and the later handler fires only if the event was not yet consumed. - * - * @property onEvent The callback invoked with (node, [KeyEvent]) on a key press. - */ -data class OnKeyEventModifier( - val onEvent: (UINode, KeyEvent) -> Unit, -) : Modifier.Element { - - override fun mergeWith(other: OnKeyEventModifier): OnKeyEventModifier = - OnKeyEventModifier { node, event -> - onEvent(node, event) - if (!event.isConsumed) other.onEvent(node, event) - } - - override fun toString(): String = "OnKeyEventModifier()" -} - -/** - * Registers a key-press handler on this composable. - * - * The handler is called when a keyboard key is pressed while the node (or a descendant) - * holds focus in the key event dispatch chain. - * - * @param onEvent Callback invoked with (node, [KeyEvent]). - */ -fun Modifier.onKeyEvent(onEvent: (UINode, KeyEvent) -> Unit): Modifier = - this then OnKeyEventModifier(onEvent) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnPointerEventModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnPointerEventModifier.kt deleted file mode 100644 index 63a27be46..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnPointerEventModifier.kt +++ /dev/null @@ -1,150 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers.input - -import androidx.compose.runtime.Stable -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.kernelpanicsoft.archie.gui.modifiers.Modifier - -/** Identifies the type of pointer (mouse) interaction that triggers an event handler. */ -enum class PointerEventType { - /** The primary or secondary mouse button was pressed inside the node's bounds. */ - PRESS, - /** - * A mouse button was pressed anywhere on the screen regardless of bounds. - * Useful for detecting clicks outside a focused element. - */ - GLOBAL_PRESS, - /** A mouse button was released inside the node's bounds. */ - RELEASE, - /** A mouse button was released anywhere on the screen. */ - GLOBAL_RELEASE, - /** The mouse cursor moved while inside the node's bounds. */ - MOVE, - /** The mouse cursor entered the node's bounds from outside. */ - ENTER, - /** The mouse cursor left the node's bounds. */ - EXIT, - /** The mouse wheel was scrolled over the node. */ - SCROLL, - /** The mouse wheel was scrolled anywhere on the screen. */ - GLOBAL_SCROLL, - /** The mouse was dragged (button held + moved) over the node. */ - DRAG, - /** The mouse was dragged anywhere on the screen. */ - GLOBAL_DRAG, -} - -/** Milliseconds a press must be held to qualify as a long-click. */ -const val LONG_CLICK_THRESHOLD = 500 - -/** Milliseconds within which two successive presses qualify as a double-click. */ -const val DOUBLE_CLICK_THRESHOLD = 300 - -/** - * A [Modifier.Element] that registers a callback for a specific [PointerEventType]. - * - * When multiple [OnPointerEventModifier] elements with the same [eventType] exist on a node, - * the **last** one replaces earlier ones (they do not chain). Use [combinedClickable] if - * you need multiple click behaviours on a single node. - * - * @param T The concrete [UINode] subtype the handler expects. - * @param eventType The pointer event that triggers [onEvent]. - * @param onEvent The handler invoked with the receiving node and the event. - */ -data class OnPointerEventModifier( - val eventType: PointerEventType, - val onEvent: (T, PointerEvent) -> Unit, -) : Modifier.Element> { - override fun mergeWith(other: OnPointerEventModifier<*>): OnPointerEventModifier<*> = other - override fun toString(): String = "OnPointerEventModifier(eventType=${eventType.name})" -} - -/** - * Registers a handler for the given pointer [type] on this composable. - * - * @param T The expected [UINode] subtype; use [UINode] for the generic case. - * @param type The [PointerEventType] to listen for. - * @param onEvent The callback invoked with (node, event) when the event occurs. - */ -@Stable -fun Modifier.onPointerEvent( - type: PointerEventType, - onEvent: (T, PointerEvent) -> Unit, -): Modifier = this then OnPointerEventModifier(type, onEvent) - -/** - * Registers a scroll event handler on this composable. - * - * @param global When `true`, the handler fires for scroll events anywhere on screen. - * @param onScrollEvent The callback invoked with (node, [ScrollEvent]). - */ -@Suppress("UNCHECKED_CAST") -@Stable -fun Modifier.onScroll( - global: Boolean = false, - onScrollEvent: (T, ScrollEvent) -> Unit, -): Modifier = this then OnPointerEventModifier( - if (global) PointerEventType.GLOBAL_SCROLL else PointerEventType.SCROLL, - onScrollEvent as (T, PointerEvent) -> Unit, -) - -/** - * Registers a drag event handler on this composable. - * - * @param global When `true`, the handler fires for drag events anywhere on screen. - * @param onDragEvent The callback invoked with (node, [DragEvent]). - */ -@Suppress("UNCHECKED_CAST") -@Stable -fun Modifier.onDrag( - global: Boolean = false, - onDragEvent: (T, DragEvent) -> Unit, -): Modifier = this then OnPointerEventModifier( - if (global) PointerEventType.GLOBAL_DRAG else PointerEventType.DRAG, - onDragEvent as (T, PointerEvent) -> Unit, -) - -/** - * Adds multiple click-type handlers to a composable in a single modifier. - * - * At least one of the three callbacks must be non-null. - * - * @param onLongClick Invoked when the node is held for more than [LONG_CLICK_THRESHOLD] ms. - * @param onDoubleClick Invoked when two presses occur within [DOUBLE_CLICK_THRESHOLD] ms. - * @param onClick Invoked on a normal single click (mouse release). - */ -@Stable -fun Modifier.combinedClickable( - onLongClick: ((T, PointerEvent) -> Unit)? = null, - onDoubleClick: ((T, PointerEvent) -> Unit)? = null, - onClick: ((T, PointerEvent) -> Unit)? = null, -): Modifier { - require(onClick != null || onLongClick != null || onDoubleClick != null) { - "You must specify at least one click handler" - } - var mod = this - - if (onLongClick != null) { - var clickStart = 0L - mod = mod - .onPointerEvent(PointerEventType.PRESS) { _, _ -> clickStart = System.currentTimeMillis() } - .onPointerEvent(PointerEventType.RELEASE) { node, event -> - if (clickStart != 0L && (System.currentTimeMillis() - clickStart) > LONG_CLICK_THRESHOLD) { - clickStart = 0L - onLongClick(node, event) - } - } - } - if (onDoubleClick != null) { - var clickStart = 0L - mod = mod.onPointerEvent(PointerEventType.PRESS) { node, event -> - if (clickStart != 0L && (System.currentTimeMillis() - clickStart) < DOUBLE_CLICK_THRESHOLD) { - clickStart = 0L - return@onPointerEvent onDoubleClick(node, event) - } - clickStart = System.currentTimeMillis() - } - } - if (onClick != null) mod = mod.onPointerEvent(PointerEventType.RELEASE, onClick) - - return mod -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/MarginModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/MarginModifier.kt deleted file mode 100644 index 436839603..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/MarginModifier.kt +++ /dev/null @@ -1,90 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers.position - -import androidx.compose.runtime.Stable -import net.kernelpanicsoft.archie.gui.layout.IntCoordinates -import net.kernelpanicsoft.archie.gui.modifiers.LayoutChangingModifier -import net.kernelpanicsoft.archie.gui.modifiers.Modifier - -/** - * Holds the four-sided margin values used by [MarginModifier]. - * - * @property left Left margin in pixels. - * @property right Right margin in pixels. - * @property top Top margin in pixels. - * @property bottom Bottom margin in pixels. - */ -data class MarginValues( - val left: Int = 0, - val right: Int = 0, - val top: Int = 0, - val bottom: Int = 0, -) { - /** Returns the top-left corner offset (left, top) as an [IntCoordinates]. */ - fun getOffset(): IntCoordinates = IntCoordinates(left, top) - - operator fun plus(other: MarginValues): MarginValues = MarginValues( - left + other.left, right + other.right, top + other.top, bottom + other.bottom, - ) -} - -/** - * A [Modifier.Element] that adds outer spacing (margin) around a composable. - * - * Margins are applied **outside** the node bounds and are accumulated additively when - * multiple [MarginModifier] elements are chained. - * - * @property margin The [MarginValues] describing each side's margin. - */ -data class MarginModifier(val margin: MarginValues) : Modifier.Element, LayoutChangingModifier { - override fun mergeWith(other: MarginModifier): MarginModifier = MarginModifier(margin + other.margin) - - /** Total horizontal margin (left + right). */ - val horizontal get() = margin.left + margin.right - - /** Total vertical margin (top + bottom). */ - val vertical get() = margin.top + margin.bottom - - override fun modifyPosition(offset: IntCoordinates): IntCoordinates = offset + margin.getOffset() - - override fun toString(): String = buildString { - append("MarginModifier(") - val sides = buildList { - if (margin.left != 0) add("left=${margin.left}") - if (margin.right != 0) add("right=${margin.right}") - if (margin.top != 0) add("top=${margin.top}") - if (margin.bottom != 0) add("bottom=${margin.bottom}") - } - append(sides.joinToString(", ")) - append(")") - } -} - -/** - * Adds independent per-side margins around this composable. - * - * @param left Left margin in pixels. - * @param right Right margin in pixels. - * @param top Top margin in pixels. - * @param bottom Bottom margin in pixels. - */ -@Stable -fun Modifier.margin(left: Int = 0, right: Int = 0, top: Int = 0, bottom: Int = 0): Modifier = - this then MarginModifier(MarginValues(left, right, top, bottom)) - -/** - * Adds symmetric horizontal and vertical margins. - * - * @param horizontal Margin applied to both the left and right sides. - * @param vertical Margin applied to both the top and bottom sides. - */ -@Stable -fun Modifier.margin(horizontal: Int = 0, vertical: Int = 0): Modifier = - margin(horizontal, horizontal, vertical, vertical) - -/** - * Adds a uniform margin on all four sides. - * - * @param all The margin in pixels applied to every side. - */ -@Stable -fun Modifier.margin(all: Int = 0): Modifier = margin(all, all, all, all) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/OffsetModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/OffsetModifier.kt deleted file mode 100644 index a3536ebe7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/OffsetModifier.kt +++ /dev/null @@ -1,34 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers.position - -import androidx.compose.runtime.Stable -import net.kernelpanicsoft.archie.gui.layout.IntOffset -import net.kernelpanicsoft.archie.gui.modifiers.LayoutChangingModifier -import net.kernelpanicsoft.archie.gui.modifiers.Modifier - -/** - * A [Modifier.Element] that shifts a composable's position by a fixed pixel offset after - * layout has been computed. - * - * The offset is applied on top of any position assigned by the parent layout; it does not - * affect the parent's size calculation. - * - * Only the **last** [OffsetModifier] in a chain takes effect. - * - * @property offset The pixel offset to apply as an [IntOffset] value. - */ -data class OffsetModifier(val offset: IntOffset) : Modifier.Element, LayoutChangingModifier { - override fun mergeWith(other: OffsetModifier): OffsetModifier = other - - override fun modifyPosition(offset: IntOffset): IntOffset = offset + this.offset -} - -/** - * Shifts the composable by ([x], [y]) pixels after layout. - * - * The shift does not affect the space reserved for the composable in its parent layout. - * - * @param x Horizontal pixel offset (positive moves right). - * @param y Vertical pixel offset (positive moves down). - */ -@Stable -fun Modifier.offset(x: Int = 0, y: Int = 0): Modifier = this then OffsetModifier(IntOffset(x, y)) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/PaddingModifier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/PaddingModifier.kt deleted file mode 100644 index 5db12d5f3..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/PaddingModifier.kt +++ /dev/null @@ -1,96 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers.position - -import androidx.compose.runtime.Stable -import net.kernelpanicsoft.archie.gui.layout.IntCoordinates -import net.kernelpanicsoft.archie.gui.modifiers.Constraints -import net.kernelpanicsoft.archie.gui.modifiers.LayoutChangingModifier -import net.kernelpanicsoft.archie.gui.modifiers.Modifier - -/** - * Holds the four-sided padding values used by [PaddingModifier]. - * - * @property left Left padding in pixels. - * @property right Right padding in pixels. - * @property top Top padding in pixels. - * @property bottom Bottom padding in pixels. - */ -data class PaddingValues( - val left: Int = 0, - val right: Int = 0, - val top: Int = 0, - val bottom: Int = 0, -) { - /** Returns the top-left corner offset (left, top) as an [IntCoordinates]. */ - fun getOffset(): IntCoordinates = IntCoordinates(left, top) - - operator fun plus(other: PaddingValues): PaddingValues = PaddingValues( - left + other.left, right + other.right, top + other.top, bottom + other.bottom, - ) -} - -/** - * A [Modifier.Element] that adds inner spacing (padding) inside a composable. - * - * Padding is applied **inside** the node bounds and reduces the available space for children. - * - * @property padding The [PaddingValues] describing each side's padding. - */ -data class PaddingModifier(val padding: PaddingValues) : Modifier.Element, LayoutChangingModifier { - override fun mergeWith(other: PaddingModifier): PaddingModifier = PaddingModifier(padding + other.padding) - - /** Total horizontal padding (left + right). */ - val horizontal get() = padding.left + padding.right - - /** Total vertical padding (top + bottom). */ - val vertical get() = padding.top + padding.bottom - - override fun modifyInnerConstraints(constraints: Constraints): Constraints = - constraints.copy( - maxWidth = (constraints.maxWidth - horizontal).coerceAtLeast(0), - maxHeight = (constraints.maxHeight - vertical).coerceAtLeast(0), - minWidth = (constraints.minWidth - horizontal).coerceAtLeast(0), - minHeight = (constraints.minHeight - vertical).coerceAtLeast(0), - ) - - override fun toString(): String = buildString { - append("PaddingModifier(") - val sides = buildList { - if (padding.left != 0) add("left=${padding.left}") - if (padding.right != 0) add("right=${padding.right}") - if (padding.top != 0) add("top=${padding.top}") - if (padding.bottom != 0) add("bottom=${padding.bottom}") - } - append(sides.joinToString(", ")) - append(")") - } -} - -/** - * Adds independent per-side padding inside this composable. - * - * @param left Left padding in pixels. - * @param right Right padding in pixels. - * @param top Top padding in pixels. - * @param bottom Bottom padding in pixels. - */ -@Stable -fun Modifier.padding(left: Int = 0, right: Int = 0, top: Int = 0, bottom: Int = 0): Modifier = - this then PaddingModifier(PaddingValues(left, right, top, bottom)) - -/** - * Adds symmetric horizontal and vertical padding. - * - * @param horizontal Padding applied to both the left and right sides. - * @param vertical Padding applied to both the top and bottom sides. - */ -@Stable -fun Modifier.padding(horizontal: Int = 0, vertical: Int = 0): Modifier = - padding(horizontal, horizontal, vertical, vertical) - -/** - * Adds uniform padding on all four sides. - * - * @param all The padding in pixels applied to every side. - */ -@Stable -fun Modifier.padding(all: Int = 0): Modifier = padding(all, all, all, all) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/ZIndex.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/ZIndex.kt deleted file mode 100644 index c60dcbf8a..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/ZIndex.kt +++ /dev/null @@ -1,33 +0,0 @@ -package net.kernelpanicsoft.archie.gui.modifiers.position - -import androidx.compose.runtime.Stable -import net.kernelpanicsoft.archie.gui.modifiers.Modifier - -/** - * A [Modifier.Element] that controls the rendering and input-dispatch order of a composable - * relative to its siblings. - * - * A higher [zIndex] causes the node to be drawn on top of siblings with lower z-indices and - * to receive input events first. The effective depth is accumulated hierarchically: each - * node's z-index is added to its parent's computed depth. - * - * When multiple [ZIndexModifier] elements are chained on the same node, the **last** one wins. - * - * @property zIndex The z-index value. Positive values move the node towards the viewer. - */ -data class ZIndexModifier(val zIndex: Float) : Modifier.Element { - /** When multiple z-index modifiers exist on the same node, the last one always wins. */ - override fun mergeWith(other: ZIndexModifier): ZIndexModifier = other - - override fun toString(): String = "ZIndexModifier(zIndex=$zIndex)" -} - -/** - * Sets the rendering depth of this composable relative to its siblings. - * - * Higher values appear on top and receive input events before lower-valued siblings. - * - * @param zIndex The z-index to apply. - */ -@Stable -fun Modifier.zIndex(zIndex: Float): Modifier = this then ZIndexModifier(zIndex) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/LayoutNodeApplier.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/LayoutNodeApplier.kt deleted file mode 100644 index 70561f268..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/LayoutNodeApplier.kt +++ /dev/null @@ -1,42 +0,0 @@ -package net.kernelpanicsoft.archie.gui.nodes - -import androidx.compose.runtime.AbstractApplier -import net.kernelpanicsoft.archie.gui.layout.LayoutNode - -/** - * Compose [AbstractApplier] that materializes composed UI into the [LayoutNode] tree rooted - * at [root]. Used as the applier for each [net.kernelpanicsoft.archie.gui.layer.Layer]'s - * [androidx.compose.runtime.Composition]. - */ -class LayoutNodeApplier(root: LayoutNode) : AbstractApplier(root) { - override fun insertTopDown(index: Int, instance: LayoutNode) { - // Ignored, we insert bottom-up. - } - - override fun insertBottomUp(index: Int, instance: LayoutNode) { - current.children.add(index, instance) - current.invalidateChildrenZCache() - check(instance.parent == null) { - "$instance must not have a parent when being inserted." - } - instance.parent = current - } - - override fun remove(index: Int, count: Int) { - repeat(count) { - current.children.removeAt(index).parent = null - } - current.invalidateChildrenZCache() - } - - override fun move(from: Int, to: Int, count: Int) { - current.children.move(from, to, count) - current.invalidateChildrenZCache() - } - - override fun onClear() { - current.children.forEach { it.parent = null } - current.children.clear() - current.invalidateChildrenZCache() - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/UINode.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/UINode.kt deleted file mode 100644 index e066a0da1..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/UINode.kt +++ /dev/null @@ -1,57 +0,0 @@ -package net.kernelpanicsoft.archie.gui.nodes - -import net.kernelpanicsoft.archie.gui.layout.LayoutNode -import net.kernelpanicsoft.archie.gui.layout.MeasurePolicy -import net.kernelpanicsoft.archie.gui.layout.Renderer -import net.kernelpanicsoft.archie.gui.modifiers.Modifier -import net.minecraft.client.gui.GuiGraphics - -/** - * A node in Archie's Compose-based UI tree, exposing the layout/render state a node needs - * regardless of its concrete representation. Implemented by [LayoutNode], the tree node type - * produced by [LayoutNodeApplier]. - */ -interface UINode { - /** Determines how this node measures and places its children. */ - var measurePolicy: MeasurePolicy - - /** Draws this node's own content (not its children) each frame. */ - var renderer: Renderer - - /** The chained [Modifier] applied to this node. */ - var modifier: Modifier - - /** This node's measured width, in pixels. */ - var width: Int - - /** This node's measured height, in pixels. */ - var height: Int - - /** This node's placed x position, in pixels, relative to its parent. */ - var x: Int - - /** This node's placed y position, in pixels, relative to its parent. */ - var y: Int - - /** - * The [net.kernelpanicsoft.archie.gui.composables.theme.TextureStates] key a stateful - * [Renderer] most recently selected to draw (e.g. `"hovered"`, `"clicked_and_hovered"`), - * or `null` for nodes that don't render theme-state-driven visuals. - * - * Set by the [Renderer] itself, purely as a test hook - lets a client GameTest assert which - * visual state a component resolved to without pixel comparison. Not read by the framework. - */ - var renderState: String? - - /** - * Renders this node and its subtree at the given absolute screen position. - * - * @param x Absolute screen x position to render at. - * @param y Absolute screen y position to render at. - * @param guiGraphics The graphics context to draw with. - * @param mouseX Current mouse x position, in screen space. - * @param mouseY Current mouse y position, in screen space. - * @param partialTick Fractional tick time for this frame, for smooth animation. - */ - fun render(x: Int, y: Int, guiGraphics: GuiGraphics, mouseX: Int, mouseY: Int, partialTick: Float) -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt deleted file mode 100644 index 57d3be9d5..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt +++ /dev/null @@ -1,21 +0,0 @@ -package net.kernelpanicsoft.archie.gui.render - -import net.minecraft.client.renderer.texture.TextureAtlasSprite -import net.minecraft.world.level.material.Fluid - -/** - * Cross-loader lookup of a [Fluid]'s client-rendering appearance, backed by an `actual` per mod - * loader - Fabric's `FluidRenderHandlerRegistry` and NeoForge's `IClientFluidTypeExtensions` - * expose the same information through unrelated APIs, so [net.kernelpanicsoft.archie.gui.composables.basic.FluidTank] - * goes through this instead of touching either directly. - * - * Client-only; only ever called from GUI rendering code. - */ -expect object AFluidRenderPlatform -{ - /** The fluid's still-texture sprite from the blocks atlas, or `null` if it can't be resolved. */ - fun getStillSprite(fluid: Fluid): TextureAtlasSprite? - - /** The ARGB tint color applied over [getStillSprite]'s sprite (`0xFFFFFFFF` = no tint). */ - fun getTintColor(fluid: Fluid): Int -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/ComposableTheme.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/ComposableTheme.kt deleted file mode 100644 index cf7feb180..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/ComposableTheme.kt +++ /dev/null @@ -1,287 +0,0 @@ -package net.kernelpanicsoft.archie.gui.theme - -import dev.architectury.registry.ReloadListenerRegistry -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.gui.composables.theme.TextureStates -import net.kernelpanicsoft.archie.gui.layout.Size -import net.kernelpanicsoft.archie.resourcepacks.SerializationReloadListener -import net.kernelpanicsoft.archie.serialization.SerializationManager -import net.kernelpanicsoft.archie.serialization.serializers.SResourceLocation -import net.minecraft.resources.ResourceLocation -import net.minecraft.server.packs.resources.PreparableReloadListener -import net.minecraft.server.packs.resources.ResourceManager -import net.minecraft.util.profiling.ProfilerFiller - -/* ─────────────────────── Theme state data classes ─────────────────────── */ - -/** Sealed base for composable theme states rendered as sprites. */ -@Serializable -sealed interface ThemeState { - /** Resource location of the sprite atlas or source image. */ - val texture: SResourceLocation - /** Full pixel size of the source image when UV rendering is used. */ - @SerialName("texture_size") val textureSize: Size - /** Horizontal UV offset within the atlas. */ - val u: Int - /** Vertical UV offset within the atlas. */ - val v: Int -} - -/** - * A fixed-size sprite slice within a sprite atlas or source image. - * - * @property width Rendered width in pixels. - * @property height Rendered height in pixels. - * @property uWidth Width of the source region in the atlas. - * @property vHeight Height of the source region in the atlas. - */ -@Serializable -data class SimpleThemeState( - override val texture: SResourceLocation, - @SerialName("texture_size") override val textureSize: Size, - override val u: Int = 0, - override val v: Int = 0, - val width: Int, - val height: Int, - @SerialName("u_width") val uWidth: Int, - @SerialName("v_height") val vHeight: Int, -) : ThemeState - -/** A map of state-name → [ThemeState] for a single variant of a composable. */ -@Serializable -data class StatefulTheme(val states: Map) - -/** Raw JSON shape of a theme file, deserialized as-is and resolved by [ThemeResourceListener] into a [ComposableTheme]. */ -@Serializable -data class RawComposableTheme( - val states: Map = emptyMap(), - val variants: Map> = emptyMap(), -) - -/** Raw JSON shape of a single theme state; fields left `null` inherit from the state's `"default"` entry. */ -@Serializable -data class RawThemeState( - val texture: String? = null, - @SerialName("texture_size") val textureSize: Size? = null, - val u: Int? = null, - val v: Int? = null, - val width: Int? = null, - val height: Int? = null, - @SerialName("u_width") val uWidthSnake: Int? = null, - @SerialName("v_height") val vHeightSnake: Int? = null, - val uWidth: Int? = null, - val vHeight: Int? = null, -) - -@Serializable -private data class GuiTextureMetadata( - val gui: GuiMetadataSection? = null, -) - -@Serializable -private data class GuiMetadataSection( - val scaling: GuiScalingMetadata? = null, -) - -@Serializable -private data class GuiScalingMetadata( - val type: String? = null, -) - -/** - * The full theme definition for a single composable type (e.g. `button`, `slot`). - * - * Contains base [states] and optional named [variants] (e.g. `"dark"`). - * - * @property isNineslice Whether [states]' default texture is nine-slice scaled, per its - * `.mcmeta` sprite metadata. When `false`, composables using this theme get a minimum - * size matching the sprite's own pixel dimensions instead of stretching arbitrarily. - * @property states Base state map (always contains at least `"default"`). - * @property variants Named variant overrides (e.g. `"dark"` → its own state map). - */ -@Serializable -data class ComposableTheme( - val isNineslice: Boolean = false, - val states: Map, - val variants: Map = emptyMap(), -) { - companion object { - /** - * Retrieves the [ComposableTheme] for [loc] from the loaded registry. - * - * @throws IllegalStateException if no theme was loaded for [loc]. - */ - operator fun get(loc: ResourceLocation): ComposableTheme = - ThemeResourceListener.COMPOSABLES[loc] - ?: throw IllegalStateException("No theme found for composable: $loc") - } - - /** - * Returns the [ThemeState] for [stateName] in [variantName], falling back to the base - * state map and ultimately the `"default"` state. - */ - @Suppress("NOTHING_TO_INLINE") - inline fun getState(stateName: String, variantName: String): ThemeState = - variants[variantName]?.states?.get(stateName) - ?: states[stateName] - ?: states[TextureStates.DEFAULT]!! - - /** - * Returns `true` if [stateName] exists in [variantName] or the base state map. - */ - @Suppress("NOTHING_TO_INLINE") - inline fun hasState(stateName: String, variantName: String?): Boolean = - (variantName?.let { variants[it] }?.states?.get(stateName) ?: states[stateName]) != null -} - -/* ─────────────────────── Reload listener ─────────────────────── */ - -/** - * A client resource-reload listener that loads [ComposableTheme] definitions from - * `assets//archie_themes/` directories in resource packs. - * - * Theme files are JSON objects matching the structure of [ComposableTheme]. Register via - * [ReloadListenerRegistry.register] during `initClient()`. - * - * ### File format - * ```json - * { - * "states": { - * "default": { - * "texture": "archie:java/button", - * "texture_size": { "width": 64, "height": 64 }, - * "width": 64, - * "height": 20 - * }, - * "hovered": { "texture": "archie:java/button_highlighted" } - * } - * } - * ``` - */ -class ThemeResourceListener : - SerializationReloadListener( - format = SerializationManager.json, - serializer = RawComposableTheme.serializer(), - directory = "archie_themes", - fileExtension = ".json", - ), - PreparableReloadListener { - - companion object { - /** All registered [ComposableTheme]s keyed by their [ResourceLocation]. */ - internal val COMPOSABLES = mutableMapOf() - } - - /** Excludes `*.theme.json` [ThemeManifest] files, which this listener does not parse. */ - override fun shouldLoadResource(fileLocation: ResourceLocation): Boolean = - !fileLocation.path.endsWith(".theme.json") - - override fun apply( - objs: Map, - resourceManager: ResourceManager, - profiler: ProfilerFiller, - ) { - COMPOSABLES.clear() - for ((location, root) in objs) { - if (location.path.endsWith(".theme")) continue - try { - val statesObj = root.states - if (statesObj.isEmpty()) { - throw IllegalStateException("Theme must have a valid states object: $location") - } - val defaultObj = statesObj[TextureStates.DEFAULT] - ?: throw IllegalStateException("Theme must have a \"default\" state: $location") - - val defaultState = parseSimple(location, "default", defaultObj) - - val states = mutableMapOf() - for ((name, stateEl) in statesObj) { - states[name] = parseSimple(location, name, stateEl, defaultState) - } - - val variants = mutableMapOf() - root.variants.forEach { (variantName, variantEl) -> - val vs = mutableMapOf() - variantEl.forEach { (sName, sEl) -> - vs[sName] = parseSimple(location, sName, sEl, defaultState) - } - variants[variantName] = StatefulTheme(vs) - } - - val isNineslice = resourceManager.isNineSliceTexture(defaultState.texture) - COMPOSABLES[location] = ComposableTheme(isNineslice, states, variants) - Archie.LOGGER.info( - "Theme \"{}\" loaded ({} states, {} variants, nineslice={})", - location, states.size, variants.size, isNineslice, - ) - } catch (e: Exception) { - Archie.LOGGER.warn("Error processing theme at {}: {}", location, e.message, e) - } - } - } - - private data class BaseFields( - val texture: ResourceLocation, - val textureSize: Size, - val u: Int, - val v: Int, - ) - - private fun baseFields(loc: ResourceLocation, name: String, el: RawThemeState, default: ThemeState?): BaseFields { - val texture = el.texture?.let { ResourceLocation.parse(it) } - ?: default?.texture - ?: throw IllegalStateException("Missing texture for state \"$name\" in: $loc") - val textureSize = el.textureSize - ?: default?.textureSize - ?: throw IllegalStateException("Missing texture_size for state \"$name\" in: $loc") - return BaseFields( - texture = texture, - textureSize = textureSize, - u = el.u ?: default?.u ?: 0, - v = el.v ?: default?.v ?: 0, - ) - } - - - private fun parseSimple(loc: ResourceLocation, name: String, el: RawThemeState, default: SimpleThemeState? = null): SimpleThemeState { - val base = baseFields(loc, name, el, default) - val width = el.width ?: default?.width ?: throw IllegalStateException("Missing width for state \"$name\" in: $loc") - val height = el.height ?: default?.height ?: throw IllegalStateException("Missing height for state \"$name\" in: $loc") - return SimpleThemeState( - base.texture, - base.textureSize, - base.u, - base.v, - width, height, - el.uWidthSnake ?: el.uWidth ?: default?.uWidth ?: width, - el.vHeightSnake ?: el.vHeight ?: default?.vHeight ?: height, - ) - } - - private fun ResourceManager.isNineSliceTexture(texture: ResourceLocation): Boolean { - val candidates = if (texture.path.startsWith("textures/") && texture.path.endsWith(".png")) { - listOf(ResourceLocation.fromNamespaceAndPath(texture.namespace, "${texture.path}.mcmeta")) - } else { - listOf( - ResourceLocation.fromNamespaceAndPath(texture.namespace, "textures/gui/sprites/${texture.path}.png.mcmeta"), - ResourceLocation.fromNamespaceAndPath(texture.namespace, "textures/${texture.path}.png.mcmeta"), - ) - } - - for (candidate in candidates) { - val resource = getResource(candidate).orElse(null) ?: continue - try { - resource.openAsReader().use { reader -> - val metadata = SerializationManager.json.decodeFromString(reader.readText()) - val type = metadata.gui?.scaling?.type - if (type == "nine_slice") return true - } - } catch (_: Exception) { - // Ignore malformed metadata and continue trying other candidates. - } - } - return false - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/Theme.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/Theme.kt deleted file mode 100644 index ecc8dd081..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/Theme.kt +++ /dev/null @@ -1,193 +0,0 @@ -package net.kernelpanicsoft.archie.gui.theme - -import androidx.compose.runtime.Composable -import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.Immutable -import androidx.compose.runtime.compositionLocalOf -import kotlinx.serialization.SerialName -import kotlinx.serialization.Serializable -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.resourcepacks.SerializationReloadListener -import net.kernelpanicsoft.archie.serialization.SerializationManager -import net.kernelpanicsoft.archie.gui.util.KColor -import net.kernelpanicsoft.archie.util.div -import net.kernelpanicsoft.archie.util.rem -import net.minecraft.resources.ResourceLocation -import net.minecraft.server.packs.resources.PreparableReloadListener -import net.minecraft.server.packs.resources.ResourceManager -import net.minecraft.util.profiling.ProfilerFiller - -/** - * Constants for the built-in theme variant names. - * - * Pass these as the `mode` parameter of [ThemeData] to switch between light and dark variants. - */ -object ThemeVariants { - /** The default (light) theme variant. */ - const val DEFAULT = "" - /** The dark theme variant. */ - const val DARK = "dark" -} - -/** - * Resource-pack-defined metadata for a theme (namespace + type), declaring which variant - * names are valid and how requested modes should resolve to them. - * - * @property variants The set of variant names this theme actually defines resources for. - * @property defaultVariant The variant to fall back to when a requested mode isn't in [variants]. - * @property aliases Maps a requested mode name to a canonical variant name before - * validating it against [variants] (e.g. letting a pack expose `"night"` as an alias for `"dark"`). - */ -@Serializable -data class ThemeManifest( - val variants: Set = setOf(ThemeVariants.DEFAULT), - @SerialName("default_variant") val defaultVariant: String = ThemeVariants.DEFAULT, - val aliases: Map = emptyMap(), -) - -/** - * Loads `*.theme.json` [ThemeManifest] resources from the `archie_themes` directory on - * resource pack reload, keyed by their resource location. - * - * [resolveMode] is what [ThemeData.resolvedMode] calls to turn a requested variant name into - * one the active resource pack(s) actually support. - */ -class ThemeManifestResourceListener : - SerializationReloadListener( - format = SerializationManager.json, - serializer = ThemeManifest.serializer(), - directory = "archie_themes", - fileExtension = ".theme.json", - ), - PreparableReloadListener { - - companion object { - internal val MANIFESTS = mutableMapOf() - - /** - * Resolves [requestedMode] against the [ThemeManifest] loaded for [namespace]/[type], - * applying [ThemeManifest.aliases] then falling back to [ThemeManifest.defaultVariant] - * if the (possibly aliased) mode isn't in [ThemeManifest.variants]. - * - * Returns [requestedMode] unchanged when no manifest is registered for that - * namespace/type (i.e. the theme doesn't declare one). - */ - fun resolveMode(namespace: String, type: String, requestedMode: String): String { - val key = ResourceLocation.fromNamespaceAndPath(namespace, type.ifEmpty { "default" }) - val manifest = MANIFESTS[key] ?: return requestedMode - val canonical = manifest.aliases[requestedMode] ?: requestedMode - return if (canonical in manifest.variants) canonical else manifest.defaultVariant - } - } - - /** Replaces the registered [MANIFESTS] with the newly loaded [prepared] set. */ - override fun apply( - prepared: Map, - resourceManager: ResourceManager, - profiler: ProfilerFiller, - ) { - MANIFESTS.clear() - MANIFESTS.putAll(prepared) - } -} - -/** - * Immutable data holder describing the active theme context for composables. - * - * Provided through [LocalTheme] to all composables under a [Theme] wrapper. - * - * @property mode The active variant name (e.g. [ThemeVariants.DARK]). Empty string = default. - * @property type The platform type (e.g. `"java"`). Used as a path prefix for theme files. - * @property darkTextColor The text color used on light/bright surfaces. - * @property lightTextColor The text color used on dark surfaces. - * @property namespace The resource namespace to look up theme definitions in. - */ -@Immutable -data class ThemeData( - val mode: String, - val type: String, - val darkTextColor: KColor, - val lightTextColor: KColor, - val namespace: String = Archie.MOD_ID, -) { - val resolvedMode: String - get() = ThemeManifestResourceListener.resolveMode(namespace, type, mode) - - /** - * Resolves the [ComposableTheme] for the given composable name using the active namespace, - * type, and global mode. - * - * @param composable The composable theme name (e.g. `"button"`, `"slot"`). - * @return The [ComposableTheme] definition loaded from resources. - */ - fun getComposableTheme(composable: String): ComposableTheme { - val mode = resolvedMode - if (mode.isNotEmpty()) { - val globalVariantLocation = composableThemeLocation(namespace, type, mode, composable) - ThemeResourceListener.COMPOSABLES[globalVariantLocation]?.let { return it } - } - return ComposableTheme[composableThemeLocation(namespace, type, composable)] - } -} - -/** Provides the current [ThemeData] to composables in the tree. */ -val LocalTheme = compositionLocalOf { ThemeData(ThemeVariants.DEFAULT, "java", KColor.DARK_GRAY, KColor.WHITE, Archie.MOD_ID) } - -/** - * Builds the [ResourceLocation] used to look up a composable's default-variant theme - * definition. - * - * The resulting path is `:` with an optional `/` prefix when - * [type] is non-empty (e.g. `archie:java/button`). - */ -@Suppress("NOTHING_TO_INLINE") -inline fun composableThemeLocation( - namespace: String, - type: String, - composable: String, -): ResourceLocation = if (type.isNotEmpty()) namespace % type / composable else namespace % composable - -/** - * Builds the [ResourceLocation] used to look up a composable's theme definition for a - * specific non-default [mode] (variant). - * - * The resulting path is `:` with an optional `/` and `/` - * prefix, in that order, for each that is non-empty (e.g. `archie:java/dark/button`). - */ -@Suppress("NOTHING_TO_INLINE") -inline fun composableThemeLocation( - namespace: String, - type: String, - mode: String, - composable: String, -): ResourceLocation { - var ret = namespace % composable - if (mode.isNotEmpty()) ret = mode / ret - if (type.isNotEmpty()) ret = type / ret - return ret -} - -/** - * Sets the active [ThemeData] for all composables in [content]. - * - * @param mode The variant name to activate (`""` for default, `"dark"` for dark mode). - * @param type The platform type (`"java"` by default). - * @param namespace The resource namespace for theme files. - * @param content The composable tree that will receive the theme. - */ -@Composable -fun Theme( - mode: String = ThemeVariants.DEFAULT, - type: String = "java", - darkTextColor: KColor = KColor.DARK_GRAY, - lightTextColor: KColor = KColor.WHITE, - namespace: String = Archie.MOD_ID, - content: @Composable () -> Unit, -) = CompositionLocalProvider(LocalTheme provides ThemeData(mode, type, darkTextColor, lightTextColor, namespace)) { content() } - -/** - * Sets the active theme using a pre-built [ThemeData]. - */ -@Composable -fun Theme(data: ThemeData, content: @Composable () -> Unit) = - CompositionLocalProvider(LocalTheme provides data) { content() } diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/HsvColor.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/HsvColor.kt deleted file mode 100644 index 372c6ef8d..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/HsvColor.kt +++ /dev/null @@ -1,55 +0,0 @@ -package net.kernelpanicsoft.archie.gui.util - -import androidx.compose.runtime.Immutable -import kotlinx.serialization.Serializable - -/** - * An immutable colour representation using the Hue-Saturation-Value model with an alpha channel. - * - * This representation is primarily intended for use with [net.kernelpanicsoft.archie.gui.composables.input.ColorPicker], - * which operates natively in HSV space to avoid lossy round-trip conversions. - * - * All component values are in the range **[0.0, 1.0]**. - * - * ### Example - * ```kotlin - * val red = HsvColor(hue = 0f, saturation = 1f, value = 1f, alpha = 1f) - * val fromKColor = HsvColor.from(KColor.CYAN) - * val backToKColor = red.toKColor() - * ``` - * - * @property hue The hue angle normalized to [0, 1] (0 and 1 both represent red). - * @property saturation The saturation (0 = grey, 1 = fully saturated). - * @property value The brightness value (0 = black, 1 = maximum brightness). - * @property alpha The alpha transparency (0 = fully transparent, 1 = fully opaque). - */ -@Immutable -data class HsvColor( - val hue: Float, - val saturation: Float, - val value: Float, - val alpha: Float, -) { - /** - * Converts this HSV colour to an equivalent [KColor] (ARGB). - */ - fun toKColor(): KColor = KColor.ofHsv(hue, saturation, value, alpha) - - companion object { - /** - * Creates an [HsvColor] from an existing [KColor] by converting its RGB components - * to HSV using the JVM's [java.awt.Color.RGBtoHSB] utility. - * - * @param color The source [KColor] to convert. - */ - fun from(color: KColor): HsvColor { - val hsb = java.awt.Color.RGBtoHSB(color.red, color.green, color.blue, null) - return HsvColor( - hue = hsb[0], - saturation = hsb[1], - value = hsb[2], - alpha = color.alpha / 255f, - ) - } - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/KColor.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/KColor.kt deleted file mode 100644 index 17a2c9dd4..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/KColor.kt +++ /dev/null @@ -1,135 +0,0 @@ -package net.kernelpanicsoft.archie.gui.util - -import kotlinx.serialization.Serializable -import net.minecraft.ChatFormatting -import net.minecraft.network.chat.TextColor -import net.minecraft.util.Mth -import net.minecraft.world.item.DyeColor -import kotlin.random.Random - -/** - * A serializable, immutable ARGB colour value for use in GUI composables. - * - * [KColor] is a pure-Kotlin alternative to [java.awt.Color] that works with - * Minecraft's integer-packed colour conventions. All component values are in - * the range 0–255. - * - * ### Creating colours - * ```kotlin - * val red = KColor.RED - * val custom = KColor.ofRgb(0xFF8C00) // opaque dark orange - * val semi = KColor.ofArgb(0x80FF0000L) // 50 % transparent red - * val hsv = KColor.ofHsv(0.33f, 1f, 0.8f) // dark green via HSV - * ``` - * - * @property red The red channel (0–255). - * @property green The green channel (0–255). - * @property blue The blue channel (0–255). - * @property alpha The alpha channel (0–255, where 0 is fully transparent and 255 is opaque). - */ -@Serializable -data class KColor( - val red: Int = 0, - val green: Int = 0, - val blue: Int = 0, - val alpha: Int = 255, -) { - companion object { - // ── Predefined colours ────────────────────────────────── - val WHITE = ofRgb(0xFFFFFF) - val LIGHT_GRAY = ofRgb(0xC0C0C0) - val GRAY = ofRgb(0x808080) - val DARK_GRAY = ofRgb(0x404040) - val BLACK = ofRgb(0x000000) - val RED = ofRgb(0xFF0000) - val PINK = ofRgb(0xFFAFAF) - val ORANGE = ofRgb(0xFFA500) - val YELLOW = ofRgb(0xFFFF00) - val GREEN = ofRgb(0x4CAF50) - val MAGENTA = ofRgb(0xFF00FF) - val CYAN = ofRgb(0x00FFFF) - val LIGHT_BLUE = ofRgb(0x2196F3) - val BLUE = ofRgb(0x0000FF) - - /** - * Creates a [KColor] from a packed ARGB [Long] in the form `0xAARRGGBB`. - * - * @param argb The packed ARGB value. - */ - fun ofArgb(argb: Long): KColor = KColor( - red = (argb shr 16 and 255).toInt(), - green = (argb shr 8 and 255).toInt(), - blue = (argb and 255).toInt(), - alpha = (argb shr 24).toInt(), - ) - - /** - * Creates an opaque [KColor] from a packed RGB [Int] in the form `0xRRGGBB`. - * - * @param rgb The packed RGB value (alpha is set to 255). - */ - fun ofRgb(rgb: Int): KColor = KColor( - red = rgb shr 16 and 255, - green = rgb shr 8 and 255, - blue = rgb and 255, - ) - - /** - * Creates a [KColor] from HSV (Hue, Saturation, Value) components with full opacity. - * - * @param hue Hue in the range [0, 1]. - * @param saturation Saturation in the range [0, 1]. - * @param value Value (brightness) in the range [0, 1]. - */ - fun ofHsv(hue: Float, saturation: Float, value: Float): KColor = - ofRgb(Mth.hsvToRgb(hue - 0.5e-7f, saturation, value)) - - /** - * Creates a [KColor] from HSV components with a custom alpha. - * - * @param hue Hue in the range [0, 1]. - * @param saturation Saturation in the range [0, 1]. - * @param value Value (brightness) in the range [0, 1]. - * @param alpha Alpha in the range [0, 1] (0 = fully transparent, 1 = opaque). - */ - fun ofHsv(hue: Float, saturation: Float, value: Float, alpha: Float): KColor = - ofArgb(((alpha * 255).toInt().toLong() shl 24 or Mth.hsvToRgb(hue - 0.5e-7f, saturation, value).toLong())) - - /** - * Creates a [KColor] from the colour associated with a [ChatFormatting] constant. - * - * @param formatting A [ChatFormatting] value with an associated colour. - */ - fun ofFormatting(formatting: ChatFormatting): KColor = ofRgb(formatting.color ?: 0) - - /** - * Creates a [KColor] from a Minecraft [DyeColor]. - * - * @param dye The dye colour. - */ - fun ofDye(dye: DyeColor): KColor = ofArgb(dye.textureDiffuseColor.toLong()) - - /** - * Generates a random [KColor]. - * - * @param alpha Whether the alpha channel should also be randomized. When `false` the - * colour is fully opaque. - */ - fun random(alpha: Boolean = true): KColor = - if (alpha) ofArgb(Random.nextLong(0x100000000) or 0xFF000000L) - else ofRgb(Random.nextInt(0x1000000)) - } - - /** - * The packed RGB integer representation (no alpha channel, in the form `0xRRGGBB`). - */ - val rgb: Int get() = (red shl 16) or (green shl 8) or blue - - /** - * The packed ARGB integer representation (in the form `0xAARRGGBB`). - */ - val argb: Int get() = (alpha shl 24) or (red shl 16) or (green shl 8) or blue - - /** Converts this to a vanilla [TextColor] (RGB only - [TextColor] carries no alpha channel). */ - fun toTextColor(): TextColor = TextColor.fromRgb(rgb) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/GuiGraphics.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/GuiGraphics.kt deleted file mode 100644 index 7ed42a12c..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/GuiGraphics.kt +++ /dev/null @@ -1,164 +0,0 @@ -package net.kernelpanicsoft.archie.gui.util.extension - -import com.mojang.blaze3d.vertex.PoseStack -import net.kernelpanicsoft.archie.gui.layout.IntRect -import net.kernelpanicsoft.archie.gui.theme.SimpleThemeState -import net.kernelpanicsoft.archie.gui.theme.ThemeState -import net.minecraft.client.gui.GuiGraphics -import net.minecraft.client.renderer.RenderType -import net.minecraft.resources.ResourceLocation - -private fun ResourceLocation.isAtlasSprite(): Boolean = - !path.startsWith("textures/") && !path.endsWith(".png") - -/** Draws a ThemeState to the screen. */ -fun GuiGraphics.drawThemeState(state: ThemeState, x: Int, y: Int, width: Int, height: Int) { - state as SimpleThemeState - if (state.texture.isAtlasSprite()) { - blitSprite(state.texture, x, y, width, height) - } else { - blit(state, x, y) - } -} - -/** A helper extension to blit a SimpleThemeState without manually extracting all its properties. */ -fun GuiGraphics.blit(state: SimpleThemeState, x: Int, y: Int) { - this.blit( - state.texture, - x, - y, - state.width, - state.height, - state.u.toFloat(), - state.v.toFloat(), - state.uWidth, - state.vHeight, - state.textureSize.width, - state.textureSize.height, - ) -} - -/** Fills a rectangle with a 4-corner color gradient using the default GUI [RenderType]. */ -fun GuiGraphics.fillGradient( - x: Int, - y: Int, - width: Int, - height: Int, - topLeftColor: Int, - topRightColor: Int, - bottomLeftColor: Int, - bottomRightColor: Int, -) = fillGradient( - RenderType.gui(), - x, - y, - width, - height, - topLeftColor, - topRightColor, - bottomLeftColor, - bottomRightColor, -) - -/** - * Fills a rectangle with a 4-corner color gradient, unlike vanilla's [GuiGraphics.fillGradient] - * (top-to-bottom only), by directly emitting one quad with a per-vertex ARGB color to [type]. - */ -fun GuiGraphics.fillGradient( - type: RenderType, - x: Int, - y: Int, - width: Int, - height: Int, - topLeftColor: Int, - topRightColor: Int, - bottomLeftColor: Int, - bottomRightColor: Int, -) { - val buffer = bufferSource().getBuffer(type) - val matrix = pose().last().pose() - - buffer.addVertex(matrix, x + width, y, 0).setColor(topRightColor) - buffer.addVertex(matrix, x, y, 0).setColor(topLeftColor) - buffer.addVertex(matrix, x, y + height, 0).setColor(bottomLeftColor) - buffer.addVertex(matrix, x + width, y + height, 0).setColor(bottomRightColor) -} - -/** Draws an unfilled rectangle outline of [thickness] pixels using the default GUI [RenderType]. */ -fun GuiGraphics.drawRectOutline( - x: Int, - y: Int, - width: Int, - height: Int, - color: Int, - thickness: Int = 1, -) = drawRectOutline(RenderType.gui(), x, y, width, height, color, thickness) - -/** Draws an unfilled rectangle outline of [thickness] pixels as four filled edge strips. */ -fun GuiGraphics.drawRectOutline( - type: RenderType, - x: Int, - y: Int, - width: Int, - height: Int, - color: Int, - thickness: Int = 1, -) { - fill(type, x, y, x + width, y + thickness, color) - fill(type, x, y + height - thickness, x + width, y + height, color) - - fill(type, x, y + thickness, x + thickness, y + height - thickness, color) - fill(type, x + width - thickness, y + thickness, x + width, y + height - thickness, color) -} - -/** - * Runs [block] with this [GuiGraphics]'s [PoseStack][com.mojang.blaze3d.vertex.PoseStack] pushed, - * popping it again afterwards (including when [block] throws). Saves the manual - * `pose().pushPose()` / `pose().popPose()` pairing renderers otherwise need around - * translate/scale/rotate calls. - */ -fun GuiGraphics.pose(block: PoseStack.() -> T): T -{ - val pose = pose() - pose.pushPose() - try - { - return pose.block() - } - finally - { - pose.popPose() - } -} - -/** - * Runs [block] with scissoring enabled to the `[minX, minY, maxX, maxY)` rectangle, disabling - * it again afterwards. Saves the manual `enableScissor(...)` / `disableScissor()` pairing - * renderers otherwise need around clipped content. - */ -fun GuiGraphics.scissor(minX: Int, minY: Int, maxX: Int, maxY: Int, block: () -> T): T -{ - enableScissor(minX, minY, maxX, maxY) - try - { - return block() - } - finally - { - disableScissor() - } -} - -/** Overload of [scissor] taking the clip bounds as an [IntRect]. */ -fun GuiGraphics.scissor(rect: IntRect, block: () -> T): T -{ - val (minX: Int, minY: Int, maxX: Int, maxY: Int) = rect - return scissor(minX, minY, maxX, maxY, block) -} - -/** - * Lets a [GuiGraphics] receiver be invoked like `guiGraphics { ... }`, running [block] with - * `this` as the receiver. Used throughout the built-in composables' `Renderer` implementations - * to avoid repeating the `guiGraphics.` prefix on every draw call. - */ -operator fun GuiGraphics.invoke(block: GuiGraphics.() -> Unit): Unit = block() diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/Screen.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/Screen.kt deleted file mode 100644 index db1b6a0e3..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/Screen.kt +++ /dev/null @@ -1,177 +0,0 @@ -package net.kernelpanicsoft.archie.gui.util.extension - -import net.kernelpanicsoft.archie.gui.layout.LayoutNode -import net.kernelpanicsoft.archie.gui.modifiers.input.* -import net.kernelpanicsoft.archie.gui.nodes.UINode -import net.minecraft.client.gui.screens.Screen - -// ───────────────────────────────────────────────────────────────────────────── -// Generic traversal -// ───────────────────────────────────────────────────────────────────────────── - -/** - * Recursively dispatches [event] through the [LayoutNode] tree starting at [node]. - * - * Children are processed in reverse z-index order (highest z first) so that the - * topmost visible node receives the event first. Propagation stops as soon as - * [InputEvent.isConsumed] becomes `true`. - * - * @param node The root of the subtree to traverse. - * @param event The [InputEvent] being dispatched. - * @param condition Optional per-node predicate; the [process] callback is only invoked - * when this returns `true` for a given node. - * @param process The callback invoked on each eligible node. - */ -internal fun Screen.processInputEvent( - node: LayoutNode, - event: T, - condition: (LayoutNode) -> Boolean = { true }, - process: (LayoutNode, T) -> Unit, -) { - for (child in node.childrenDescendingZ()) { - if (event.isConsumed) break - processInputEvent(child, event, condition, process) - } - if (!event.isConsumed && condition(node)) { - process(node, event) - } -} - -// ───────────────────────────────────────────────────────────────────────────── -// Pointer (mouse) events -// ───────────────────────────────────────────────────────────────────────────── - -/** - * Dispatches a [PointerEvent] of [eventType] through the [node] tree. - * - * Only nodes that pass [condition] (default: bounded by the mouse position) receive - * the event. Pass `global = true` to dispatch to all nodes regardless of bounds. - * - * @return The dispatched [PointerEvent] (check [PointerEvent.bypassSuper] to decide - * whether to call the vanilla screen's `super` method). - */ -@Suppress("NOTHING_TO_INLINE") -internal inline fun Screen.processPointerEvent( - node: LayoutNode, - mouseX: Double, - mouseY: Double, - eventType: PointerEventType, - global: Boolean = false, - noinline condition: (LayoutNode) -> Boolean = { it.isBounded(mouseX.toInt(), mouseY.toInt()) }, -): PointerEvent { - val event = BasicPointerEvent(eventType, mouseX, mouseY) - processInputEvent(node, event, if (global) { _ -> true } else condition) { currentNode, currentEvent -> - currentNode.modifier.foldIn(Unit) { _, el -> - if (el is OnPointerEventModifier<*> && el.eventType == eventType && (global || !currentEvent.isConsumed)) - @Suppress("UNCHECKED_CAST") - (el.onEvent as (UINode, PointerEvent) -> Unit)(currentNode, event) - } - } - return event -} - -/** - * Dispatches a [ScrollEvent] through the [node] tree. - * - * @return The dispatched [ScrollEvent]. - */ -@Suppress("NOTHING_TO_INLINE") -internal inline fun Screen.processScrollEvent( - node: LayoutNode, - mouseX: Double, - mouseY: Double, - scrollX: Double, - scrollY: Double, - eventType: PointerEventType, - global: Boolean = false, -): ScrollEvent { - val event = ScrollEvent(eventType, mouseX, mouseY, scrollX, scrollY) - processInputEvent( - node, event, - if (global) { _ -> true } else { n -> n.isBounded(mouseX.toInt(), mouseY.toInt()) }, - ) { currentNode, currentEvent -> - currentNode.modifier.foldIn(Unit) { _, el -> - if (el is OnPointerEventModifier<*> && el.eventType == eventType && (global || !currentEvent.isConsumed)) - @Suppress("UNCHECKED_CAST") - (el.onEvent as (UINode, PointerEvent) -> Unit)(currentNode, event) - } - } - return event -} - -/** - * Dispatches a [DragEvent] through the [node] tree. - * - * @return The dispatched [DragEvent]. - */ -@Suppress("NOTHING_TO_INLINE") -internal inline fun Screen.processDragEvent( - node: LayoutNode, - mouseX: Double, - mouseY: Double, - button: Int, - dragX: Double, - dragY: Double, - eventType: PointerEventType, - global: Boolean = false, -): DragEvent { - val event = DragEvent(eventType, mouseX, mouseY, button, dragX, dragY) - processInputEvent( - node, event, - if (global) { _ -> true } else { n -> n.isBounded(mouseX.toInt(), mouseY.toInt()) }, - ) { currentNode, currentEvent -> - currentNode.modifier.foldIn(Unit) { _, el -> - if (el is OnPointerEventModifier<*> && el.eventType == eventType && (global || !currentEvent.isConsumed)) - @Suppress("UNCHECKED_CAST") - (el.onEvent as (UINode, PointerEvent) -> Unit)(currentNode, event) - } - } - return event -} - -// ───────────────────────────────────────────────────────────────────────────── -// Keyboard events -// ───────────────────────────────────────────────────────────────────────────── - -/** - * Dispatches a [KeyEvent] through the [node] tree, chaining all [OnKeyEventModifier]s. - * - * @return The dispatched [KeyEvent]. - */ -@Suppress("NOTHING_TO_INLINE") -internal inline fun Screen.processKeyEvent( - node: LayoutNode, - keyCode: Int, - scanCode: Int, - modifiers: Int, -): KeyEvent { - val event = KeyEvent(keyCode, scanCode, modifiers) - processInputEvent(node, event) { currentNode, currentEvent -> - currentNode.modifier.foldIn(Unit) { _, el -> - if (el is OnKeyEventModifier && !currentEvent.isConsumed) - el.onEvent(currentNode, currentEvent) - } - } - return event -} - -/** - * Dispatches a [CharEvent] through the [node] tree, chaining all [OnCharTypedModifier]s. - * - * @return The dispatched [CharEvent]. - */ -@Suppress("NOTHING_TO_INLINE") -internal inline fun Screen.processCharEvent( - node: LayoutNode, - codePoint: Char, - modifiers: Int, -): CharEvent { - val event = CharEvent(codePoint, modifiers) - processInputEvent(node, event) { currentNode, currentEvent -> - currentNode.modifier.foldIn(Unit) { _, el -> - if (el is OnCharTypedModifier && !currentEvent.isConsumed) - el.onEvent(currentNode, currentEvent) - } - } - return event -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/VertexConsumer.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/VertexConsumer.kt deleted file mode 100644 index 74c7307e6..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/VertexConsumer.kt +++ /dev/null @@ -1,20 +0,0 @@ -package net.kernelpanicsoft.archie.gui.util.extension - -import com.mojang.blaze3d.vertex.VertexConsumer -import org.joml.Matrix4f - -/* ─────────────────────────── VertexConsumer ─────────────────────────── */ - -/** - * Adds a vertex to this [VertexConsumer] using integer screen coordinates. - * - * This is a convenience overload that avoids repeated [Int.toFloat] casts when working - * with pixel-aligned UI geometry. - * - * @param matrix The current pose matrix. - * @param x The x position in screen pixels. - * @param y The y position in screen pixels. - * @param z The z (depth) position. - */ -fun VertexConsumer.addVertex(matrix: Matrix4f, x: Int, y: Int, z: Int): VertexConsumer = - addVertex(matrix, x.toFloat(), y.toFloat(), z.toFloat()) \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt deleted file mode 100644 index 04b7e3160..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt +++ /dev/null @@ -1,27 +0,0 @@ -package net.kernelpanicsoft.archie.networking - -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.gui.ComposeContainerMenuBase -import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStatePacketRegistry -import net.kernelpanicsoft.archie.gui.item.ItemStatePacketRegistry -import net.kernelpanicsoft.archie.util.rem - -/** - * Archie's own [NetworkChannel], used for its internal packets (Compose container menu slot - * syncing, block entity/item state syncing). Not intended for use by downstream mods; create your - * own [NetworkChannel] instance instead. - */ -object ArchieNetworkChannel : NetworkChannel(Archie % "main") -{ - /** - * Registers Archie's built-in packet handlers, then [register]s the channel. Called once - * from [Archie.init]. - */ - fun init() - { - ComposeContainerMenuBase.register() - BlockEntityStatePacketRegistry.register() - ItemStatePacketRegistry.register() - register() - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/IPacketContext.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/IPacketContext.kt deleted file mode 100644 index 12ded35bd..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/IPacketContext.kt +++ /dev/null @@ -1,35 +0,0 @@ -package net.kernelpanicsoft.archie.networking - -import net.minecraft.client.Minecraft -import net.minecraft.core.RegistryAccess -import net.minecraft.world.entity.player.Player - -/** - * Provides contextual information available when handling a received network packet. - * - * Implementations are created by [NetworkChannel] when dispatching received payloads to - * their registered handlers. Both server-bound and client-bound handlers receive an instance - * of this interface, giving them access to the receiving player, the registry, and (on the - * client side) the [Minecraft] instance. - */ -interface IPacketContext { - /** - * The player associated with the packet. On the server this is the sending [net.minecraft.server.level.ServerPlayer]; - * on the client this is the local player. - */ - val player: Player - - /** - * The [RegistryAccess] for the current connection, providing access to dynamic registries. - */ - val registryAccess: RegistryAccess - - /** - * The client-side [Minecraft] instance. - * - * Only safe to access from client-bound packet handlers. Calling this from a server-bound - * handler will throw an [IllegalStateException] because the dedicated server has no - * [Minecraft] instance. - */ - val minecraft: Minecraft get() = Minecraft.getInstance() -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt deleted file mode 100644 index 4424c678c..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt +++ /dev/null @@ -1,486 +0,0 @@ -@file:OptIn(InternalSerializationApi::class) - -package net.kernelpanicsoft.archie.networking - -import dev.architectury.networking.NetworkManager -import dev.architectury.utils.Env -import dev.architectury.utils.EnvExecutor -import dev.architectury.utils.GameInstance -import kotlinx.coroutines.Runnable -import kotlinx.serialization.* -import net.kernelpanicsoft.archie.config.ConfigSpec -import net.kernelpanicsoft.archie.gui.util.KColor -import net.kernelpanicsoft.archie.serialization.SerializationManager -import net.kernelpanicsoft.archie.serialization.serializers.SResourceLocation -import net.kernelpanicsoft.archie.serialization.streamCodec -import net.kernelpanicsoft.archie.util.foldEnv -import net.kernelpanicsoft.archie.util.sendSystemMessage -import net.minecraft.core.RegistryAccess -import net.minecraft.network.chat.Component -import net.minecraft.network.chat.TextColor -import net.minecraft.network.protocol.Packet -import net.minecraft.network.protocol.common.ClientboundCustomPayloadPacket -import net.minecraft.network.protocol.common.custom.CustomPacketPayload -import net.minecraft.network.protocol.game.ClientboundBundlePacket -import net.minecraft.resources.ResourceLocation -import net.minecraft.server.level.ServerChunkCache -import net.minecraft.server.level.ServerLevel -import net.minecraft.server.level.ServerPlayer -import net.minecraft.world.entity.Entity -import net.minecraft.world.entity.player.Player -import net.minecraft.world.level.ChunkPos -import kotlin.reflect.KClass - -/** - * A function type that handles a received packet of type [T] along with its [IPacketContext]. - * - * @param T The packet data class type. - */ -typealias PacketHandler = (T, IPacketContext) -> Unit - -/** - * Internal payload wrapper that carries an index into the registered packet list plus the - * CBOR-encoded packet bytes. Using a single payload type per channel keeps the number of - * registered Architectury payload types small. - */ -@Serializable -internal data class Payload( - val id: SResourceLocation, - val index: Int, - val data: ByteArray, -) : CustomPacketPayload { - override fun type(): CustomPacketPayload.Type = - CustomPacketPayload.Type(id) - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - other as Payload - if (index != other.index) return false - if (!data.contentEquals(other.data)) return false - return true - } - - override fun hashCode(): Int { - var result = index - result = 31 * result + data.contentHashCode() - return result - } -} - -internal val PayloadCodec = Payload.serializer().streamCodec - -/** - * Manages the registration and sending of strongly-typed, serialization-backed network packets. - * - * A single [NetworkChannel] can handle any number of server-bound and client-bound packet - * types. All packets are serialized with CBOR via kotlinx.serialization. - * - * Packet classes **must** be Kotlin data classes annotated with `@Serializable`. - * - * ### Example - * ```kotlin - * val CHANNEL = NetworkChannel(Archie["main"]) - * - * @Serializable - * data class SyncDataPacket(val value: Int) - * - * // During mod init: - * CHANNEL.clientbound(SyncDataPacket::class) { packet, ctx -> - * // handle on client - * } - * CHANNEL.register() - * - * // Sending: - * CHANNEL.toPlayer(player, SyncDataPacket(42)) - * ``` - * - * @param id The unique [ResourceLocation] identifier for this channel. - */ -@Suppress("unused") -@OptIn(ExperimentalSerializationApi::class) -open class NetworkChannel(private val id: ResourceLocation) { - private val clientPacketId = CustomPacketPayload.Type(id.withSuffix("_client")) - private val serverPacketId = CustomPacketPayload.Type(id.withSuffix("_server")) - - private val serverClasses = mutableListOf>() - private val clientClasses = mutableListOf>() - - private val serverConfigs = mutableListOf() - private val clientConfigs = mutableListOf() - - private val serverboundHandlers = mutableListOf>() - private val clientboundHandlers = mutableListOf>() - - /** - * Registers a server-bound packet type and its handler. - * - * The handler is invoked on the server when a client sends a packet of class [klass]. - * - * @param T The packet data class type. - * @param klass The [KClass] of the packet. Must be a data class with `@Serializable`. - * @param handler The handler invoked on the receiving side. - * @throws IllegalArgumentException if [klass] is not a data class, lacks a serializer, or is already registered. - */ - fun serverbound(klass: KClass, handler: PacketHandler) { - require(klass.isData) { "Only data classes can be used as packets" } - require(klass.serializerOrNull() != null) { "Data class doesn't have a serializer. Did you forget to add @Serializable?" } - require(serverClasses.find { it == klass } == null) { "Packet is already registered" } - serverboundHandlers.add(handler) - serverClasses.add(klass) - } - - /** - * Registers a server-bound packet type and its handler, inferring the packet class from the - * reified type parameter [T] instead of requiring `T::class` to be passed explicitly. - * - * Equivalent to `serverbound(T::class, handler)`. - * - * @param T The packet data class type. - * @param handler The handler invoked on the receiving side. - * @throws IllegalArgumentException if [T] is not a data class, lacks a serializer, or is already registered. - */ - inline fun serverbound(noinline handler: PacketHandler) = serverbound(T::class, handler) - - /** - * Registers a client-bound packet type and its handler. - * - * The handler is invoked on the client when the server sends a packet of class [klass]. - * - * @param T The packet data class type. - * @param klass The [KClass] of the packet. Must be a data class with `@Serializable`. - * @param handler The handler invoked on the receiving side. - * @throws IllegalArgumentException if [klass] is not a data class, lacks a serializer, or is already registered. - */ - fun clientbound(klass: KClass, handler: PacketHandler) { - require(klass.isData) { "Only data classes can be used as packets." } - require(klass.serializerOrNull() != null) { "Data class doesn't have a serializer. Did you forget to add @Serializable?" } - require(clientClasses.find { it == klass } == null) { "Packet is already registered" } - clientboundHandlers.add(handler) - clientClasses.add(klass) - } - - /** - * Registers a client-bound packet type and its handler, inferring the packet class from the - * reified type parameter [T] instead of requiring `T::class` to be passed explicitly. - * - * Equivalent to `clientbound(T::class, handler)`. - * - * @param T The packet data class type. - * @param handler The handler invoked on the receiving side. - * @throws IllegalArgumentException if [T] is not a data class, lacks a serializer, or is already registered. - */ - inline fun clientbound(noinline handler: PacketHandler) = clientbound(T::class, handler) - - /** - * Registers [spec] as a server-bound "save my changes" packet: when the server receives one, - * it is decoded with [spec]'s own [kotlinx.serialization.KSerializer] (via - * [net.kernelpanicsoft.archie.config.ConfigSpec.serializer]) rather than the reflective one - * used for ordinary packet classes, since a `ConfigSpec` singleton isn't itself - * `@Serializable`. The permission check, in-memory decode (mutating [spec]'s fields directly), - * and broadcast-or-reject all happen in `decodeDispatchData`, before the handler registered - * here ever runs - that handler is the one place that actually persists the result, calling - * [net.kernelpanicsoft.archie.config.ConfigSpec.save] to write it to disk. No-op if [spec] is - * already registered. - * - * Internal: used by [net.kernelpanicsoft.archie.config.ConfigSpec.init] to wire up - * server/client config sync. Not part of the public packet API. - */ - internal fun configServerbound(klass: KClass, spec: T) - { - if (spec in serverConfigs) return - serverConfigs.add(spec) - serverboundHandlers.add { config: T, context -> config.save() } - serverClasses.add(klass) - } - - internal inline fun configServerbound(spec: T) = configServerbound(spec::class, spec) - - /** - * Registers [spec] as a client-bound config-sync packet: when the client receives one, it is - * decoded with [spec]'s own [kotlinx.serialization.KSerializer] and saved locally via - * [net.kernelpanicsoft.archie.config.ConfigSpec.save]. Used to push a - * [net.kernelpanicsoft.archie.config.ConfigSpec.Server] config's values to a joining player. - * No-op if [spec] is already registered. - * - * Internal: used by [net.kernelpanicsoft.archie.config.ConfigSpec.init] to wire up - * server/client config sync. Not part of the public packet API. - */ - internal fun configClientbound(klass: KClass, spec: T) - { - if (spec in clientConfigs) return - clientConfigs.add(spec) - clientboundHandlers.add { config: T, context -> - config.save() - } - clientClasses.add(klass) - } - - internal inline fun configClientbound(spec: T) = configClientbound(spec::class, spec) - - /** - * Sends one or more packets from the client to the server. - * - * @param packets The packets to send. All must have been registered via [serverbound]. - * @throws IllegalArgumentException if no packets are provided. - * @throws IllegalStateException if a packet type was not registered. - */ - fun toServer(vararg packets: T) { - require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } - packets.map { - createPayload( - packet = it, - classes = serverClasses, - configs = serverConfigs, - payloadId = id.withSuffix("_client"), - missingMessage = "Trying to send a packet to server but it hasn't registered the packet and its handler", - ) - }.forEach { NetworkManager.sendToServer(it) } - } - - /** - * Sends one or more packets from the server to a specific [player]. - * - * @param player The target [ServerPlayer]. - * @param packets The packets to send. All must have been registered via [clientbound]. - * @throws IllegalArgumentException if no packets are provided. - */ - fun toPlayer(player: ServerPlayer, vararg packets: T) { - require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } - createPayloads(packets).forEach { NetworkManager.sendToPlayer(player, it) } - } - - /** - * Sends one or more packets from the server to a list of [players]. - * - * @param players The list of target [ServerPlayer]s. - * @param packets The packets to send. - * @throws IllegalArgumentException if no packets are provided. - */ - fun toPlayers(players: List, vararg packets: T) { - require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } - createPayloads(packets).forEach { NetworkManager.sendToPlayers(players, it) } - } - - /** - * Sends one or more packets from the server to **all** connected players. - * - * @param packets The packets to send. - * @throws IllegalArgumentException if no packets are provided. - * @throws IllegalStateException if called from the client side. - */ - fun toAllPlayers(vararg packets: T) { - require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } - val server = GameInstance.getServer() - ?: throw IllegalStateException("Cannot send clientbound payloads on the client") - createPayloads(packets).forEach { NetworkManager.sendToPlayers(server.playerList.players, it) } - } - - /** - * Sends one or more packets to all players currently in the given [level] (dimension). - * - * @param level The [ServerLevel] whose players should receive the packets. - * @param packets The packets to send. - * @throws IllegalArgumentException if no packets are provided. - */ - fun toPlayersInDimension(level: ServerLevel, vararg packets: T) { - require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } - createPayloads(packets).forEach { NetworkManager.sendToPlayers(level.players(), it) } - } - - /** - * Sends one or more packets to all players within [radius] blocks of the given coordinates - * in [level], optionally excluding [exclude]. - * - * @param level The [ServerLevel] to broadcast within. - * @param exclude A [ServerPlayer] to exclude, or `null` to include all nearby players. - * @param x The X coordinate of the broadcast origin. - * @param y The Y coordinate of the broadcast origin. - * @param z The Z coordinate of the broadcast origin. - * @param radius The broadcast radius in blocks. - * @param packets The packets to send. - * @throws IllegalArgumentException if no packets are provided. - */ - fun toNearPlayers( - level: ServerLevel, - exclude: ServerPlayer? = null, - x: Double, - y: Double, - z: Double, - radius: Double, - vararg packets: T, - ) { - require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } - val payloads = createPayloads(packets) - level.server.playerList.broadcast( - exclude, x, y, z, radius, level.dimension(), - makeClientboundPacket(*payloads.toTypedArray()), - ) - } - - /** - * Sends one or more packets to all players tracking [entity] (i.e., the entity is loaded - * on their client). - * - * @param entity The entity being tracked. - * @param self Whether to also send the packet to the entity itself if it is a [ServerPlayer]. - * @param packets The packets to send. - * @throws IllegalArgumentException if no packets are provided. - * @throws IllegalStateException if called from the client side. - */ - fun toPlayersTrackingEntity(entity: Entity, self: Boolean = false, vararg packets: T) { - require(packets.isNotEmpty()) { "You need to specify one or more packets to send" } - val payloads = createPayloads(packets) - val chunk = entity.level().chunkSource as? ServerChunkCache - ?: throw IllegalStateException("Cannot send clientbound payloads on the client") - if (self) chunk.broadcastAndSend(entity, makeClientboundPacket(*payloads.toTypedArray())) - else chunk.broadcast(entity, makeClientboundPacket(*payloads.toTypedArray())) - } - - /** - * Sends one or more packets to all players tracking chunk [pos] in [level]. - * - * @param level The [ServerLevel] containing the chunk. - * @param pos The [ChunkPos] of the chunk being tracked. - * @param packets The packets to send. - */ - fun toPlayersTrackingChunk(level: ServerLevel, pos: ChunkPos, vararg packets: T) = - toPlayers(level.chunkSource.chunkMap.getPlayers(pos, false), *packets) - - @Suppress("UNCHECKED_CAST") - private fun createPayloads(packets: Array): List { - return packets.map { - createPayload( - packet = it, - classes = clientClasses, - configs = clientConfigs, - payloadId = id.withSuffix("_server"), - missingMessage = "Trying to send a packet to clients but client hasn't registered the packet and its handler", - ) - } - } - - @Suppress("UNCHECKED_CAST") - private fun createPayload( - packet: T, - classes: List>, - configs: List, - payloadId: ResourceLocation, - missingMessage: String, - ): Payload { - val klass = classes.find { it == packet::class } as? KClass - ?: throw IllegalStateException(missingMessage) - val index = classes.indexOf(klass) - val config = configs.find { it::class == klass } - val bytes = if (config != null) { - SerializationManager.cbor.encodeToByteArray(config.serializer, packet as ConfigSpec) - } - else { - SerializationManager.cbor.encodeToByteArray(klass.serializer(), packet) - } - return Payload(payloadId, index, bytes) - } - - @Suppress("UNCHECKED_CAST") - private fun decodeDispatchData( - payload: Payload, - classes: List>, - handlers: List>, - configs: List, - missingClassMessage: String, - missingHandlerMessage: String, - ctx: NetworkManager.PacketContext - ): Pair> { - val klass = classes.getOrNull(payload.index) - ?: throw NoSuchElementException(missingClassMessage) - val handler = handlers.getOrNull(payload.index) as? PacketHandler - ?: throw NoSuchElementException(missingHandlerMessage) - val config = configs.find { it::class == klass } - val msg = if (config != null) { - foldEnv( - client = { - SerializationManager.cbor.decodeFromByteArray(config.serializer, payload.data) - }, - server = { - if (ctx.player.hasPermissions(3)) - { - SerializationManager.cbor.decodeFromByteArray(config.serializer, payload.data) - toAllPlayers(config) - config - } else - { - ctx.player.sendSystemMessage { - style { - color = KColor.RED.toTextColor() - underlined = true - } - translate("archie.networking.config.no_permissions") - } - toPlayer(ctx.player as ServerPlayer, config) - config - } - }) - } else { - SerializationManager.cbor.decodeFromByteArray(klass.serializer(), payload.data) - } - return msg to handler - } - - private fun makeClientboundPacket(vararg payloads: CustomPacketPayload): Packet<*> { - return if (payloads.size == 1) ClientboundCustomPayloadPacket(payloads.first()) - else ClientboundBundlePacket(payloads.map { ClientboundCustomPayloadPacket(it) }) - } - - /** - * Registers this channel with the Architectury networking layer. - * - * Must be called once during mod initialization (before any packets are sent or received). - * Both [serverbound] and [clientbound] handlers should be registered before calling this. - */ - @Suppress("UNCHECKED_CAST") - fun register() { - EnvExecutor.runInEnv(Env.SERVER) { - Runnable { - NetworkManager.registerS2CPayloadType(serverPacketId, PayloadCodec) - } - } - EnvExecutor.runInEnv(Env.CLIENT) { - Runnable { - NetworkManager.registerReceiver(NetworkManager.Side.S2C, serverPacketId, PayloadCodec) { payload, ctx -> - val (msg, handler) = decodeDispatchData( - payload = payload, - classes = clientClasses, - handlers = clientboundHandlers, - configs = clientConfigs, - missingClassMessage = "No class was found on the clientside. Did you forget to do clientbound?", - missingHandlerMessage = "No handler was found on the clientside. Did you forget to do clientbound?", - ctx = ctx - ) - handler(msg, object : IPacketContext - { - override val player: Player get() = ctx.player - override val registryAccess: RegistryAccess get() = ctx.registryAccess() - }) - } - } - } - - - NetworkManager.registerReceiver(NetworkManager.Side.C2S, clientPacketId, PayloadCodec) { payload, ctx -> - val (msg, handler) = decodeDispatchData( - payload = payload, - classes = serverClasses, - handlers = serverboundHandlers, - configs = serverConfigs, - missingClassMessage = "No class was found on the serverside. Did you forget to do serverbound?", - missingHandlerMessage = "No handler was found on the serverside. Did you forget to do serverbound?", - ctx = ctx - ) - handler(msg, object : IPacketContext - { - override val player: Player get() = ctx.player - override val registryAccess: RegistryAccess get() = ctx.registryAccess() - }) - } - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.common.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.common.kt deleted file mode 100644 index 13d6e55bd..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.common.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.kernelpanicsoft.archie.registries - -import dev.architectury.platform.Mod - -/** - * Schedules [block] to run on the client at the earliest point client-side registration APIs - * that depend on registries already being populated - like Architectury's - * `MenuRegistry.registerScreenFactory` - are safe to call. Backed by an `actual` per mod loader, - * since the loaders genuinely differ on where that point is; only call this from inside a - * client-only guard (e.g. [net.kernelpanicsoft.archie.util.onClient]) - it does no environment - * checking of its own. - * - * On Fabric there's no staged registry-event model to race, so this runs [block] effectively - * immediately. On NeoForge, [dev.architectury.event.events.common.LifecycleEvent.SETUP]/ - * `FMLCommonSetupEvent` - the timing [ADeferredRegistryHolder.initClient] used to schedule on - * unconditionally - actually runs *after* several client registration-stage events (e.g. - * `RegisterMenuScreensEvent`), so calling `MenuRegistry.registerScreenFactory` from there - * silently never fires: it internally attaches a listener for that exact event, which has - * already fired and moved on by the time Common Setup runs. - * - * @param mod The mod whose event bus [block] should run on (NeoForge only needs this - Fabric's - * `actual` ignores it, since Fabric has no per-mod bus to look up). - */ -expect fun scheduleEarlyClientRegistration(mod: Mod, block: () -> Unit) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ACreativeTabRegistry.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ACreativeTabRegistry.kt deleted file mode 100644 index eea74c7bc..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ACreativeTabRegistry.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.kernelpanicsoft.archie.registries - -import dev.architectury.registry.CreativeTabRegistry -import net.minecraft.world.item.CreativeModeTab - -/** Thin wrapper over Architectury's [CreativeTabRegistry] for one-off creative tab creation. */ -object ACreativeTabRegistry -{ - /** Builds a [CreativeModeTab] via [block] without registering it. See [CreativeTabRegistryHelper] to register one. */ - fun create(block: CreativeModeTab.Builder.() -> Unit): CreativeModeTab = CreativeTabRegistry.create(block) -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ADeferredRegistryHolder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ADeferredRegistryHolder.kt deleted file mode 100644 index 04a66b104..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ADeferredRegistryHolder.kt +++ /dev/null @@ -1,89 +0,0 @@ -package net.kernelpanicsoft.archie.registries - -import dev.architectury.platform.Mod -import dev.architectury.registry.registries.DeferredRegister -import dev.architectury.registry.registries.RegistrySupplier -import net.kernelpanicsoft.archie.util.onClient -import net.kernelpanicsoft.archie.util.rem -import net.minecraft.core.Registry -import net.minecraft.resources.ResourceKey -import net.minecraft.resources.ResourceLocation -import kotlin.reflect.KProperty - -/** - * A registry holder that stores every registered entry in a [Map] keyed by its [ResourceLocation], - * providing O(1) lookup by id string as well as property delegation via `by register(...)`. - * - * This class wraps an Architectury [DeferredRegister] and exposes the registered suppliers - * through the [Map] interface. Use it as a base for per-registry object singletons. - * - * **Note:** Always use `by register(...)` (delegation) rather than calling `.get()` eagerly, - * to avoid touching the registry before it is unfrozen. - * - * ### Example - * ```kotlin - * object MyItems : ADeferredRegistryHolder(MyMod.MOD, Registries.ITEM) { - * val MY_ITEM by register("my_item") { Item(Item.Properties()) } - * } - * // In mod init: - * MyItems.init() - * ``` - * - * @param T The registry entry type. - */ -abstract class ADeferredRegistryHolder private constructor( - private val mod: Mod, - registryKey: ResourceKey>, - private val map: MutableMap> -) : - Map> by map -{ - constructor(mod: Mod, registryKey: ResourceKey>) : this(mod, registryKey, mutableMapOf()) - - private val registry: DeferredRegister = DeferredRegister.create(mod.modId, registryKey) - - /** - * Registers the underlying [DeferredRegister], then schedules [initClient] to run on the - * client, at the earliest point registration APIs that depend on registries already being - * populated (e.g. Architectury's `MenuRegistry.registerScreenFactory`) are safe to call - see - * [scheduleEarlyClientRegistration]. Must be called once during mod initialization. - */ - fun init() - { - registry.register() - onClient { - scheduleEarlyClientRegistration(mod) { - initClient() - } - } - } - - /** Client-only setup run after [init] - see [scheduleEarlyClientRegistration] for exactly when. No-op by default. */ - open fun initClient() = Unit - - /** Looks up a registered entry by its unqualified [id] (namespaced under [mod] automatically). */ - operator fun get(id: String): RegistrySupplier? = map[mod % id] - - - /** Property delegate operator that unwraps a [RegistrySupplier] to its concrete value. */ - operator fun RegistrySupplier.getValue(any: Any?, property: KProperty<*>): R - { - return get() - } - - /** Registers an entry under [id] (namespaced under [mod]) and records it in [map]. */ - protected fun register(id: String, supplier: () -> R): RegistrySupplier - { - val ret = registry.register(id, supplier) - map[ret.registryId] = ret - return ret - } - - /** Registers an entry under the fully-qualified [id] and records it in [map]. */ - protected fun register(id: ResourceLocation, supplier: () -> R): RegistrySupplier - { - val ret = registry.register(id, supplier) - map[ret.registryId] = ret - return ret - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/BlockRegistryHelper.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/BlockRegistryHelper.kt deleted file mode 100644 index 70e2e3712..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/BlockRegistryHelper.kt +++ /dev/null @@ -1,63 +0,0 @@ -package net.kernelpanicsoft.archie.registries - -import dev.architectury.registry.registries.DeferredRegister -import dev.architectury.registry.registries.RegistrySupplier -import net.minecraft.core.registries.Registries -import net.minecraft.world.item.BlockItem -import net.minecraft.world.item.Item -import net.minecraft.world.level.block.Block - -/** - * A [RegistryHelper] specialised for [Block] registration that also automatically registers - * a corresponding [BlockItem] in the item registry. - * - * Extend this class for each set of blocks in your mod. Each call to [block] registers both - * the block and (optionally) its item form. - * - * ### Example - * ```kotlin - * object MyBlocks : BlockRegistryHelper(modId) { - * val MY_BLOCK by block("my_block") { MyBlock(BlockBehaviour.Properties.of()) } - * } - * - * // In mod init: - * MyBlocks.init() - * ``` - * - * @param T The base block type; must extend [Block]. - * @param modId The mod ID used as the namespace for registered entries. - */ -@Suppress("UNCHECKED_CAST") -open class BlockRegistryHelper(modId: String) : RegistryHelper( - DeferredRegister.create(modId, Registries.BLOCK) as DeferredRegister, -) { - /** The [DeferredRegister] for the item registry, used to register [BlockItem]s. */ - open val itemRegistry: DeferredRegister = DeferredRegister.create(modId, Registries.ITEM) - - override fun init() = super.init().also { itemRegistry.register() } - - /** - * Registers a block and its associated [BlockItem]. - * - * The [itemSupplier] defaults to a plain [BlockItem]. Pass `null` to suppress item - * registration entirely (useful for technical blocks that should not appear in inventories). - * - * @param id The registry name for both the block and its item. - * @param itemSupplier A factory that produces the [BlockItem] given the registered block - * and default [Item.Properties]. Pass `null` to skip item registration. - * @param supplier Factory that produces the block instance. - * @return A [RegistrySupplier] for the registered block. - */ - open fun block( - id: String, - itemSupplier: ((V, Item.Properties) -> BlockItem)? = { block, props -> BlockItem(block, props) }, - supplier: () -> V, - ): RegistrySupplier { - val holder = register(id, supplier) - itemRegistry.register(id) { - val block = holder.get() - itemSupplier?.invoke(block, Item.Properties()) ?: BlockItem(block, Item.Properties()) - } - return holder - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/CreativeTabRegistryHelper.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/CreativeTabRegistryHelper.kt deleted file mode 100644 index 9e5fbd621..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/CreativeTabRegistryHelper.kt +++ /dev/null @@ -1,45 +0,0 @@ -package net.kernelpanicsoft.archie.registries - -import dev.architectury.registry.CreativeTabRegistry -import dev.architectury.registry.registries.DeferredRegister -import dev.architectury.registry.registries.RegistrySupplier -import net.minecraft.core.registries.Registries -import net.minecraft.world.item.CreativeModeTab - -/** - * A [RegistryHelper] specialised for [CreativeModeTab] registration via Architectury's - * [CreativeTabRegistry]. - * - * ### Example - * ```kotlin - * object MyTabs : CreativeTabRegistryHelper(modId) { - * val MY_TAB by create("my_tab") { - * title(Component.translatable("itemGroup.mymod.my_tab")) - * icon { ItemStack(MyBlocks.MY_BLOCK) } - * displayItems { _, output -> - * output.accept(MyBlocks.MY_BLOCK) - * } - * } - * } - * - * // In mod init: - * MyTabs.init() - * ``` - * - * @param T The creative tab type; must extend [CreativeModeTab]. - * @param modId The mod ID used as the namespace for registered entries. - */ -@Suppress("UNCHECKED_CAST") -open class CreativeTabRegistryHelper(modId: String) : RegistryHelper( - DeferredRegister.create(modId, Registries.CREATIVE_MODE_TAB) as DeferredRegister, -) { - /** - * Registers a new [CreativeModeTab] using an Architectury [CreativeTabRegistry] builder. - * - * @param id The registry name of the creative tab. - * @param block A builder lambda applied to [CreativeModeTab.Builder] to configure the tab. - * @return A [RegistrySupplier] for the registered creative tab. - */ - open fun create(id: String, block: CreativeModeTab.Builder.() -> Unit): RegistrySupplier = - register(id) { CreativeTabRegistry.create(block) as V } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistrarHelper.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistrarHelper.kt deleted file mode 100644 index 64af3dc65..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistrarHelper.kt +++ /dev/null @@ -1,42 +0,0 @@ -package net.kernelpanicsoft.archie.registries - -import dev.architectury.registry.registries.Registrar -import dev.architectury.registry.registries.RegistrarBuilder -import dev.architectury.registry.registries.RegistrarManager -import net.kernelpanicsoft.archie.util.rem - -/** - * A base for declaring **custom** Architectury registries (i.e. a whole new registry, the way - * [net.minecraft.core.registries.Registries.ITEM] is a registry) via lazily-built [Registrar]s. - * - * This is unrelated to registering *entries* into an existing registry - for that, use - * [RegistryHelper]/[ADeferredRegistryHolder] with a [dev.architectury.registry.registries.DeferredRegister]. - * Once a custom registry declared here exists, populating it with entries still requires its - * own [dev.architectury.registry.registries.DeferredRegister] targeting the [Registrar]'s - * registry key, created separately from this helper. - * - * @param modId The mod id passed to [RegistrarManager.get] and used as the namespace for each [registry]. - */ -abstract class RegistrarHelper(private val modId: String) -{ - private val manager = RegistrarManager.get(modId) - private val lazies = mutableListOf>>() - - /** - * Declares a lazily-built custom [Registrar] (registry) for [id], configured via [block]. - * - * @param id The unqualified registry id, namespaced under [modId]. - */ - fun registry(id: String, block: RegistrarBuilder.() -> Unit = {}): Lazy> - { - return lazy { - manager.builder(modId % id).apply(block).build() - }.also { lazies += it } - } - - /** Forces every custom registry declared via [registry] to be built. */ - fun init() - { - lazies.forEach { it.value } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistryHelper.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistryHelper.kt deleted file mode 100644 index 2bb4923b7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistryHelper.kt +++ /dev/null @@ -1,66 +0,0 @@ -package net.kernelpanicsoft.archie.registries - -import dev.architectury.registry.registries.DeferredRegister -import dev.architectury.registry.registries.RegistrySupplier -import net.minecraft.resources.ResourceLocation -import kotlin.reflect.KProperty - -/** - * A convenience base class for managing a collection of registry entries backed by an - * Architectury [DeferredRegister]. - * - * Subclass this object (or class) for each registry you want to populate, declare your - * entries with `by register(...)`, and call [init] during your mod's initialization phase. - * - * **Important:** Always use the `by` delegation operator when declaring entries to avoid - * "Registry is frozen" errors that occur when values are accessed before the registry tick. - * - * ### Example - * ```kotlin - * object MyItems : RegistryHelper(DeferredRegister.create(modId, Registries.ITEM)) { - * val MY_ITEM by register("my_item") { Item(Item.Properties()) } - * } - * - * // In mod init: - * MyItems.init() - * ``` - * - * @param T The base type stored in the target registry. - * @property registry The [DeferredRegister] to which entries will be submitted. - */ -abstract class RegistryHelper(val registry: DeferredRegister) { - - /** - * Registers this helper's [DeferredRegister] with the game's registry system. - * - * Must be called once during mod initialization. - */ - open fun init() = registry.register() - - /** - * Registers a new entry and returns a [RegistrySupplier] for lazy access. - * - * @param id The entry's registry name (without namespace). The mod namespace is prepended automatically. - * @param supplier Factory producing the entry. Must not cache the returned instance. - * @return A [RegistrySupplier] wrapping the registered entry. - */ - open fun register(id: String, supplier: () -> V): RegistrySupplier = - registry.register(id, supplier) - - /** - * Registers a new entry using a fully-qualified [ResourceLocation] and returns a [RegistrySupplier]. - * - * @param id The fully-qualified [ResourceLocation] for the entry. - * @param supplier Factory producing the entry. Must not cache the returned instance. - * @return A [RegistrySupplier] wrapping the registered entry. - */ - open fun register(id: ResourceLocation, supplier: () -> V): RegistrySupplier = - registry.register(id, supplier) - - /** - * Kotlin property delegate operator that unwraps a [RegistrySupplier] to its concrete value. - * - * This enables the idiomatic `val MY_ENTRY by register(...)` pattern. - */ - operator fun RegistrySupplier.getValue(any: Any?, property: KProperty<*>): V = get() -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/extensions.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/extensions.kt deleted file mode 100644 index 05f57a0cb..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/extensions.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.kernelpanicsoft.archie.registries - -import dev.architectury.extensions.injected.InjectedRegistryEntryExtension -import dev.architectury.registry.registries.RegistrySupplier -import net.minecraft.core.Holder -import net.minecraft.resources.ResourceLocation -import kotlin.reflect.KProperty - -/** The registry name Architectury injected into this registry entry. Only valid once registered. */ -val InjectedRegistryEntryExtension.id: ResourceLocation - get() = `arch$registryName`()!! - -/** The [Holder] Architectury injected into this registry entry. Only valid once registered. */ -val InjectedRegistryEntryExtension.holder: Holder - get() = `arch$holder`() - diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/resourcepacks/SerializationReloadListener.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/resourcepacks/SerializationReloadListener.kt deleted file mode 100644 index 730a6b2a7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/resourcepacks/SerializationReloadListener.kt +++ /dev/null @@ -1,88 +0,0 @@ -package net.kernelpanicsoft.archie.resourcepacks - -import com.mojang.logging.LogUtils -import kotlinx.serialization.KSerializer -import kotlinx.serialization.StringFormat -import net.minecraft.resources.FileToIdConverter -import net.minecraft.resources.ResourceLocation -import net.minecraft.server.packs.resources.ResourceManager -import net.minecraft.server.packs.resources.SimplePreparableReloadListener -import net.minecraft.util.profiling.ProfilerFiller -import org.slf4j.Logger -import java.io.InputStreamReader - -/** - * An abstract [SimplePreparableReloadListener] that automatically discovers resource files - * under a given [directory] and deserializes them using kotlinx.serialization. - * - * Override [apply] to store or process the resulting map of [ResourceLocation] to [T] after - * each reload. Register instances of subclasses with Architectury's `ReloadListenerRegistry` - * during `initClient()`. - * - * ### Example - * ```kotlin - * class MyDataListener : SerializationReloadListener( - * format = Json { ignoreUnknownKeys = true }, - * serializer = MyData.serializer(), - * directory = "my_data", - * fileExtension = ".json", - * ) { - * override fun apply(prepared: Map, ...) { - * MyDataRegistry.ENTRIES.clear() - * MyDataRegistry.ENTRIES += prepared - * } - * } - * ``` - * - * @param T The data class type that each resource file deserializes into. - * @param format The kotlinx.serialization [StringFormat] to use (e.g. `Json`, `Toml`). - * @param serializer The [KSerializer] for [T]. - * @param directory The resource-pack directory to scan (e.g. `"archie_themes"`). - * @param fileExtension The file extension to match, including the leading dot (e.g. `".json"`). - */ -abstract class SerializationReloadListener( - private val format: StringFormat, - private val serializer: KSerializer, - private val directory: String, - private val fileExtension: String, -) : SimplePreparableReloadListener>() { - - companion object { - private val LOGGER: Logger = LogUtils.getLogger() - } - - /** - * Returns `true` when [fileLocation] should be decoded by this listener. - * - * Subclasses can override this to skip known non-data resources that share the same - * directory and extension pattern. - */ - protected open fun shouldLoadResource(fileLocation: ResourceLocation): Boolean = true - - /** - * Scans [resourceManager] for all files matching [directory] / * [fileExtension], - * deserializes each one, and returns the resulting map keyed by entry id. - * - * Errors in individual files are logged and that file is skipped; other entries still load. - */ - override fun prepare( - resourceManager: ResourceManager, - profiler: ProfilerFiller, - ): Map { - val dataMap = mutableMapOf() - val fileToIdConverter = FileToIdConverter(directory, fileExtension) - - for ((fileLocation, resource) in fileToIdConverter.listMatchingResources(resourceManager)) { - if (!shouldLoadResource(fileLocation)) continue - val resourceId = fileToIdConverter.fileToId(fileLocation) - try { - InputStreamReader(resource.open()).use { reader -> - dataMap[resourceId] = format.decodeFromString(serializer, reader.readText()) - } - } catch (e: Exception) { - LOGGER.error("Couldn't parse data file {} from {}", resourceId, fileLocation, e) - } - } - return dataMap - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ArchieDataAttachmentImpl.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ArchieDataAttachmentImpl.kt deleted file mode 100644 index 4696d61a2..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ArchieDataAttachmentImpl.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.kernelpanicsoft.archie.serialization - -import earth.terrarium.common_storage_lib.data.DataManager - -/** - * Thin wrapper around a single Common Storage Lib [DataManager], backing [AttachmentRegistry.attachment]. - * See [ArchieDataAttachment] for the full contract this implements. - */ -internal class ArchieDataAttachmentImpl(private val manager: DataManager) : ArchieDataAttachment -{ - override fun get(holder: Any): T = manager.get(holder) - override fun getOrThrow(holder: Any): T = manager.getOrThrow(holder) - override fun getOrCreate(holder: Any, default: T): T = manager.getOrCreate(holder, default) - - override fun set(holder: Any, value: T): T - { - manager.set(holder, value) - return value - } - - override fun remove(holder: Any): T = manager.remove(holder) - override fun has(holder: Any): Boolean = manager.has(holder) - override fun modify(holder: Any, block: (T) -> T): T = manager.modify(holder, block) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/AttachmentRegistry.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/AttachmentRegistry.kt deleted file mode 100644 index 47f4dc59c..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/AttachmentRegistry.kt +++ /dev/null @@ -1,102 +0,0 @@ -package net.kernelpanicsoft.archie.serialization - -import earth.terrarium.common_storage_lib.data.DataManagerRegistry -import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.serializer -import net.kernelpanicsoft.archie.config.toSnakeCase -import kotlin.properties.PropertyDelegateProvider -import kotlin.properties.ReadOnlyProperty - -/** - * Base class for declaring a mod's [ArchieDataAttachment]s, wrapping a single Common Storage Lib - * `DataManagerRegistry`. Declare one `object` per mod extending this class, declare attachments - * as delegated properties on it via [attachment] (or one of the primitive convenience wrappers), - * then call [init] once at mod-init time - after those property initializers have already run, - * same ordering as [net.kernelpanicsoft.archie.networking.NetworkChannel]/`Config.init()`. - * - * ```kotlin - * object MyAttachments : AttachmentRegistry(MyMod.MOD_ID) { - * val mana by intAttachment(sync = true, default = { 0 }) - * } - * - * // mod init: - * MyAttachments.init() - * ``` - */ -abstract class AttachmentRegistry(modId: String) -{ - @PublishedApi - internal val registry = DataManagerRegistry(modId) - - /** - * Declares an [ArchieDataAttachment] backed by [serializer], keyed by the delegated property's - * snake_case name. See [ArchieDataAttachment] for the full contract, including exactly what - * [sync] and [itemComponent] each do and don't cover. - * - * @param sync Reactively push updates to tracking players on every [ArchieDataAttachment.set]/ - * [ArchieDataAttachment.remove] - Entity/BlockEntity on both loaders, ServerLevel on NeoForge - * only. - * @param copyOnDeath Preserve the value across a player's death/respawn. Entity/BlockEntity - * holders only; meaningless (and untested by Archie) for `itemComponent`-only attachments. - * @param itemComponent Additionally back this attachment with a vanilla `DataComponentType`, - * making it usable on `ItemStack` holders too. - * @param default Supplies the value used before a holder has anything explicitly set. - */ - fun attachment( - serializer: KSerializer, - sync: Boolean = false, - copyOnDeath: Boolean = false, - itemComponent: Boolean = false, - default: () -> T, - ): PropertyDelegateProvider>> = PropertyDelegateProvider { _, property -> - val builder = registry.builder(default).serialize(serializer.codec) - // itemComponent needs a client codec regardless of `sync`: CSL's own builder unconditionally - // calls `.networkSynchronized(clientCodec)` when building the DataComponentType, and leaves - // clientCodec null unless networkSerializer(...) was called - passing null there breaks at - // registration time. Always supplying our own explicit StreamCodec here (rather than CSL's - // no-arg networkSerializer(), which derives one from the Codec instead) keeps this consistent - // with the rest of Archie's serialization, which encodes over the network via kotlinx CBOR - // directly rather than round-tripping through a Codec. - if (sync || itemComponent) builder.networkSerializer(serializer.streamCodec) - if (copyOnDeath) builder.copyOnDeath() - if (itemComponent) builder.withDataComponent() - // ArchieDataAttachment itself implements ReadWriteProperty (so it can *also* back a `var - // Holder.x by MyAttachments.x` extension property once resolved) - if `attachment(...)` - // returned it directly as the PropertyDelegateProvider's own delegate type, `by attachment(...)` - // here would unwrap straight through to ArchieDataAttachment's getValue() and bind `mana`'s - // type to T, not to ArchieDataAttachment itself. Wrapping it in a plain ReadOnlyProperty - // stops that second unwrap, the same way NBTHolderImpl.itemField/fluidField/energyField wrap - // their storage objects for the exact same reason. - val attachment = ArchieDataAttachmentImpl(builder.buildAndRegister(property.name.toSnakeCase())) - ReadOnlyProperty { _, _ -> attachment } - } - - fun booleanAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Boolean = { false }) = - attachment(Boolean.serializer(), sync, copyOnDeath, itemComponent, default) - fun byteAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Byte = { 0 }) = - attachment(Byte.serializer(), sync, copyOnDeath, itemComponent, default) - fun shortAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Short = { 0 }) = - attachment(Short.serializer(), sync, copyOnDeath, itemComponent, default) - fun intAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Int = { 0 }) = - attachment(Int.serializer(), sync, copyOnDeath, itemComponent, default) - fun longAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Long = { 0 }) = - attachment(Long.serializer(), sync, copyOnDeath, itemComponent, default) - fun floatAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Float = { 0.0f }) = - attachment(Float.serializer(), sync, copyOnDeath, itemComponent, default) - fun doubleAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> Double = { 0.0 }) = - attachment(Double.serializer(), sync, copyOnDeath, itemComponent, default) - fun stringAttachment(sync: Boolean = false, copyOnDeath: Boolean = false, itemComponent: Boolean = false, default: () -> String = { "" }) = - attachment(String.serializer(), sync, copyOnDeath, itemComponent, default) - - /** Registers every attachment declared through this registry against the mod event bus. Call once, at mod-init time, after all of this object's `by attachment(...)` properties have already run. */ - fun init() = registry.init() -} - -/** Reified variant of [AttachmentRegistry.attachment] that resolves the [KSerializer] for [T] automatically. */ -inline fun AttachmentRegistry.attachment( - sync: Boolean = false, - copyOnDeath: Boolean = false, - itemComponent: Boolean = false, - noinline default: () -> T, -): PropertyDelegateProvider>> = attachment(serializer(), sync, copyOnDeath, itemComponent, default) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/DataAttachment.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/DataAttachment.kt deleted file mode 100644 index c9fb30ca1..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/DataAttachment.kt +++ /dev/null @@ -1,106 +0,0 @@ -package net.kernelpanicsoft.archie.serialization - -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty - -/** - * A [DataManager][earth.terrarium.common_storage_lib.data.DataManager]-backed attachment. Unlike - * [NBTHolder], which owns its own per-instance field storage, a single [ArchieDataAttachment] - * instance is stateless and reusable as the delegate for a `var Holder.property by ...` - * extension property on *any* number of holder instances - the holder passed to each method - * (or, via [getValue]/[setValue], the property's receiver) is where the data actually lives. - * Obtain instances via [AttachmentRegistry.attachment]. - * - * ### Usage - * ```kotlin - * object MyAttachments : AttachmentRegistry(MyMod.MOD_ID) { - * val mana by intAttachment(sync = true, default = { 0 }) - * } - * var Entity.mana by MyAttachments.mana - * - * // in mod init, after MyAttachments' properties above have already run: - * MyAttachments.init() - * ``` - * - * ### Supported holder types - * What's supported depends on the platform and on whether the attachment was declared with - * `itemComponent = true`: - * - **Entity / BlockEntity**: a NeoForge attachment / Fabric `AttachmentTarget`, on both platforms. - * - **ServerLevel**: NeoForge only. Fabric's `updateTarget` dispatch has no `Level`/`ServerLevel` - * case at all, so even where the underlying `get`/`set`/`has` calls happen to succeed there, - * `sync = true` will silently never push an update. Don't rely on world-level attachments if - * you need Fabric parity. - * - **ItemStack**: only if declared with `itemComponent = true` (backed by a vanilla - * [net.minecraft.core.component.DataComponentType] instead of an attachment). Calling any - * method here against an `ItemStack` for an attachment that *wasn't* declared with - * `itemComponent = true` throws [NullPointerException] (its backing `DataComponentType` is - * null) rather than [IllegalArgumentException] - `ItemStack` still passes CSL's holder-kind - * check either way, it just has nowhere to actually read/write. - * - * Any other object type throws [IllegalArgumentException] from every method here except - * [getValue]/[setValue], which forward straight into [get]/[set]. - * - * ### `sync` vs. `itemComponent` - * These are two genuinely different mechanisms, not two flavors of one thing: - * - Entity/BlockEntity/ServerLevel sync (`sync = true` on [AttachmentRegistry.attachment]) is a - * reactive push straight out of [set]/[remove] to tracking players, driven by CSL's own - * `DataManagerImpl`/packets. - * - `itemComponent` attachments are **not** covered by that push at all - [set] on an `ItemStack` - * never sends anything itself. They ride vanilla's normal item/component replication instead - * (the same mechanism as vanilla's own `BundleContents`), which isn't reactive the same way. - * - * Declaring `itemComponent = true` without `sync = true` still needs a network codec under the - * hood (vanilla's `DataComponentType` always carries one) - [AttachmentRegistry.attachment] - * handles that for you regardless of what you pass for `sync`. - * - * ### `has()` after `get()` on Entity/BlockEntity/ServerLevel holders - * Confirmed on real Fabric/NeoForge attachment internals, not documented by Common Storage Lib - * itself: [get] on these holder kinds silently creates *and persists* the default value on first - * read (Fabric's `AttachmentTarget.getAttachedOrCreate`, NeoForge's `AttachmentHolder.getData` - - * both write-through on a miss, they don't just compute-and-discard). That means [has] can only - * tell "never touched" apart from "read once" if you call it *before* the first [get] - calling - * [get] first, then [has], will report `true` even though nothing was ever explicitly [set]. - * `ItemStack`/`itemComponent` holders don't have this quirk - `DataComponentHolder.getOrDefault` - * genuinely doesn't persist on read. - */ -interface ArchieDataAttachment : ReadWriteProperty -{ - /** - * Reads [holder]'s current value, falling back to the attachment's default if unset. Never - * throws for an unset value - only for an unsupported [holder]. - * - * On Entity/BlockEntity/ServerLevel holders, an unset read silently creates *and persists* the - * default - see the class-level "`has()` after `get()`" note before relying on [has] afterward. - */ - fun get(holder: Any): T - - /** Reads [holder]'s current value, throwing if it's never been explicitly [set]. The exact exception type (`NullPointerException` vs. `RuntimeException`) differs by platform for `ItemStack` holders - don't match on a specific type for that case. */ - fun getOrThrow(holder: Any): T - - /** Reads [holder]'s current value if [has] is true, otherwise [set]s it to [default] first. Returns the (possibly just-written) current value either way. */ - fun getOrCreate(holder: Any, default: T): T - - /** Writes [value] onto [holder], returning [value]. */ - fun set(holder: Any, value: T): T - - /** - * Removes [holder]'s explicitly-set value, if any, reverting subsequent [get] calls to the - * default. The return value mirrors the removed data 1:1 from the underlying Java API and can - * be a JVM-level null if nothing was set - prefer checking [has] first if you actually need it. - */ - fun remove(holder: Any): T - - /** True if [holder] has an explicitly-[set] value (as opposed to just reading back the default). */ - fun has(holder: Any): Boolean - - /** Reads [holder]'s current value, applies [block], writes the result back, and returns it. */ - fun modify(holder: Any, block: (T) -> T): T - - override operator fun getValue(thisRef: Any?, property: KProperty<*>): T = - get(thisRef ?: throw IllegalStateException("${property.name} has no receiver to read a data attachment from - it can't be a top-level property")) - - override operator fun setValue(thisRef: Any?, property: KProperty<*>, value: T) - { - set(thisRef ?: throw IllegalStateException("${property.name} has no receiver to write a data attachment to - it can't be a top-level property"), value) - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt deleted file mode 100644 index 39cb03450..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt +++ /dev/null @@ -1,244 +0,0 @@ -package net.kernelpanicsoft.archie.serialization - -import net.kernelpanicsoft.archie.config.toSnakeCase -import net.kernelpanicsoft.archie.transfer.ArchieEnergyStorage -import net.kernelpanicsoft.archie.transfer.ArchieFluidStorage -import net.kernelpanicsoft.archie.transfer.ArchieItemStorage -import dev.architectury.fluid.FluidStack -import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.MapSerializer -import kotlinx.serialization.builtins.serializer -import net.benwoodworth.knbt.NbtTag -import net.minecraft.core.component.DataComponents -import net.minecraft.nbt.CompoundTag -import net.minecraft.world.item.ItemStack -import net.minecraft.world.item.component.CustomData -import net.minecraft.world.level.block.entity.BlockEntity -import kotlin.properties.PropertyDelegateProvider -import kotlin.properties.ReadOnlyProperty -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty -import kotlin.reflect.full.hasAnnotation - -/** - * [NBTHolder] implementation backing [NBTHolder.fluid], persisting field values into [stack]'s - * [CustomData] component instead of an in-memory map. - */ -class FluidStackNBTHolderImpl(private val stack: FluidStack) : NBTHolder -{ - private val data: MutableMap = mutableMapOf() - private val itemStorage: MutableMap = mutableMapOf() - private val fluidStorage: MutableMap = mutableMapOf() - private val energyStorage: MutableMap = mutableMapOf() - - init - { - loadFromStack() - } - - override fun field( - serializer: KSerializer, - default: () -> T - ): PropertyDelegateProvider> - { - return PropertyDelegateProvider { thisRef, property -> - val delegate = object : ReadWriteProperty - { - override fun getValue(thisRef: Any?, property: KProperty<*>): T - { - loadFromStack() - return runCatching { - NBT.decodeFromNbtTagRootless(serializer, data.getOrPut(property.name.toSnakeCase()) { - NBT.encodeToNbtTagRootless(serializer, default()) - }) - }.recover { - val ret = default() - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(serializer, ret) - ret - }.getOrThrow() - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) - { - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(serializer, value) - saveToStack() - } - } - if (property.name.toSnakeCase() !in data) - delegate.setValue(thisRef, property, default()) - - delegate - } - } - - override fun listField( - serializer: KSerializer, - default: () -> List - ): PropertyDelegateProvider>> - { - return PropertyDelegateProvider { thisRef, property -> - val delegate = object : ReadWriteProperty> - { - override fun getValue(thisRef: Any?, property: KProperty<*>): MutableList - { - loadFromStack() - return ObservableList(runCatching { - NBT.decodeFromNbtTagRootless(ListSerializer(serializer), data.getOrPut(property.name.toSnakeCase()) { - NBT.encodeToNbtTagRootless(ListSerializer(serializer), default()) - }) - }.recover { - val ret = default() - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(ListSerializer(serializer), ret) - ret - }.getOrThrow().toMutableList()) { list -> setValue(thisRef, property, list)} - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: MutableList) - { - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(ListSerializer(serializer), value) - saveToStack() - } - } - if (property.name.toSnakeCase() !in data) - delegate.setValue(thisRef, property, default().toMutableList()) - - delegate - } - } - - override fun mapField( - serializer: KSerializer, - default: () -> Map - ): PropertyDelegateProvider>> - { - return PropertyDelegateProvider { thisRef, property -> - val delegate = object : ReadWriteProperty> - { - override fun getValue(thisRef: Any?, property: KProperty<*>): MutableMap - { - loadFromStack() - return ObservableMap(runCatching { - NBT.decodeFromNbtTagRootless(MapSerializer(String.serializer(), serializer), data.getOrPut(property.name.toSnakeCase()) { - NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), default()) - }) - }.recover { - val ret = default() - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), ret) - ret - }.getOrThrow().toMutableMap()) { map -> setValue(thisRef, property, map)} - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: MutableMap) - { - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), value) - saveToStack() - } - } - if (property.name.toSnakeCase() !in data) - delegate.setValue(thisRef, property, default().toMutableMap()) - - delegate - } - } - - override fun itemField(size: Int): PropertyDelegateProvider> - { - return PropertyDelegateProvider { thisRef, property -> - val onUpdate = { - saveToStack() - } - itemStorage[property.name.toSnakeCase()] = ArchieItemStorage(size, onUpdate) - ReadOnlyProperty { _, _ -> itemStorage[property.name.toSnakeCase()]!! } - } - } - - override fun fluidField(limit: Long, size: Int): PropertyDelegateProvider> - { - return PropertyDelegateProvider { thisRef, property -> - val onUpdate = { - saveToStack() - } - fluidStorage[property.name.toSnakeCase()] = ArchieFluidStorage(limit, size, onUpdate) - ReadOnlyProperty { _, _ -> fluidStorage[property.name.toSnakeCase()]!! } - } - } - - override fun energyField(capacity: Long): PropertyDelegateProvider> - { - return PropertyDelegateProvider { thisRef, property -> - val onUpdate = { - saveToStack() - } - energyStorage[property.name.toSnakeCase()] = ArchieEnergyStorage(capacity, onUpdate) - ReadOnlyProperty { _, _ -> energyStorage[property.name.toSnakeCase()]!! } - } - } - - override fun loadFromTag(compoundTag: CompoundTag) - { - forEachTag(compoundTag) { (key, value) -> - data[key] = value - } - itemStorage.forEach { (key, value) -> - value.readSnapshot(data.getOrPut(key) { - value.createSnapshot() - }) - } - fluidStorage.forEach { (key, value) -> - value.readSnapshot(data.getOrPut(key) { - value.createSnapshot() - }) - } - energyStorage.forEach { (key, value) -> - value.readSnapshot(data.getOrPut(key) { - value.createSnapshot() - }) - } - } - - override fun saveToTag(compoundTag: CompoundTag) - { - mergeToCompoundTag(compoundTag) { - itemStorage.forEach { (key, value) -> - data[key] = value.createSnapshot() - } - fluidStorage.forEach { (key, value) -> - data[key] = value.createSnapshot() - } - energyStorage.forEach { (key, value) -> - data[key] = value.createSnapshot() - } - data.forEach { (key, value) -> - put(key, value) - - } - } - } - - fun loadFromStack() - { - stack.get(DataComponents.CUSTOM_DATA)?.apply { - loadFromTag(copyTag()) - } - } - - fun saveToStack() - { - stack.applyComponents(buildComponentPatch { - set(DataComponents.CUSTOM_DATA, CustomData.of(CompoundTag().also { tag -> - saveToTag(tag) - })) - }) - } - - override fun getSyncTag(): CompoundTag - { - return CompoundTag() - } - - override fun updateProperty(propertyName: String, serializer: KSerializer, value: T) - { - this.data[propertyName] = NBT.encodeToNbtTagRootless(serializer, value) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt deleted file mode 100644 index e97bfe7b7..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt +++ /dev/null @@ -1,348 +0,0 @@ -package net.kernelpanicsoft.archie.serialization - -import net.kernelpanicsoft.archie.config.toSnakeCase -import net.kernelpanicsoft.archie.gui.item.SyncedItemHolder -import net.kernelpanicsoft.archie.transfer.ArchieEnergyStorage -import net.kernelpanicsoft.archie.transfer.ArchieFluidStorage -import net.kernelpanicsoft.archie.transfer.ArchieItemStorage -import earth.terrarium.common_storage_lib.storage.base.UpdateManager -import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.MapSerializer -import kotlinx.serialization.builtins.serializer -import net.benwoodworth.knbt.NbtTag -import net.minecraft.core.component.DataComponents -import net.minecraft.nbt.CompoundTag -import net.minecraft.world.item.ItemStack -import net.minecraft.world.item.component.CustomData -import kotlin.properties.PropertyDelegateProvider -import kotlin.properties.ReadOnlyProperty -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty -import kotlin.reflect.full.hasAnnotation - -/** - * [NBTHolder] implementation backing [NBTHolder.item], persisting field values into [stack]'s - * [CustomData] component instead of an in-memory map. - * - * `@Sync`-annotated fields additionally push updates through [SyncedItemHolder] when the - * delegating `thisRef` implements it - the [ItemStack]-holder equivalent of [NBTHolderImpl]'s - * `thisRef is BlockEntity` handling. - */ -class ItemStackNBTHolderImpl(private val stack: ItemStack) : NBTHolder -{ - private val data: MutableMap = mutableMapOf() - private val itemStorage: MutableMap = mutableMapOf() - private val fluidStorage: MutableMap = mutableMapOf() - private val energyStorage: MutableMap = mutableMapOf() - private val sync: MutableSet = mutableSetOf() - - init - { - loadFromStack() - } - - override fun field( - serializer: KSerializer, - default: () -> T - ): PropertyDelegateProvider> - { - return PropertyDelegateProvider { thisRef, property -> - if (property.hasAnnotation()) - { - sync += property.name.toSnakeCase() - if (thisRef is SyncedItemHolder) - thisRef.registerSyncedProperty(property.name.toSnakeCase(), serializer) - } - - val delegate = object : ReadWriteProperty - { - override fun getValue(thisRef: Any?, property: KProperty<*>): T - { - loadFromStack() - return runCatching { - NBT.decodeFromNbtTagRootless(serializer, data.getOrPut(property.name.toSnakeCase()) { - NBT.encodeToNbtTagRootless(serializer, default()) - }) - }.recover { - val ret = default() - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(serializer, ret) - ret - }.getOrThrow() - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) - { - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(serializer, value) - if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) - thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), serializer, value) - saveToStack() - } - } - if (property.name.toSnakeCase() !in data) - delegate.setValue(thisRef, property, default()) - else if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) - // Value pre-existed on the stack, so setValue() above never ran - announce it now - // so a menu opened against pre-existing data doesn't start out unsynced. - thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), serializer, delegate.getValue(thisRef, property)) - - delegate - } - } - - override fun listField( - serializer: KSerializer, - default: () -> List - ): PropertyDelegateProvider>> - { - return PropertyDelegateProvider { thisRef, property -> - if (property.hasAnnotation()) - { - sync += property.name.toSnakeCase() - if (thisRef is SyncedItemHolder) - thisRef.registerSyncedProperty(property.name.toSnakeCase(), ListSerializer(serializer)) - } - - val delegate = object : ReadWriteProperty> - { - override fun getValue(thisRef: Any?, property: KProperty<*>): MutableList - { - loadFromStack() - return ObservableList(runCatching { - NBT.decodeFromNbtTagRootless(ListSerializer(serializer), data.getOrPut(property.name.toSnakeCase()) { - NBT.encodeToNbtTagRootless(ListSerializer(serializer), default()) - }) - }.recover { - val ret = default() - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(ListSerializer(serializer), ret) - ret - }.getOrThrow().toMutableList()) { list -> setValue(thisRef, property, list) } - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: MutableList) - { - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(ListSerializer(serializer), value) - if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) - thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ListSerializer(serializer), value.toList()) - saveToStack() - } - } - if (property.name.toSnakeCase() !in data) - delegate.setValue(thisRef, property, default().toMutableList()) - else if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) - // See the equivalent branch in field() above - same pre-existing-data gap. - thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ListSerializer(serializer), delegate.getValue(thisRef, property).toList()) - - delegate - } - } - - override fun mapField( - serializer: KSerializer, - default: () -> Map - ): PropertyDelegateProvider>> - { - return PropertyDelegateProvider { thisRef, property -> - if (property.hasAnnotation()) - { - sync += property.name.toSnakeCase() - if (thisRef is SyncedItemHolder) - thisRef.registerSyncedProperty(property.name.toSnakeCase(), MapSerializer(String.serializer(), serializer)) - } - - val delegate = object : ReadWriteProperty> - { - override fun getValue(thisRef: Any?, property: KProperty<*>): MutableMap - { - loadFromStack() - return ObservableMap(runCatching { - NBT.decodeFromNbtTagRootless(MapSerializer(String.serializer(), serializer), data.getOrPut(property.name.toSnakeCase()) { - NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), default()) - }) - }.recover { - val ret = default() - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), ret) - ret - }.getOrThrow().toMutableMap()) { map -> setValue(thisRef, property, map) } - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: MutableMap) - { - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), value) - if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) - thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), MapSerializer(String.serializer(), serializer), value.toMap()) - saveToStack() - } - } - if (property.name.toSnakeCase() !in data) - delegate.setValue(thisRef, property, default().toMutableMap()) - else if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) - // See the equivalent branch in field() above - same pre-existing-data gap. - thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), MapSerializer(String.serializer(), serializer), delegate.getValue(thisRef, property).toMap()) - - delegate - } - } - - override fun itemField(size: Int): PropertyDelegateProvider> - { - return PropertyDelegateProvider { thisRef, property -> - if (property.hasAnnotation()) - { - sync += property.name.toSnakeCase() - if (thisRef is SyncedItemHolder) - thisRef.registerSyncedProperty(property.name.toSnakeCase(), ArchieItemStorage.serializer()) - } - val onUpdate = { - if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) - thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ArchieItemStorage.serializer(), itemStorage[property.name.toSnakeCase()]!!) - saveToStack() - } - val storage = ArchieItemStorage(size, onUpdate) - // init { loadFromStack() } already ran (before this delegate even existed to be - // hydrated by loadFromTag's itemStorage.forEach loop, unlike a BlockEntity's field - // declarations - which all run in its constructor, before NBTBlockEntity.load() ever - // calls loadFromTag) - so data may already hold this key's raw tag with nothing to - // apply it to yet. Apply it now, directly, instead. - data[property.name.toSnakeCase()]?.let { storage.readSnapshot(it) } - itemStorage[property.name.toSnakeCase()] = storage - // readSnapshot() above never calls onUpdate, so announce the starting contents now. - if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) - thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ArchieItemStorage.serializer(), storage) - ReadOnlyProperty { _, _ -> itemStorage[property.name.toSnakeCase()]!! } - } - } - - override fun fluidField(limit: Long, size: Int): PropertyDelegateProvider> - { - return PropertyDelegateProvider { thisRef, property -> - if (property.hasAnnotation()) - { - sync += property.name.toSnakeCase() - if (thisRef is SyncedItemHolder) - thisRef.registerSyncedProperty(property.name.toSnakeCase(), ArchieFluidStorage.serializer()) - } - val onUpdate = { - if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) - thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ArchieFluidStorage.serializer(), fluidStorage[property.name.toSnakeCase()]!!) - saveToStack() - } - val storage = ArchieFluidStorage(limit, size, onUpdate) - data[property.name.toSnakeCase()]?.let { storage.readSnapshot(it) } - fluidStorage[property.name.toSnakeCase()] = storage - // See itemField() above - same "storage's initial contents never announced" gap. - if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) - thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ArchieFluidStorage.serializer(), storage) - ReadOnlyProperty { _, _ -> fluidStorage[property.name.toSnakeCase()]!! } - } - } - - override fun energyField(capacity: Long): PropertyDelegateProvider> - { - return PropertyDelegateProvider { thisRef, property -> - if (property.hasAnnotation()) - { - sync += property.name.toSnakeCase() - if (thisRef is SyncedItemHolder) - thisRef.registerSyncedProperty(property.name.toSnakeCase(), ArchieEnergyStorage.serializer()) - } - val onUpdate = { - if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) - thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ArchieEnergyStorage.serializer(), energyStorage[property.name.toSnakeCase()]!!) - saveToStack() - } - val storage = ArchieEnergyStorage(capacity, onUpdate) - data[property.name.toSnakeCase()]?.let { storage.readSnapshot(it) } - energyStorage[property.name.toSnakeCase()] = storage - // See itemField() above - same "storage's initial contents never announced" gap. - if (thisRef is SyncedItemHolder && property.name.toSnakeCase() in sync) - thisRef.onSyncedPropertyChanged(property.name.toSnakeCase(), ArchieEnergyStorage.serializer(), storage) - ReadOnlyProperty { _, _ -> energyStorage[property.name.toSnakeCase()]!! } - } - } - - override fun loadFromTag(compoundTag: CompoundTag) - { - forEachTag(compoundTag) { (key, value) -> - data[key] = value - } - itemStorage.forEach { (key, value) -> - value.readSnapshot(data.getOrPut(key) { - value.createSnapshot() - }) - } - fluidStorage.forEach { (key, value) -> - value.readSnapshot(data.getOrPut(key) { - value.createSnapshot() - }) - } - energyStorage.forEach { (key, value) -> - value.readSnapshot(data.getOrPut(key) { - value.createSnapshot() - }) - } - } - - override fun saveToTag(compoundTag: CompoundTag) - { - mergeToCompoundTag(compoundTag) { - itemStorage.forEach { (key, value) -> - data[key] = value.createSnapshot() - } - fluidStorage.forEach { (key, value) -> - data[key] = value.createSnapshot() - } - energyStorage.forEach { (key, value) -> - data[key] = value.createSnapshot() - } - data.forEach { (key, value) -> - put(key, value) - - } - } - } - - fun loadFromStack() - { - stack.get(DataComponents.CUSTOM_DATA)?.apply { - loadFromTag(copyTag()) - } - } - - fun saveToStack() - { - stack.applyComponents(buildComponentPatch { - set(DataComponents.CUSTOM_DATA, CustomData.of(CompoundTag().also { tag -> - saveToTag(tag) - })) - }) - } - - override fun getSyncTag(): CompoundTag - { - return buildCompoundTag { - data.filter { (key, _) -> key in sync } - .forEach { (key, value) -> put(key, value) } - } - } - - override fun updateProperty(propertyName: String, serializer: KSerializer, value: T) - { - // Storage-backed fields (item/fluid/energy) are canonically the *live* storage object, not - // `data` - write through readSnapshot(), or saveToTag() below would just re-derive `data` - // from the untouched live storage and clobber this write. - val storage: UpdateManager? = itemStorage[propertyName] ?: fluidStorage[propertyName] ?: energyStorage[propertyName] - if (storage != null && value is UpdateManager<*>) - { - @Suppress("UNCHECKED_CAST") - storage.readSnapshot((value as UpdateManager).createSnapshot()) - } - else - { - this.data[propertyName] = NBT.encodeToNbtTagRootless(serializer, value) - } - // Unlike NBTHolderImpl's `data` (a BlockEntity's own persisted state), `data` here is only - // a transient copy - must be flushed to the stack explicitly or a remote edit is lost. - saveToStack() - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/KOps.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/KOps.kt deleted file mode 100644 index acaee5b5b..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/KOps.kt +++ /dev/null @@ -1,674 +0,0 @@ -package net.kernelpanicsoft.archie.serialization - -import com.mojang.datafixers.util.Pair -import com.mojang.serialization.DataResult -import com.mojang.serialization.DynamicOps -import com.mojang.serialization.MapLike -import kotlinx.serialization.json.* -import net.benwoodworth.knbt.* -import net.minecraft.nbt.ListTag -import net.minecraft.nbt.NumericTag -import net.peanuuutz.tomlkt.* -import java.math.BigDecimal -import java.nio.ByteBuffer -import java.util.* -import java.util.function.BiConsumer -import java.util.function.Consumer -import java.util.stream.IntStream -import java.util.stream.LongStream -import java.util.stream.Stream - -/** - * Mojang [DynamicOps] implementations for the tree formats used elsewhere in this package - * ([kotlinx.serialization.json.JsonElement], [net.peanuuutz.tomlkt.TomlElement], and knbt's - * [net.benwoodworth.knbt.NbtTag]), so [com.mojang.serialization.Codec]s can operate on them - * directly. Registered with [SerializationManager] and used internally by [SerializerCodec] - * and [CodecSerializer]; not usually needed directly. - */ -object KOps -{ - /** [DynamicOps] over [JsonElement]. */ - object Json : DynamicOps - { - override fun empty(): JsonElement = JsonNull - - override fun convertTo(outOps: DynamicOps, input: JsonElement): U - { - when (input) - { - is JsonObject -> return convertMap(outOps, input) - is JsonArray -> return convertList(outOps, input) - is JsonNull -> return outOps.empty() - else -> - { - val literal = input.jsonPrimitive - if (literal.isString) - return outOps.createString(literal.content) - literal.booleanOrNull?.let { return outOps.createBoolean(it) } - val decimal = BigDecimal(literal.content) - try - { - return when (val long = decimal.longValueExact()) - { - long.toByte().toLong() -> outOps.createByte(long.toByte()) - long.toShort().toLong() -> outOps.createShort(long.toShort()) - long.toInt().toLong() -> outOps.createInt(long.toInt()) - else -> outOps.createLong(long) - } - } catch (e: ArithmeticException) - { - return when (val double = decimal.toDouble()) - { - double.toFloat().toDouble() -> outOps.createFloat(double.toFloat()) - else -> outOps.createDouble(double) - } - } - } - } - } - - override fun getNumberValue(input: JsonElement): DataResult - { - val literal = input.jsonPrimitive - try - { - val decimal = BigDecimal(literal.content) - - try - { - return when (val long = decimal.longValueExact()) - { - long.toByte().toLong() -> DataResult.success(long.toByte()) - long.toShort().toLong() -> DataResult.success(long.toShort()) - long.toInt().toLong() -> DataResult.success(long.toInt()) - else -> DataResult.success(long) - } - } catch (e: ArithmeticException) - { - return when (val double = decimal.toDouble()) - { - double.toFloat().toDouble() -> DataResult.success(double.toFloat()) - else -> DataResult.success(double) - } - } - } - catch (e: NumberFormatException) - { - return DataResult.error { "Not a number: $input" } - } - } - - override fun createNumeric(i: Number): JsonElement - { - return JsonPrimitive(i) - } - - override fun getBooleanValue(input: JsonElement): DataResult - { - val literal = input.jsonPrimitive - literal.booleanOrNull?.let { return DataResult.success(it) } ?: return DataResult.error { "Not a boolean: $input" } - - } - - override fun createBoolean(value: Boolean): JsonElement - { - return JsonPrimitive(value) - } - - override fun getStringValue(input: JsonElement): DataResult - { - val literal = input.jsonPrimitive - literal.contentOrNull?.takeIf { literal.isString }?.let { return DataResult.success(it) } ?: return DataResult.error { "Not a string: $input" } - } - - override fun createString(value: String): JsonElement - { - return JsonPrimitive(value) - } - - override fun mergeToList(list: JsonElement, value: JsonElement): DataResult - { - if (list !is JsonArray && list != empty()) - return DataResult.error({ "mergeToList called with not a list: $list" }, list) - if (list != empty()) - { - return DataResult.success(JsonArray(list.jsonArray + value)) - } - return DataResult.success(JsonArray(listOf(value))) - } - - override fun mergeToMap(map: JsonElement, key: JsonElement, value: JsonElement): DataResult - { - if (map !is JsonObject && map != empty()) - return DataResult.error({ "mergeToMap called with not a map: $map" }, map) - if (key !is JsonPrimitive || !key.jsonPrimitive.isString) - return DataResult.error({ "key is not a string: $key" }, map) - if (map != empty()) - { - return DataResult.success(JsonObject(map.jsonObject + mapOf(key.content to value))) - } - return DataResult.success(JsonObject(mapOf(key.content to value))) - } - - override fun getMapValues(input: JsonElement): DataResult>> - { - if (input !is JsonObject) return DataResult.error { "Not a json object: $input" } - return DataResult.success(input.entries.stream().map { (key, value) -> Pair(createString(key), value) }) - } - - override fun getMapEntries(input: JsonElement): DataResult>> - { - if (input !is JsonObject) return DataResult.error { "Not a json object: $input" } - return DataResult.success(Consumer { c -> - input.entries.forEach { (key, value) -> c.accept(createString(key), value) } - }) - } - - override fun getMap(input: JsonElement): DataResult> - { - if (input !is JsonObject) return DataResult.error { "Not a json object: $input" } - return DataResult.success(object : MapLike - { - override fun get(key: JsonElement): JsonElement? - { - return input[key.jsonPrimitive.content] - } - - override fun get(key: String): JsonElement? - { - return input[key] - } - - override fun entries(): Stream> - { - return input.entries.stream().map { (key, value) -> Pair(createString(key), value) } - } - }) - } - - override fun createMap(map: Stream>): JsonElement - { - return JsonObject(map.map { it.first.jsonPrimitive.content to it.second }.toList().toMap()) - } - - override fun getStream(input: JsonElement): DataResult> - { - return if (input is JsonArray) - DataResult.success(input.stream()) - else - DataResult.error { "Not a json array: $input" } - } - - override fun getList(input: JsonElement): DataResult>> - { - return if (input is JsonArray) - DataResult.success(Consumer { c -> - input.forEach { - c.accept(it) - } - }) - else - DataResult.error { "Not a json array: $input" } - } - - override fun createList(input: Stream): JsonElement - { - return JsonArray(input.toList()) - } - - override fun remove(input: JsonElement, key: String): JsonElement - { - if (input is JsonObject) - { - return JsonObject(input - key) - } - return input - } - } - - /** [DynamicOps] over [TomlElement]. */ - object Toml : DynamicOps - { - private fun Number.toTomlElement(): TomlElement - { - return when (this) - { - is Byte -> TomlLiteral(this) - is Short -> TomlLiteral(this) - is Int -> TomlLiteral(this) - is Long -> TomlLiteral(this) - is Float -> TomlLiteral(this) - is Double -> TomlLiteral(this) - else -> error("Unsupported class: ${this::class.simpleName}") - } - } - - override fun empty(): TomlElement = TomlNull - - override fun convertTo(outOps: DynamicOps, input: TomlElement): U - { - when (input) - { - is TomlTable -> return convertMap(outOps, input) - is TomlArray -> return convertList(outOps, input) - is TomlNull -> return outOps.empty() - else -> - { - val literal = input.asTomlLiteral() - if (literal.type == TomlLiteral.Type.String) - return outOps.createString(literal.toString()) - if (literal.type == TomlLiteral.Type.Boolean) - return outOps.createBoolean(literal.toBoolean()) - val decimal = BigDecimal(literal.content) - try - { - return when (val long = decimal.longValueExact()) - { - long.toByte().toLong() -> outOps.createByte(long.toByte()) - long.toShort().toLong() -> outOps.createShort(long.toShort()) - long.toInt().toLong() -> outOps.createInt(long.toInt()) - else -> outOps.createLong(long) - } - } catch (e: ArithmeticException) - { - return when (val double = decimal.toDouble()) - { - double.toFloat().toDouble() -> outOps.createFloat(double.toFloat()) - else -> outOps.createDouble(double) - } - } - } - } - } - - override fun getNumberValue(input: TomlElement): DataResult - { - val literal = input.asTomlLiteral() - try - { - val decimal = BigDecimal(literal.content) - - try - { - return when (val long = decimal.longValueExact()) - { - long.toByte().toLong() -> DataResult.success(long.toByte()) - long.toShort().toLong() -> DataResult.success(long.toShort()) - long.toInt().toLong() -> DataResult.success(long.toInt()) - else -> DataResult.success(long) - } - } catch (e: ArithmeticException) - { - return when (val double = decimal.toDouble()) - { - double.toFloat().toDouble() -> DataResult.success(double.toFloat()) - else -> DataResult.success(double) - } - } - } - catch (e: NumberFormatException) - { - return DataResult.error { "Not a number: $input" } - } - } - - override fun createNumeric(i: Number): TomlElement - { - return i.toTomlElement() - } - - override fun getBooleanValue(input: TomlElement): DataResult - { - val literal = input.asTomlLiteral() - return if (literal.type == TomlLiteral.Type.Boolean) - DataResult.success(literal.toBoolean()) - else - DataResult.error { "Not a boolean: $input" } - } - - override fun createBoolean(value: Boolean): TomlElement - { - return TomlLiteral(value) - } - - override fun getStringValue(input: TomlElement): DataResult - { - val literal = input.asTomlLiteral() - return if (literal.type == TomlLiteral.Type.String) - DataResult.success(literal.toString()) - else - DataResult.error { "Not a string: $input" } - } - - override fun createString(value: String): TomlElement - { - return TomlLiteral(value) - } - - override fun mergeToList(list: TomlElement, value: TomlElement): DataResult - { - if (list !is TomlArray && list != empty()) - return DataResult.error({ "mergeToList called with not a list: $list" }, list) - if (list != empty()) - { - return DataResult.success(TomlArray(list.asTomlArray().plus(value))) - } - return DataResult.success(TomlArray(value)) - } - - override fun mergeToMap(map: TomlElement, key: TomlElement, value: TomlElement): DataResult - { - if (map !is TomlTable && map != empty()) - return DataResult.error({ "mergeToMap called with not a map: $map" }, map) - if (key !is TomlLiteral || key.asTomlLiteral().type != TomlLiteral.Type.String) - return DataResult.error({ "key is not a string: $key" }, map) - if (map != empty()) - { - return DataResult.success(TomlTable(map.asTomlTable().plus(mapOf(key.content to value)))) - } - return DataResult.success(TomlTable(mapOf(key.content to value))) - } - - override fun getMapValues(input: TomlElement): DataResult>> - { - if (input !is TomlTable) return DataResult.error { "Not a toml table: $input" } - return DataResult.success(input.entries.stream().map { (key, value) -> Pair(createString(key), value) }) - } - - override fun getMapEntries(input: TomlElement): DataResult>> - { - if (input !is TomlTable) return DataResult.error { "Not a toml table: $input" } - return DataResult.success(Consumer { c -> - input.entries.forEach { entry -> c.accept(createString(entry.key), entry.value) } - }) - } - - override fun getMap(input: TomlElement): DataResult> - { - if (input !is TomlTable) return DataResult.error { "Not a toml table: $input" } - return DataResult.success(object : MapLike - { - override fun get(key: TomlElement): TomlElement? - { - return input[key] - } - - override fun get(key: String): TomlElement? - { - return input[key] - } - - override fun entries(): Stream> - { - return input.entries.stream().map { (key, value) -> Pair(createString(key), value) } - } - }) - } - - override fun createMap(map: Stream>): TomlElement - { - return TomlTable(map.toList().associate { it.first.asTomlLiteral().toString() to it.second }) - } - - override fun getStream(input: TomlElement): DataResult> - { - return if (input is TomlArray) - DataResult.success(input.stream()) - else - DataResult.error { "Not a toml array: $input" } - } - - override fun getList(input: TomlElement): DataResult>> - { - return if (input is TomlArray) - DataResult.success(Consumer { c -> - input.forEach { - c.accept(it) - } - }) - else - DataResult.error { "Not a toml array: $input" } - } - - override fun createList(input: Stream): TomlElement - { - return TomlArray(input.toList()) - } - - override fun remove(input: TomlElement, key: String): TomlElement - { - if (input is TomlTable) - { - return TomlTable(input.minus(key)) - } - return input - } - } - - /** [DynamicOps] over knbt's [NbtTag]. */ - object Nbt : DynamicOps - { - override fun empty(): NbtTag? = null - - override fun convertTo(outOps: DynamicOps, input: NbtTag?): U - { - return when (input) - { - null -> outOps.empty() - is NbtByte -> outOps.createByte(input.value) - is NbtShort -> outOps.createShort(input.value) - is NbtInt -> outOps.createInt(input.value) - is NbtLong -> outOps.createLong(input.value) - is NbtFloat -> outOps.createFloat(input.value) - is NbtDouble -> outOps.createDouble(input.value) - is NbtByteArray -> outOps.createByteList(ByteBuffer.wrap(input.toByteArray())) - is NbtString -> outOps.createString(input.value) - is NbtList<*> -> convertList(outOps, input) - is NbtCompound -> convertMap(outOps, input) - is NbtIntArray -> outOps.createIntList(Arrays.stream(input.toIntArray())) - is NbtLongArray -> outOps.createLongList(Arrays.stream(input.toLongArray())) - } - } - - override fun getNumberValue(input: NbtTag): DataResult - { - return input.toMinecraft.takeIf { it is NumericTag }?.let { DataResult.success((it as NumericTag).asNumber) } ?: DataResult.error { "Not a number" } - } - - override fun createNumeric(i: Number): NbtTag - { - return NbtDouble(i.toDouble()) - } - - override fun createByte(value: Byte): NbtTag - { - return NbtByte(value) - } - - override fun createShort(value: Short): NbtTag - { - return NbtShort(value) - } - - override fun createInt(value: Int): NbtTag - { - return NbtInt(value) - } - - override fun createLong(value: Long): NbtTag - { - return NbtLong(value) - } - - override fun createFloat(value: Float): NbtTag - { - return NbtFloat(value) - } - - override fun createDouble(value: Double): NbtTag - { - return NbtDouble(value) - } - - override fun createBoolean(value: Boolean): NbtTag - { - return NbtByte(value) - } - - override fun getStringValue(input: NbtTag): DataResult - { - return input.takeIf { it is NbtString }?.nbtString?.let { DataResult.success(it.value) } ?: DataResult.error { "Not a string: $input" } - } - - override fun createString(value: String): NbtTag - { - return NbtString(value) - } - - private operator fun NbtList.Companion.invoke(content: List): NbtList<*> = ListTag().apply { addAll(content.map { it.toMinecraft })}.fromMinecraft!! - - override fun mergeToList(list: NbtTag?, value: NbtTag): DataResult - { - if (list !is NbtList<*> && list != empty()) - return DataResult.error({ "mergeToList called with not a list: $list" }, list) - if (list != empty()) - { - return DataResult.success(NbtList(list!!.nbtList + value)) - } - return DataResult.success(NbtList(listOf(value))) - } - - override fun mergeToMap(map: NbtTag?, key: NbtTag, value: NbtTag): DataResult - { - if (map !is NbtCompound && map != empty()) - return DataResult.error({ "mergeToMap called with not a map: $map" }, map) - if (key !is NbtString) - return DataResult.error({ "key is not a string: $key" }, map) - if (map != empty()) - { - return DataResult.success(NbtCompound(map!!.nbtCompound + mapOf(key.value to value))) - } - return DataResult.success(NbtCompound(mapOf(key.value to value))) - } - - override fun getMapValues(input: NbtTag): DataResult>> - { - if (input !is NbtCompound) return DataResult.error { "Not an nbt compound: $input" } - return DataResult.success(input.entries.stream().map { (key, value) -> Pair(createString(key), value) }) - } - - override fun getMapEntries(input: NbtTag): DataResult>> - { - if (input !is NbtCompound) return DataResult.error { "Not an nbt compound: $input" } - return DataResult.success(Consumer { c -> - input.entries.forEach { (key, value) -> c.accept(createString(key), value) } - }) - } - - override fun getMap(input: NbtTag): DataResult> - { - if (input !is NbtCompound) return DataResult.error { "Not an nbt compound: $input" } - return DataResult.success(object : MapLike - { - override fun get(key: NbtTag): NbtTag? - { - return input[key.nbtString.value] - } - - override fun get(key: String): NbtTag? - { - return input[key] - } - - override fun entries(): Stream> - { - return input.entries.stream().map { (key, value) -> Pair(createString(key), value) } - } - }) - } - - override fun createMap(map: Stream>): NbtTag - { - return NbtCompound(map.toList().associate { it.first.nbtString.value to it.second }) - } - - override fun getStream(input: NbtTag): DataResult> - { - return if (input is NbtList<*>) - DataResult.success(input.stream()) - else - DataResult.error { "Not an nbt list: $input" } - } - - override fun getList(input: NbtTag): DataResult>> - { - return if (input is NbtList<*>) - DataResult.success(Consumer { c -> - input.forEach { - c.accept(it) - } - }) - else - DataResult.error { "Not an nbt list: $input" } - } - - override fun createList(input: Stream): NbtTag - { - return NbtList(input.toList()) - } - - override fun remove(input: NbtTag, key: String): NbtTag - { - if (input is NbtCompound) - { - return NbtCompound(input - key) - } - return input - } - - override fun getByteBuffer(input: NbtTag): DataResult - { - if (input is NbtByteArray) - { - return DataResult.success(ByteBuffer.wrap(input.toByteArray())) - } - return super.getByteBuffer(input) - } - - override fun createByteList(input: ByteBuffer): NbtTag - { - val byteBuffer: ByteBuffer = input.duplicate().clear() - val bs = ByteArray(input.capacity()) - byteBuffer[0, bs, 0, bs.size] - return NbtByteArray(bs) - } - - override fun getIntStream(input: NbtTag): DataResult - { - if (input is NbtIntArray) - { - return DataResult.success(Arrays.stream(input.toIntArray())) - } - return super.getIntStream(input) - } - - override fun createIntList(input: IntStream): NbtTag - { - return NbtIntArray(input.toArray()) - } - - override fun getLongStream(input: NbtTag): DataResult - { - if (input is NbtLongArray) - { - return DataResult.success(Arrays.stream(input.toLongArray())) - } - return super.getLongStream(input) - } - - override fun createLongList(input: LongStream): NbtTag - { - return NbtLongArray(input.toArray()) - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBT.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBT.kt deleted file mode 100644 index 9cbff4cec..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBT.kt +++ /dev/null @@ -1,270 +0,0 @@ -package net.kernelpanicsoft.archie.serialization - -import net.kernelpanicsoft.archie.util.toMutableEntry -import kotlinx.serialization.DeserializationStrategy -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.InternalSerializationApi -import kotlinx.serialization.SerializationStrategy -import kotlinx.serialization.descriptors.StructureKind -import kotlinx.serialization.internal.AbstractPolymorphicSerializer -import kotlinx.serialization.serializer -import net.benwoodworth.knbt.* -import net.minecraft.core.component.DataComponentPatch -import net.minecraft.nbt.* -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.experimental.ExperimentalTypeInference - -/** A [Nbt] (knbt) instance pre-configured for Minecraft's Java-edition NBT format, uncompressed. */ -val NBT = Nbt { - variant = NbtVariant.Java - compression = NbtCompression.None -} - -/** - * Like [Nbt.encodeToNbtTag], but for class/polymorphic types unwraps the single top-level - * compound entry keyed by the serial name, returning its value directly instead of a - * one-entry [NbtCompound] wrapper. - */ -@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) -fun Nbt.encodeToNbtTagRootless(serializer: SerializationStrategy, value: T): NbtTag -{ - return if (serializer.descriptor.kind == StructureKind.CLASS || - serializer is AbstractPolymorphicSerializer - ) - encodeToNbtTag(serializer, value).nbtCompound[serializer.descriptor.serialName]!! - else - encodeToNbtTag(serializer, value) -} - -/** - * The inverse of [encodeToNbtTagRootless]: decodes [tag] as [T], re-wrapping it in a one-entry - * compound keyed by the serial name first if [T] is a class/polymorphic type. - */ -@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) -fun Nbt.decodeFromNbtTagRootless(deserializer: DeserializationStrategy, tag: NbtTag): T -{ - return if (deserializer.descriptor.kind == StructureKind.CLASS || - deserializer is AbstractPolymorphicSerializer - ) - decodeFromNbtTag(deserializer, buildNbtCompound { - put(deserializer.descriptor.serialName, tag) - }) - else - decodeFromNbtTag(deserializer, tag) -} - -/** Reified variant of [encodeToNbtTagRootless] that resolves [T]'s serializer automatically. */ -@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) -inline fun Nbt.encodeToNbtTagRootless(value: T): NbtTag = - encodeToNbtTagRootless(serializersModule.serializer(), value) - -/** Reified variant of [decodeFromNbtTagRootless] that resolves [T]'s serializer automatically. */ -@OptIn(ExperimentalSerializationApi::class, InternalSerializationApi::class) -inline fun Nbt.decodeFromNbtTagRootless(tag: NbtTag): T = - decodeFromNbtTagRootless(serializersModule.serializer(), tag) - - -/** Builds a Minecraft [ListTag] using knbt's [NbtListBuilder] DSL via [builderAction]. */ -@OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class) -inline fun buildListTag( - @BuilderInference builderAction: NbtListBuilder.() -> Unit, -): ListTag -{ - contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } - return buildNbtList(builderAction).toMinecraft -} - -/** Builds a Minecraft [CompoundTag] using knbt's [NbtCompoundBuilder] DSL via [builderAction]. */ -@OptIn(ExperimentalContracts::class) -inline fun buildCompoundTag(builderAction: NbtCompoundBuilder.() -> Unit): CompoundTag -{ - contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } - return buildNbtCompound(builderAction).toMinecraft -} - -/** Builds entries via knbt's [NbtCompoundBuilder] DSL and puts each of them into the existing [compoundTag]. */ -@OptIn(ExperimentalContracts::class) -inline fun mergeToCompoundTag(compoundTag: CompoundTag, builderAction: NbtCompoundBuilder.() -> Unit) -{ - contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } - buildNbtCompound(builderAction).forEach { (key, value) -> - compoundTag.put(key, value.toMinecraft) - } -} - -/** Runs [action] for each element of [listTag], converted to a knbt [NbtTag]. No-op for an empty/untyped list. */ -@OptIn(ExperimentalContracts::class) -inline fun forEachTag(listTag: ListTag, action: (NbtTag) -> Unit) -{ - contract { callsInPlace(action, InvocationKind.UNKNOWN) } - listTag.fromMinecraft?.forEach { value -> - action(value) - } -} - -/** Runs [action] for each key/value entry of [compoundTag], with the value converted to a knbt [NbtTag]. */ -@OptIn(ExperimentalContracts::class) -inline fun forEachTag(compoundTag: CompoundTag, action: (Map.Entry) -> Unit) -{ - contract { callsInPlace(action, InvocationKind.UNKNOWN) } - compoundTag.fromMinecraft.forEach { (key, value) -> - action((key to value).toMutableEntry()) - } -} - -/** Builds a [DataComponentPatch] using Minecraft's [DataComponentPatch.Builder] DSL via [builderAction]. */ -@OptIn(ExperimentalContracts::class) -inline fun buildComponentPatch(builderAction: DataComponentPatch.Builder.() -> Unit): DataComponentPatch -{ - contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } - return DataComponentPatch.builder().apply(builderAction).build() -} - -/** Converts a knbt tag to the equivalent Minecraft [Tag] (a `null` receiver becomes [EndTag]). */ -val NbtTag?.toMinecraft: Tag - get() = when (this) - { - null -> EndTag.INSTANCE - is NbtByte -> ByteTag.valueOf(value) - is NbtByteArray -> ByteArrayTag(this) - is NbtCompound -> toMinecraft - is NbtDouble -> DoubleTag.valueOf(value) - is NbtFloat -> FloatTag.valueOf(value) - is NbtInt -> IntTag.valueOf(value) - is NbtIntArray -> IntArrayTag(this) - is NbtList<*> -> toMinecraft - is NbtLong -> LongTag.valueOf(value) - is NbtLongArray -> LongArrayTag(this) - is NbtShort -> ShortTag.valueOf(value) - is NbtString -> StringTag.valueOf(value) - } - -/** Converts a knbt [NbtCompound] to the equivalent Minecraft [CompoundTag]. */ -val NbtCompound.toMinecraft: CompoundTag - get() = CompoundTag().apply { - mapValues { it.value.toMinecraft }.forEach { (key, value) -> - put(key, value) - } - } - -/** Converts a knbt [NbtList] to the equivalent Minecraft [ListTag]. */ -val NbtList<*>.toMinecraft: ListTag - get() = ListTag().apply { - this@toMinecraft.map { - it.toMinecraft - }.forEach { - add(it) - } - } - -/** Converts a Minecraft [Tag] to the equivalent knbt tag, or `null` for an [EndTag]. */ -val Tag.fromMinecraft: NbtTag? - get() = when (id.toInt()) - { - 0 -> null - 1 -> NbtByte((this as NumericTag).asByte) - 2 -> NbtShort((this as NumericTag).asShort) - 3 -> NbtInt((this as NumericTag).asInt) - 4 -> NbtLong((this as NumericTag).asLong) - 5 -> NbtFloat((this as NumericTag).asFloat) - 6 -> NbtDouble((this as NumericTag).asDouble) - 7 -> NbtByteArray((this as ByteArrayTag).asByteArray) - 8 -> NbtString(this.asString) - 9 -> (this as ListTag).fromMinecraft - 10 -> (this as CompoundTag).fromMinecraft - 11 -> NbtIntArray((this as IntArrayTag).asIntArray) - 12 -> NbtLongArray((this as LongArrayTag).asLongArray) - else -> throw IllegalStateException("Unknown tag type: $this") - } - -/** Converts a Minecraft [CompoundTag] to the equivalent knbt [NbtCompound]. */ -val CompoundTag.fromMinecraft: NbtCompound - get() = buildNbtCompound { - this@fromMinecraft.allKeys.associateWith { - this@fromMinecraft[it]?.fromMinecraft - }.forEach { (key, value) -> - if (value != null) - put(key, value) - } - } -/** Converts a Minecraft [ListTag] to the equivalent knbt [NbtList], or `null` for an untyped (empty) list. */ -val ListTag.fromMinecraft: NbtList<*>? - get() = when (elementType.toInt()) - { - 0 -> null - 1 -> buildNbtList { - forEach { - add(it.fromMinecraft as NbtByte) - } - } - - 2 -> buildNbtList { - forEach { - add(it.fromMinecraft as NbtShort) - } - } - - 3 -> buildNbtList { - forEach { - add(it.fromMinecraft as NbtInt) - } - } - - 4 -> buildNbtList { - forEach { - add(it.fromMinecraft as NbtLong) - } - } - - 5 -> buildNbtList { - forEach { - add(it.fromMinecraft as NbtFloat) - } - } - - 6 -> buildNbtList { - forEach { - add(it.fromMinecraft as NbtDouble) - } - } - - 7 -> buildNbtList { - forEach { - add(it.fromMinecraft as NbtByteArray) - } - } - - 8 -> buildNbtList { - forEach { - add(it.fromMinecraft as NbtString) - } - } - - 9 -> buildNbtList> { - forEach { - add(it.fromMinecraft as NbtList<*>) - } - } - - 10 -> buildNbtList { - forEach { - add(it.fromMinecraft as NbtCompound) - } - } - - 11 -> buildNbtList { - forEach { - add(it.fromMinecraft as NbtIntArray) - } - } - - 12 -> buildNbtList { - forEach { - add(it.fromMinecraft as NbtLongArray) - } - } - - else -> throw IllegalStateException("Unknown tag type: $this") - } \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt deleted file mode 100644 index 586e6f3b5..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt +++ /dev/null @@ -1,119 +0,0 @@ -package net.kernelpanicsoft.archie.serialization - -import net.kernelpanicsoft.archie.transfer.ArchieEnergyStorage -import net.kernelpanicsoft.archie.transfer.ArchieFluidStorage -import net.kernelpanicsoft.archie.transfer.ArchieItemStorage -import dev.architectury.fluid.FluidStack -import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.MapSerializer -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.serializer -import net.minecraft.nbt.CompoundTag -import net.minecraft.world.item.ItemStack -import kotlin.collections.listOf -import kotlin.properties.PropertyDelegateProvider -import kotlin.properties.ReadOnlyProperty -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.full.memberProperties -import kotlin.reflect.jvm.isAccessible - -/** - * An interface for managing NBT-backed fields on block entities, item stacks, or fluid stacks. - * - * [NBTHolder] provides a property-delegation API that serializes field values to/from a - * [CompoundTag] using kotlinx.serialization. Each delegated field is keyed by its Kotlin - * property name. - * - * ### Usage on a block entity - * ```kotlin - * class MyBlockEntity(pos, state) : NBTBlockEntity(pos, state) { - * var count by nbt.intField() - * var label by nbt.stringField { "default" } - * val items by nbt.itemField(9) // 9-slot inventory - * val tank by nbt.fluidField(FluidStack.bucketAmount() * 4) // 1 tank slot, 4 buckets - * val energy by nbt.energyField(10_000) // a single energy buffer - * } - * ``` - * - * Obtain instances via [NBTHolder.create], [NBTHolder.item], or [NBTHolder.fluid]. - */ -@Suppress("unused") -interface NBTHolder -{ - /** - * Declares a read-write field backed by [serializer], keyed by the delegated property's name. - * [default] supplies the value used before the field has been loaded/set. - */ - fun field(serializer: KSerializer, default: () -> T): PropertyDelegateProvider> - - /** Declares a mutable-list field backed by [serializer], keyed by the delegated property's name. */ - fun listField(serializer: KSerializer, default: () -> List): PropertyDelegateProvider>> - /** Declares a mutable-map (keyed by [String]) field backed by [serializer], keyed by the delegated property's name. */ - fun mapField(serializer: KSerializer, default: () -> Map): PropertyDelegateProvider>> - - /** Declares an [ArchieItemStorage] field with [size] slots, keyed by the delegated property's name. */ - fun itemField(size: Int): PropertyDelegateProvider> - - /** Declares an [ArchieFluidStorage] field with [size] tank slots each capped at [limit], keyed by the delegated property's name. */ - fun fluidField(limit: Long, size: Int = 1): PropertyDelegateProvider> - - /** Declares an [ArchieEnergyStorage] field capped at [capacity], keyed by the delegated property's name. */ - fun energyField(capacity: Long): PropertyDelegateProvider> - - fun booleanField(default: () -> Boolean = { false }): PropertyDelegateProvider> = field(Boolean.serializer(), default) - fun byteField(default: () -> Byte = { 0 }): PropertyDelegateProvider> = field(Byte.serializer(), default) - fun ubyteField(default: () -> UByte = { 0u }): PropertyDelegateProvider> = field(UByte.serializer(), default) - fun shortField(default: () -> Short = { 0 }): PropertyDelegateProvider> = field(Short.serializer(), default) - fun ushortField(default: () -> UShort = { 0u }): PropertyDelegateProvider> = field(UShort.serializer(), default) - fun intField(default: () -> Int = { 0 }): PropertyDelegateProvider> = field(Int.serializer(), default) - fun uintField(default: () -> UInt = { 0u }): PropertyDelegateProvider> = field(UInt.serializer(), default) - fun longField(default: () -> Long = { 0 }): PropertyDelegateProvider> = field(Long.serializer(), default) - fun ulongField(default: () -> ULong = { 0u }): PropertyDelegateProvider> = field(ULong.serializer(), default) - fun floatField(default: () -> Float = { 0.0f }): PropertyDelegateProvider> = field(Float.serializer(), default) - fun doubleField(default: () -> Double = { 0.0 }): PropertyDelegateProvider> = field(Double.serializer(), default) - fun stringField(default: () -> String = { "" }): PropertyDelegateProvider> = field(String.serializer(), default) - - /** Loads every declared field's value from [compoundTag], overwriting current values. */ - fun loadFromTag(compoundTag: CompoundTag) - - /** Writes every declared field's current value into [compoundTag]. */ - fun saveToTag(compoundTag: CompoundTag) - - /** Builds a [CompoundTag] suitable for sending to the client to sync current field values. */ - fun getSyncTag(): CompoundTag - - /** Updates a single field, identified by [propertyName], from a client sync payload. */ - fun updateProperty(propertyName: String, serializer: KSerializer, value: T) - - companion object - { - /** Creates a standalone [NBTHolder] not backed by any particular [ItemStack]/[FluidStack]. */ - fun create(): NBTHolder = NBTHolderImpl() - - /** Creates an [NBTHolder] whose fields are persisted to [stack]'s NBT. */ - fun item(stack: ItemStack): NBTHolder = ItemStackNBTHolderImpl(stack) - - /** Creates an [NBTHolder] for [stack] and immediately runs [block] against it. */ - fun item(stack: ItemStack, block: NBTHolder.() -> R): R - { - return item(stack).block() - } - - /** Creates an [NBTHolder] whose fields are persisted to [stack]'s NBT. */ - fun fluid(stack: FluidStack): NBTHolder = FluidStackNBTHolderImpl(stack) - - /** Creates an [NBTHolder] for [stack] and immediately runs [block] against it. */ - fun fluid(stack: FluidStack, block: NBTHolder.() -> R): R - { - return fluid(stack).block() - } - } -} - -/** Reified variant of [NBTHolder.field] that resolves the [KSerializer] for [T] automatically. */ -inline fun NBTHolder.field(noinline default: () -> T): PropertyDelegateProvider> = field(serializer(), default) -/** Reified variant of [NBTHolder.listField] that resolves the [KSerializer] for [T] automatically. */ -inline fun NBTHolder.listField(noinline default: () -> List): PropertyDelegateProvider>> = listField(serializer(), default) -/** Reified variant of [NBTHolder.mapField] that resolves the [KSerializer] for [T] automatically. */ -inline fun NBTHolder.mapField(noinline default: () -> Map): PropertyDelegateProvider>> = mapField(serializer(), default) \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt deleted file mode 100644 index 5c080277c..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt +++ /dev/null @@ -1,304 +0,0 @@ -package net.kernelpanicsoft.archie.serialization - -import net.kernelpanicsoft.archie.config.toSnakeCase -import net.kernelpanicsoft.archie.transfer.ArchieEnergyStorage -import net.kernelpanicsoft.archie.transfer.ArchieFluidStorage -import net.kernelpanicsoft.archie.transfer.ArchieItemStorage -import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.MapSerializer -import kotlinx.serialization.builtins.serializer -import net.benwoodworth.knbt.NbtTag -import net.kernelpanicsoft.archie.gui.blockentity.getStateContainer -import net.minecraft.nbt.CompoundTag -import net.minecraft.world.level.block.entity.BlockEntity -import kotlin.properties.PropertyDelegateProvider -import kotlin.properties.ReadOnlyProperty -import kotlin.properties.ReadWriteProperty -import kotlin.reflect.KProperty -import kotlin.reflect.full.hasAnnotation - -/** - * Default [NBTHolder] implementation backing [NBTHolder.create]. Field values are cached - * in-memory as knbt tags keyed by the delegated property's snake_case name; properties - * annotated [Sync] additionally push updates through [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager]-backed state - * containers when the holder is attached to a [BlockEntity]. - */ -class NBTHolderImpl : NBTHolder -{ - private val data: MutableMap = mutableMapOf() - private val itemStorage: MutableMap = mutableMapOf() - private val fluidStorage: MutableMap = mutableMapOf() - private val energyStorage: MutableMap = mutableMapOf() - private val sync: MutableSet = mutableSetOf() - - override fun field( - serializer: KSerializer, - default: () -> T - ): PropertyDelegateProvider> - { - - return PropertyDelegateProvider { thisRef, property -> - if (property.hasAnnotation()) - { - sync += property.name.toSnakeCase() - if (thisRef is BlockEntity) - { - thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), serializer) - } - } - - val delegate = object : ReadWriteProperty - { - override fun getValue(thisRef: Any?, property: KProperty<*>): T - { - return runCatching { - NBT.decodeFromNbtTagRootless(serializer, data.getOrPut(property.name.toSnakeCase()) { - NBT.encodeToNbtTagRootless(serializer, default()) - }) - }.recover { - val ret = default() - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(serializer, ret) - ret - }.getOrThrow() - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: T) - { - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(serializer, value) - if (thisRef is BlockEntity) - { - if (property.name.toSnakeCase() in sync) - thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), value) - thisRef.setChanged() - } - } - } - if (property.name.toSnakeCase() !in data) - delegate.setValue(thisRef, property, default()) - delegate - } - } - - override fun listField( - serializer: KSerializer, - default: () -> List - ): PropertyDelegateProvider>> - { - return PropertyDelegateProvider { thisRef, property -> - if (property.hasAnnotation()) - { - sync += property.name.toSnakeCase() - if (thisRef is BlockEntity) - { - thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), serializer) - } - } - val delegate = object : ReadWriteProperty> - { - override fun getValue(thisRef: Any?, property: KProperty<*>): MutableList - { - return ObservableList(runCatching { - NBT.decodeFromNbtTagRootless(ListSerializer(serializer), data.getOrPut(property.name.toSnakeCase()) { - NBT.encodeToNbtTagRootless(ListSerializer(serializer), default()) - }) - }.recover { - val ret = default() - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(ListSerializer(serializer), ret) - ret - }.getOrThrow().toMutableList()) { list -> setValue(thisRef, property, list) } - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: MutableList) - { - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(ListSerializer(serializer), value) - if (thisRef is BlockEntity) - { - if (property.name.toSnakeCase() in sync) - thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), value) - thisRef.setChanged() - } - } - } - if (property.name.toSnakeCase() !in data) - delegate.setValue(thisRef, property, default().toMutableList()) - delegate - } - } - - override fun mapField( - serializer: KSerializer, - default: () -> Map - ): PropertyDelegateProvider>> - { - return PropertyDelegateProvider { thisRef, property -> - if (property.hasAnnotation()) - { - sync += property.name.toSnakeCase() - if (thisRef is BlockEntity) - { - thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), serializer) - } - } - val delegate = object : ReadWriteProperty> - { - override fun getValue(thisRef: Any?, property: KProperty<*>): MutableMap - { - return ObservableMap(runCatching { - NBT.decodeFromNbtTagRootless(MapSerializer(String.serializer(), serializer), data.getOrPut(property.name.toSnakeCase()) { - NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), default()) - }) - }.recover { - val ret = default() - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), ret) - ret - }.getOrThrow().toMutableMap()) { map -> setValue(thisRef, property, map) } - } - - override fun setValue(thisRef: Any?, property: KProperty<*>, value: MutableMap) - { - data[property.name.toSnakeCase()] = NBT.encodeToNbtTagRootless(MapSerializer(String.serializer(), serializer), value) - if (thisRef is BlockEntity) - { - if (property.name.toSnakeCase() in sync) - thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), value) - thisRef.setChanged() - } - } - } - if (property.name.toSnakeCase() !in data) - delegate.setValue(thisRef, property, default().toMutableMap()) - delegate - } - } - - override fun itemField(size: Int): PropertyDelegateProvider> - { - return PropertyDelegateProvider { thisRef, property -> - if (property.hasAnnotation()) - { - sync += property.name.toSnakeCase() - if (thisRef is BlockEntity) - { - thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), ArchieItemStorage.serializer()) - } - } - val onUpdate = when (thisRef) - { - is BlockEntity -> ({ - if (property.name.toSnakeCase() in sync) - thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), itemStorage[property.name.toSnakeCase()]) - thisRef.setChanged() - }) - else -> ({}) - } - itemStorage[property.name.toSnakeCase()] = ArchieItemStorage(size, onUpdate) - ReadOnlyProperty { _, _ -> itemStorage[property.name.toSnakeCase()]!! } - } - } - - override fun fluidField(limit: Long, size: Int): PropertyDelegateProvider> - { - return PropertyDelegateProvider { thisRef, property -> - if (property.hasAnnotation()) - { - sync += property.name.toSnakeCase() - if (thisRef is BlockEntity) - { - thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), ArchieFluidStorage.serializer()) - } - } - val onUpdate = when (thisRef) - { - is BlockEntity -> ({ - if (property.name.toSnakeCase() in sync) - thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), fluidStorage[property.name.toSnakeCase()]) - thisRef.setChanged() - }) - else -> ({}) - } - fluidStorage[property.name.toSnakeCase()] = ArchieFluidStorage(limit, size, onUpdate) - ReadOnlyProperty { _, _ -> fluidStorage[property.name.toSnakeCase()]!! } - } - } - - override fun energyField(capacity: Long): PropertyDelegateProvider> - { - return PropertyDelegateProvider { thisRef, property -> - if (property.hasAnnotation()) - { - sync += property.name.toSnakeCase() - if (thisRef is BlockEntity) - { - thisRef.getStateContainer().setPropertySerializer(property.name.toSnakeCase(), ArchieEnergyStorage.serializer()) - } - } - val onUpdate = when (thisRef) - { - is BlockEntity -> ({ - if (property.name.toSnakeCase() in sync) - thisRef.getStateContainer().updateProperty(property.name.toSnakeCase(), energyStorage[property.name.toSnakeCase()]) - thisRef.setChanged() - }) - else -> ({}) - } - energyStorage[property.name.toSnakeCase()] = ArchieEnergyStorage(capacity, onUpdate) - ReadOnlyProperty { _, _ -> energyStorage[property.name.toSnakeCase()]!! } - } - } - - override fun loadFromTag(compoundTag: CompoundTag) - { - forEachTag(compoundTag) { (key, value) -> - data[key] = value - } - itemStorage.forEach { (key, value) -> - value.readSnapshot(data.getOrPut(key) { - value.createSnapshot() - }) - } - fluidStorage.forEach { (key, value) -> - value.readSnapshot(data.getOrPut(key) { - value.createSnapshot() - }) - } - energyStorage.forEach { (key, value) -> - value.readSnapshot(data.getOrPut(key) { - value.createSnapshot() - }) - } - } - - override fun saveToTag(compoundTag: CompoundTag) - { - mergeToCompoundTag(compoundTag) { - itemStorage.forEach { (key, value) -> - data[key] = value.createSnapshot() - } - fluidStorage.forEach { (key, value) -> - data[key] = value.createSnapshot() - } - energyStorage.forEach { (key, value) -> - data[key] = value.createSnapshot() - } - data.forEach { (key, value) -> - put(key, value) - } - } - } - - override fun getSyncTag(): CompoundTag - { - return buildCompoundTag { - data.filter { (key, _) -> key in sync } - .forEach { (key, value) -> - put(key, value) - } - } - } - - override fun updateProperty(propertyName: String, serializer: KSerializer, value: T) - { - this.data[propertyName] = NBT.encodeToNbtTagRootless(serializer, value) - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableList.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableList.kt deleted file mode 100644 index adc0bf7c4..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableList.kt +++ /dev/null @@ -1,98 +0,0 @@ -package net.kernelpanicsoft.archie.serialization - -import java.util.function.IntFunction -import java.util.function.Predicate -import java.util.function.UnaryOperator - -/** - * A [MutableList] wrapper that invokes [listener] with the underlying [list] after every - * mutating operation. Used by [listField]-style [NBTHolder] delegates to detect changes and - * persist/sync them. - */ -class ObservableList(private val list: MutableList, private val listener: (MutableList) -> Unit) : MutableList by list -{ - override fun add(element: T): Boolean - { - return list.add(element).also { listener(list) } - } - - override fun add(index: Int, element: T) - { - return list.add(index, element).also { listener(list) } - } - - override fun remove(element: T): Boolean - { - return list.remove(element).also { listener(list) } - } - - override fun removeAt(index: Int): T - { - return list.removeAt(index).also { listener(list) } - } - - override fun addAll(elements: Collection): Boolean - { - return list.addAll(elements).also { listener(list) } - } - - override fun removeAll(elements: Collection): Boolean - { - return list.removeAll(elements).also { listener(list) } - } - - override fun set(index: Int, element: T): T - { - return list.set(index, element).also { listener(list) } - } - - override fun clear() - { - list.clear().also { listener(list) } - } - - override fun addAll(index: Int, elements: Collection): Boolean - { - return list.addAll(index, elements).also { listener(list) } - } - - override fun removeIf(filter: Predicate): Boolean - { - return list.removeIf(filter).also { listener(list) } - } - - override fun retainAll(elements: Collection): Boolean - { - return list.retainAll(elements).also { listener(list) } - } - - override fun replaceAll(operator: UnaryOperator) - { - list.replaceAll(operator).also { listener(list) } - } - - override fun sort(c: Comparator?) - { - list.sortWith(c!!).also { listener(list) } - } - - override fun addFirst(e: T) - { - list.addFirst(e).also { listener(list) } - } - - override fun addLast(e: T) - { - list.addLast(e).also { listener(list) } - } - - override fun removeFirst(): T - { - return list.removeFirst().also { listener(list) } - } - - override fun removeLast(): T - { - return list.removeLast().also { listener(list) } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableMap.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableMap.kt deleted file mode 100644 index ce5e32100..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableMap.kt +++ /dev/null @@ -1,84 +0,0 @@ -package net.kernelpanicsoft.archie.serialization - -import java.util.function.BiFunction -import java.util.function.Function - -/** - * A [MutableMap] wrapper that invokes [listener] with the underlying [map] after every - * mutating operation. Used by [mapField]-style [NBTHolder] delegates to detect changes and - * persist/sync them. - */ -class ObservableMap(private val map: MutableMap, private val listener: (MutableMap) -> Unit) : MutableMap by map -{ - override fun put(key: K, value: V): V? - { - return map.put(key, value).also { listener(map) } - } - - override fun remove(key: K): V? - { - return map.remove(key).also { listener(map) } - } - - override fun clear() - { - map.clear().also { listener(map) } - } - - override fun putAll(from: Map) - { - map.putAll(from).also { listener(map) } - } - - override fun remove(key: K, value: V): Boolean - { - return map.remove(key, value).also { listener(map) } - } - - override fun replace(key: K, value: V): V? - { - return map.replace(key, value).also { listener(map) } - } - - override fun replace(key: K, oldValue: V, newValue: V): Boolean - { - return map.replace(key, oldValue, newValue).also { listener(map) } - } - - override fun replaceAll(function: BiFunction) - { - map.replaceAll(function).also { listener(map) } - } - - override fun computeIfAbsent(key: K, mappingFunction: Function): V - { - return map.computeIfAbsent(key, mappingFunction).also { listener(map) } - } - - override fun putIfAbsent(key: K, value: V): V? - { - return map.putIfAbsent(key, value).also { listener(map) } - } - - override fun computeIfPresent( - key: K, - remappingFunction: BiFunction - ): V? - { - return map.computeIfPresent(key, remappingFunction).also { listener(map) } - } - - override fun compute(key: K, remappingFunction: BiFunction): V? - { - return map.compute(key, remappingFunction).also { listener(map) } - } - - override fun merge( - key: K, - value: V & Any, - remappingFunction: BiFunction - ): V? - { - return map.merge(key, value, remappingFunction).also { listener(map) } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/SerializationManager.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/SerializationManager.kt deleted file mode 100644 index 20771408a..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/SerializationManager.kt +++ /dev/null @@ -1,391 +0,0 @@ -package net.kernelpanicsoft.archie.serialization - -import com.google.gson.JsonParser -import com.mojang.serialization.Codec -import com.mojang.serialization.DynamicOps -import com.mojang.serialization.JsonOps -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.cbor.Cbor -import kotlinx.serialization.cbor.CborDecoder -import kotlinx.serialization.cbor.CborEncoder -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.Json -import kotlinx.serialization.json.JsonDecoder -import kotlinx.serialization.json.JsonElement -import kotlinx.serialization.json.JsonEncoder -import kotlinx.serialization.modules.SerializersModule -import kotlinx.serialization.modules.SerializersModuleBuilder -import kotlinx.serialization.modules.overwriteWith -import net.benwoodworth.knbt.* -import net.kernelpanicsoft.archie.serialization.SerializationManager.SerializationManagerBuilder.DynamicOpRegistryBuilder -import net.kernelpanicsoft.archie.serialization.SerializationManager.SerializationManagerBuilder.SerializerRegistryBuilder -import net.kernelpanicsoft.archie.serialization.serializers.BuiltInSerializersModule -import net.kernelpanicsoft.archie.serialization.serializers.MinecraftSerializersModule -import net.minecraft.nbt.NbtOps -import net.minecraft.nbt.Tag -import net.minecraft.resources.DelegatingOps -import kotlin.reflect.KClass -import com.google.gson.JsonElement as GsonElement - -internal typealias EncodeDynamicOp = (input: Any, strategy: KSerializer) -> T -internal typealias DecodeDynamicOp = (input: T, strategy: KSerializer) -> Any - -internal typealias EncodeSerializer = (T, Codec, Any) -> Unit -internal typealias DecodeSerializer = (T, Codec) -> Any - -/** - * Manages serializers for KLib, allowing customization and extension. - * - * ### Adding Serializers - * KLib respects your serializer annotations, because of this you can use the `@Serializable()` annotation: - * ```kotlin - * data class MyData(@Serializable(MyClassSerializer::class) val field: MyClass) - * ``` - * Otherwise, you can mark it as a `@Contextual` serializer and specify it in the [SerializationManager]: - * ```kotlin - * data class MyData(@Contextual val field: MyClass) - * - * val myModule = SerializersModule { - * contextual(MyClass::class, MyClassSerializer) - * } - * - * SerializationManager { - * module { - * include(myModule) - * } - * } - * ``` - * - * ### Overwriting Existing Serializers - * To overwrite existing serializers: - * ```kotlin - * val myModule = SerializersModule { - * contextual(ResourceLocation::class, CustomResourceLocationSerializer) - * } - * - * SerializationManager overwriteWith myModule - * ``` - */ -@OptIn(ExperimentalSerializationApi::class) -object SerializationManager { - private var sharedModule: SerializersModule = SerializersModule { - include(MinecraftSerializersModule) - include(BuiltInSerializersModule) - } - - private fun createCbor() = Cbor { serializersModule = sharedModule } - private fun createJson() = Json { - ignoreUnknownKeys = true - explicitNulls = false - serializersModule = sharedModule - } - - private fun createNbt() = Nbt { - variant = NbtVariant.Java - compression = NbtCompression.None - serializersModule = sharedModule - } - - /** The shared [Cbor] instance, reconfigured with [sharedModule] whenever [overwriteWith] or [invoke] runs. */ - var cbor: Cbor = createCbor() - private set - /** The shared [Json] instance (unknown keys ignored, nulls omitted), kept in sync with [sharedModule]. */ - var json: Json = createJson() - private set - /** The shared Java-edition, uncompressed [Nbt] instance, kept in sync with [sharedModule]. */ - var nbt: Nbt = createNbt() - private set - - // ConcurrentHashMap/CopyOnWriteArrayList: mods can call registerDynamicOp/registerSerializer - // concurrently during parallel mod init, and lookups happen far more often than registrations. - private val ops: MutableMap, DynamicOpRegistryBuilder.Operation> = - java.util.concurrent.ConcurrentHashMap() - internal val serializers: MutableList = - java.util.concurrent.CopyOnWriteArrayList() - - private fun rebuild() { - cbor = createCbor() - json = createJson() - nbt = createNbt() - } - - /** - * Overwrites existing serializers with the provided module. - * - * @param module The new `SerializersModule` to use. - */ - infix fun overwriteWith(module: SerializersModule) { - sharedModule = sharedModule overwriteWith module - rebuild() - } - - /** - * Retrieves the ops registry for a specific [DynamicOps] instance. - * - * @param op The [DynamicOps] instance. - * @return A registry that can encode/decode the `DynamicOp`, or `null` if not found. - */ - operator fun get(op: DynamicOps<*>): DynamicOpRegistryBuilder.Operation? - { - if (op is DelegatingOps<*>) { - // Handle DelegatingOps by checking the delegate - return get(op.delegate) - } - return ops[op] - } - - /** - * Retrieves the serializer registry for a specific [Encoder]. - * - * @param encoder The [Encoder] interface. - * @return A registry that contains the [Encoder], or `null` if not found. - */ - operator fun get(encoder: Encoder) = serializers.find { it.operation.encoder.isInstance(encoder) } - - /** - * Retrieves the serializer registry for a specific [Decoder]. - * - * @param decoder The [Decoder] interface. - * @return A registry that contains the [Decoder], or `null` if not found. - */ - operator fun get(decoder: Decoder) = serializers.find { it.operation.decoder.isInstance(decoder) } - - /** - * Configures the [SerializationManager] - */ - operator fun invoke(block: SerializationManagerBuilder.() -> Unit) { - SerializationManagerBuilder().block() - rebuild() - } - - class SerializationManagerBuilder { - /** - * Uses the [SerializersModuleBuilder] to configure the shared module between all serializer instances. - * - * @param block A builder function for creating a [SerializersModule] - */ - fun module(block: SerializersModuleBuilder.() -> Unit) { - sharedModule = sharedModule.overwriteWith(SerializersModule { block() }) - } - - /** - * Registers a new [DynamicOps] instance with its associated encode and decode functions. - * This is used when you convert a [KSerializer] into a [Codec] via [SerializerCodec]. - * - * ### Usage - * ```kotlin - * registerDynamicOp(JsonOps.INSTANCE) { - * encode { input, strategy -> json.encodeToJsonElement(strategy, input).toGson } - * decode { input, strategy -> - * require(input is GsonElement) { "Expected input of type JsonElement but received ${input.javaClass.simpleName}." } - * - * json.decodeFromJsonElement(strategy, input.toKson) - * } - * } - * ``` - * - * @param op The `DynamicOps` instance. - * @param block The builder function of the `DynamicOpRegistry` - */ - @Suppress("UNCHECKED_CAST") - fun registerDynamicOp( - op: DynamicOps, - block: DynamicOpRegistryBuilder.() -> Unit - ) { - val builder = DynamicOpRegistryBuilder().apply(block) - - ops[op] = builder.build() as DynamicOpRegistryBuilder.Operation - } - - /** - * Registers a new `Serializer` with its associated encode and decode functions. - * This is used when you convert a [Codec] into [KSerializer]. - * - * **Note:** Because a limitation of the [Codec] structure you need an "intermediary" such as [JsonElement] or [NbtTag] for example. - * - * ### Usage - * ```kotlin - * registerSerializer(JsonElement.serializer().descriptor) { - * encode(JsonEncoder::class) { encoder, codec, input -> - * encoder.encodeJsonElement(codec.encodeStart(KOps.Json, input).orThrow) - * } - * - * decode(JsonDecoder::class) { decoder, codec -> - * codec.parse(KOps.Json, decoder.decodeJsonElement()).orThrow - * } - * } - * ``` - * - * @param descriptor The descriptor of the serializer - * @param name An optional name for the element in the [CodecSerializer] descriptor - * @param block The builder function of the `SerializerRegistry` - */ - fun registerSerializer( - descriptor: SerialDescriptor, - name: String? = null, - block: SerializerRegistryBuilder.() -> Unit - ) { - val builder = SerializerRegistryBuilder().apply(block) - - serializers.add( - SerializerRegistryBuilder.Registry( - descriptor, - name, - builder.build() - ) - ) - } - - class DynamicOpRegistryBuilder { - data class Operation( - val encode: EncodeDynamicOp, - val decode: DecodeDynamicOp - ) - - private var encode: EncodeDynamicOp? = null - private var decode: DecodeDynamicOp? = null - - /** - * Defines the encoder for this `DynamicOp` - * - * @param block The function used for encoding data - */ - fun encode(block: EncodeDynamicOp) { - encode = block - } - - /** - * Defines the decoder for this `DynamicOp` - * - * @param block The function used for decoding data - */ - fun decode(block: DecodeDynamicOp) { - decode = block - } - - internal fun build(): Operation { - requireNotNull(encode) { "Encode function must be provided before building the operation. Call `encode { ... }` to set it." } - requireNotNull(decode) { "Decode function must be provided before building the operation. Call `decode { ... }` to set it." } - - return Operation(encode!!, decode!!) - } - } - - class SerializerRegistryBuilder { - data class Registry( - val descriptor: SerialDescriptor, - val name: String? = null, - val operation: Operation - ) - - data class Operation( - val encoder: KClass, - val decoder: KClass, - val encode: EncodeSerializer, - val decode: DecodeSerializer - ) - - private var encoder: KClass? = null - private var encode: EncodeSerializer? = null - - private var decoder: KClass? = null - private var decode: DecodeSerializer? = null - - /** - * Defines the encoder for this `Serializer` - * - * @param block The function used for encoding data - */ - @Suppress("UNCHECKED_CAST") - fun encode(enc: KClass, block: EncodeSerializer) { - encoder = enc - encode = block as EncodeSerializer - } - - /** - * Defines the decoder for this `Serializer` - * - * @param block The function used for decoding data - */ - @Suppress("UNCHECKED_CAST") - fun decode(dec: KClass, block: DecodeSerializer) { - decoder = dec - decode = block as DecodeSerializer - } - - internal fun build(): Operation { - requireNotNull(encoder) { "Encoder must be provided before building the operation. Call `encode(...) { ... }` to set it." } - requireNotNull(encode) { "Encode function must be provided before building the operation. Call `encode(...) { ... }` to set it." } - requireNotNull(decoder) { "Decoder must be provided before building the operation. Call `decode(...) { ... }` to set it." } - requireNotNull(decode) { "Decode function must be provided before building the operation. Call `decode(...) { ... }` to set it." } - - return Operation( - encoder!!, - decoder!!, - encode!!, - decode!! - ) - } - } - } - - init { - SerializationManager { - registerDynamicOp(JsonOps.INSTANCE) { - encode { input, strategy -> json.encodeToJsonElement(strategy, input).toGson } - decode { input, strategy -> - require(input is GsonElement) { "Expected input of type JsonElement but received ${input.javaClass.simpleName}." } - - json.decodeFromJsonElement(strategy, input.toKson) - } - } - - registerDynamicOp(NbtOps.INSTANCE) { - encode { input, strategy -> nbt.encodeToNbtTag(strategy, input).toMinecraft } - decode { input, strategy -> - require(input is Tag) { "Expected input of type Tag but received ${input.javaClass.simpleName}." } - - nbt.decodeFromNbtTag( - strategy, - input.fromMinecraft ?: throw IllegalStateException("Failed to convert a Minecraft Tag into a KNbtTag.") - ) - } - } - - registerSerializer(JsonElement.serializer().descriptor, name = "JsonElement") { - encode(JsonEncoder::class) { encoder, codec, input -> - encoder.encodeJsonElement(codec.encodeStart(KOps.Json, input).orThrow) - } - - decode(JsonDecoder::class) { decoder, codec -> - codec.parse(KOps.Json, decoder.decodeJsonElement()).orThrow - } - } - - registerSerializer(NbtTag.serializer().descriptor, name = "NbtTag") { - encode(NbtEncoder::class) { encoder, codec, input -> - encoder.encodeNbtTag(codec.encodeStart(KOps.Nbt, input).orThrow) - } - - decode(NbtDecoder::class) { decoder, codec -> - codec.parse(KOps.Nbt, decoder.decodeNbtTag()).orThrow - } - } - - registerSerializer(String.serializer().descriptor, name = "CborElement") { - encode(CborEncoder::class) { encoder, codec, input -> - encoder.encodeString(codec.encodeStart(JsonOps.INSTANCE, input).orThrow.toString()) - } - - decode(CborDecoder::class) { decoder, codec -> - val str = decoder.decodeString() - codec.parse(JsonOps.INSTANCE, JsonParser.parseString(str)).orThrow - } - } - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Sync.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Sync.kt deleted file mode 100644 index df22bbe8e..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Sync.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.kernelpanicsoft.archie.serialization - -/** - * Marks an [NBTHolder]-delegated property as one that should be synced from server to client. - * - * Checked by [NBTHolderImpl] (via reflection) when a delegate is created: an annotated property - * is registered with the owning block entity's state container - * ([net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateContainer], obtained through - * [net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager]) so later writes push - * updates through that container instead of only being picked up via [NBTHolder.getSyncTag]. - */ -@Target(AnnotationTarget.PROPERTY) -annotation class Sync diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Utils.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Utils.kt deleted file mode 100644 index eaa29989f..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Utils.kt +++ /dev/null @@ -1,292 +0,0 @@ -@file:Suppress("FunctionName", "unused") -@file:OptIn(ExperimentalSerializationApi::class) - -package net.kernelpanicsoft.archie.serialization - -import com.google.gson.JsonParser -import com.mojang.datafixers.util.Pair -import com.mojang.serialization.Codec -import com.mojang.serialization.DataResult -import com.mojang.serialization.DynamicOps -import kotlinx.serialization.* -import kotlinx.serialization.builtins.ArraySerializer -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.MapSerializer -import kotlinx.serialization.builtins.SetSerializer -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.json.JsonElement -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import kotlin.reflect.KClass -import com.google.gson.JsonElement as GsonElement - -/** - * Gets data from a [RegistryFriendlyByteBuf] using the provided [KSerializer] - */ -fun RegistryFriendlyByteBuf.read(serializer: KSerializer): T = - SerializationManager.cbor.decodeFromByteArray(serializer, readByteArray()) - -/** - * Writes data into a [RegistryFriendlyByteBuf] using the [KSerializer] using the class of the data - */ -@OptIn(InternalSerializationApi::class) -fun RegistryFriendlyByteBuf.write(data: T) = write(data::class.serializer() as KSerializer, data) - -/** - * Writes data into a [RegistryFriendlyByteBuf] using a [KSerializer] - */ -fun RegistryFriendlyByteBuf.write(serializer: KSerializer, data: T) = - writeBytes(SerializationManager.cbor.encodeToByteArray(serializer, data)) - -/** - * Converts a [Codec] into a [KSerializer]. - * - * **Note:** By default `JsonOps` and `NbtOps` are supported. Check [SerializationManager.SerializationManagerBuilder.registerDynamicOp] to register another [DynamicOps]. - * Trying to use unregistered [DynamicOps] implementations will result in an [UnsupportedOperationException]. - */ -val Codec.kSerializer: KSerializer - get() = CodecSerializer(this) - -/** - * Converts a [KSerializer] into a [Codec]. - * - * **Note:** By default `JsonOps` and `NbtOps` are supported. Check [SerializationManager.SerializationManagerBuilder.registerDynamicOp] to register another [DynamicOps]. - * Trying to use unregistered [DynamicOps] implementations will result in an [UnsupportedOperationException]. - * - * ### Example - * ```kotlin - * @Serializable - * data class TestData( - * val str: String, - * val int: Int, - * val float: Float, - * val double: Double, - * val boolean: Boolean - * ) - * - * // Using the toCodec extension: - * val TestCodec = TestData.serializer().toCodec() - * - * // The above is equivalent to manually creating a codec: - * val ManualTestCodec: Codec = RecordCodecBuilder.create { - * it.group( - * Codec.STRING.fieldOf("str").forGetter(TestData::str), - * Codec.INT.fieldOf("int").forGetter(TestData::int), - * Codec.FLOAT.fieldOf("float").forGetter(TestData::float), - * Codec.DOUBLE.fieldOf("double").forGetter(TestData::double), - * Codec.BOOL.fieldOf("boolean").forGetter(TestData::boolean) - * ).apply(it, ::TestData) - * } - * ``` - * - * @return A [Codec] of type `T` defined by the [KSerializer]. - * @throws UnsupportedOperationException If the provided `DynamicOps` type is not supported. - */ -val KSerializer.codec: Codec - get() = SerializerCodec(this) - -/** - * Converts any [KSerializer] into a [StreamCodec] using Cbor - */ -val KSerializer.streamCodec: StreamCodec - get() = StreamCodec.of( - { buffer, value -> buffer.writeByteArray(SerializationManager.cbor.encodeToByteArray(this, value)) }, - { buffer -> SerializationManager.cbor.decodeFromByteArray(this, buffer.readByteArray()) } - ) - -/** - * Returns serial descriptor that delegates all the calls to descriptor returned by [deferred] block. - * Used to resolve cyclic dependencies between recursive serializable structures. - */ -@OptIn(SealedSerializationApi::class) -fun defer(deferred: () -> SerialDescriptor): SerialDescriptor = object : SerialDescriptor { - - private val original: SerialDescriptor by lazy(deferred) - - override val serialName: String - get() = original.serialName - override val kind: SerialKind - get() = original.kind - override val elementsCount: Int - get() = original.elementsCount - override val isInline: Boolean - get() = original.isInline - override val isNullable: Boolean - get() = original.isNullable - override val annotations: List - get() = original.annotations - - override fun getElementName(index: Int): String = original.getElementName(index) - override fun getElementIndex(name: String): Int = original.getElementIndex(name) - override fun getElementAnnotations(index: Int): List = original.getElementAnnotations(index) - override fun getElementDescriptor(index: Int): SerialDescriptor = original.getElementDescriptor(index) - override fun isElementOptional(index: Int): Boolean = original.isElementOptional(index) -} - -/** - * Used for [KSerializer.codec], you could extend this class to make any modifications you like. - * - * **Note:** It is HIGHLY recommended to just use the extension function [KSerializer.codec] instead of manually using this class. - */ -open class SerializerCodec(private val serializer: KSerializer) : Codec { - @Suppress("UNCHECKED_CAST") - override fun encode(input: T, ops: DynamicOps, prefix: V): DataResult - { - return tryOrThrow { - val cod = SerializationManager[ops] - ?: throw UnsupportedOperationException("${ops::class.simpleName} is not a supported DynamicOps instance.") - - cod.encode(input, serializer as KSerializer) as V - } - } - - @Suppress("UNCHECKED_CAST") - override fun decode( - ops: DynamicOps, - input: V - ): DataResult> { - return tryOrThrow { - val cod = SerializationManager[ops] - ?: throw UnsupportedOperationException("${ops::class.simpleName} is not a supported DynamicOps instance.") - - val value = cod.decode(input, serializer as KSerializer) as T - - Pair(value, input) - } - } -} - -internal fun tryOrThrow(action: () -> T): DataResult { - return try { - DataResult.success(action()) - } catch (err: Exception) { - DataResult.error(err::message) - } -} - -/** - * Used for [Codec.kSerializer], you could extend this class to make any modifications you like. - * - * **Note:** It is HIGHLY recommended to just use the extension function [Codec.kSerializer] instead of manually using this class. - */ -@Suppress("UNCHECKED_CAST") -open class CodecSerializer(private val codec: Codec) : KSerializer { - @OptIn(InternalSerializationApi::class, ExperimentalSerializationApi::class) - override val descriptor: SerialDescriptor = defer { - buildSerialDescriptor("CodecSerializer", PolymorphicKind.SEALED) { - SerializationManager.serializers.forEach { - element(it.name ?: it.descriptor.serialName, defer { it.descriptor }) - } - } - } - - override fun serialize(encoder: Encoder, value: T) { - val ser = SerializationManager[encoder] - ?: throw UnsupportedOperationException("${encoder::class.simpleName} is not a supported serializer type.") - - ser.operation.encode(encoder, codec as Codec, value as Any) - } - - override fun deserialize(decoder: Decoder): T { - val ser = SerializationManager[decoder] - ?: throw UnsupportedOperationException("${decoder::class.simpleName} is not a supported serializer type.") - - return ser.operation.decode(decoder, codec as Codec) as T - } -} - -/** - * Convert any [JsonElement] from kotlinx.serialization.json into [GsonElement] from gson - */ -val JsonElement.toGson: GsonElement - get() = JsonParser.parseString(this.toString()) - -/** - * Convert any [GsonElement] from gson into [JsonElement] kotlinx.serialization.json - */ -val GsonElement.toKson: JsonElement - get() = SerializationManager.json.parseToJsonElement(this.toString()) - -/** - * Returns serializer for reference [Array] of type [E] with [descriptor][SerialDescriptor] of [StructureKind.LIST] kind. - * Each element of the array is serialized with the given [elementSerializer]. - * - * [KSerializer.descriptor] is deferred to resolve cyclic dependencies - */ -@ExperimentalSerializationApi -inline fun DeferredArraySerializer(elementSerializer: KSerializer): KSerializer> = - DeferredArraySerializer(T::class, elementSerializer) - -/** - * Returns serializer for reference [Array] of type [E] with [descriptor][SerialDescriptor] of [StructureKind.LIST] kind. - * Each element of the array is serialized with the given [elementSerializer]. - * - * [KSerializer.descriptor] is deferred to resolve cyclic dependencies - */ -@ExperimentalSerializationApi -fun DeferredArraySerializer( - kClass: KClass, - elementSerializer: KSerializer -): KSerializer> = object : KSerializer> -{ - private val surrogate by lazy { ArraySerializer(kClass, elementSerializer) } - - override val descriptor: SerialDescriptor = defer { surrogate.descriptor } - - override fun deserialize(decoder: Decoder): Array = surrogate.deserialize(decoder) - - override fun serialize(encoder: Encoder, value: Array) = surrogate.serialize(encoder, value) -} - -/** - * Creates a serializer for [`List`][List] for the given serializer of type [T]. - * - * [KSerializer.descriptor] is deferred to resolve cyclic dependencies - */ -fun DeferredListSerializer(elementSerializer: KSerializer): KSerializer> = object : KSerializer> -{ - private val surrogate by lazy { ListSerializer(elementSerializer) } - - override val descriptor: SerialDescriptor = defer { surrogate.descriptor } - - override fun deserialize(decoder: Decoder): List = surrogate.deserialize(decoder) - - override fun serialize(encoder: Encoder, value: List) = surrogate.serialize(encoder, value) -} - -/** - * Creates a serializer for [`Set`][Set] for the given serializer of type [T]. - * - * [KSerializer.descriptor] is deferred to resolve cyclic dependencies - */ -fun DeferredSetSerializer(elementSerializer: KSerializer): KSerializer> = object : KSerializer> -{ - private val surrogate by lazy { SetSerializer(elementSerializer) } - - override val descriptor: SerialDescriptor = defer { surrogate.descriptor } - - override fun deserialize(decoder: Decoder): Set = surrogate.deserialize(decoder) - - override fun serialize(encoder: Encoder, value: Set) = surrogate.serialize(encoder, value) -} - -/** - * Creates a serializer for [`Map`][Map] for the given serializers for - * its ket type [K] and value type [V]. - * - * [KSerializer.descriptor] is deferred to resolve cyclic dependencies - */ -fun DeferredMapSerializer( - keySerializer: KSerializer, - valueSerializer: KSerializer -): KSerializer> = object : KSerializer> -{ - private val surrogate by lazy { MapSerializer(keySerializer, valueSerializer) } - override val descriptor: SerialDescriptor = defer { surrogate.descriptor } - - override fun deserialize(decoder: Decoder): Map = surrogate.deserialize(decoder) - - override fun serialize(encoder: Encoder, value: Map) = surrogate.serialize(encoder, value) -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/BuiltinSerializers.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/BuiltinSerializers.kt deleted file mode 100644 index 4aa240425..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/BuiltinSerializers.kt +++ /dev/null @@ -1,134 +0,0 @@ -package net.kernelpanicsoft.archie.serialization.serializers - -import com.mojang.blaze3d.platform.InputConstants.Type -import kotlinx.serialization.Contextual -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.* -import kotlinx.serialization.modules.SerializersModule -import me.shedaniel.clothconfig2.api.Modifier -import me.shedaniel.clothconfig2.api.ModifierKeyCode -import me.shedaniel.math.Color -import net.kernelpanicsoft.archie.util.onClient - -/* ------------------ TypeAliases ------------------ */ - -/** - * Contextual type-alias for Cloth Config's [ModifierKeyCode] that uses [ModifierKeyCodeSerializer] - * when the field is annotated with `@Contextual`. - */ -typealias SModifierKeyCode = @Contextual ModifierKeyCode -/** - * Contextual type-alias for Cloth Config's [Color] that uses [ColorSerializer] when the field - * is annotated with `@Contextual`. - */ -typealias SColor = @Contextual Color - -/* ------------------ Serializers ------------------ */ - -/** A [KSerializer] for Cloth Config's [ModifierKeyCode] (a keybind plus its held modifier). */ -object ModifierKeyCodeSerializer : KSerializer -{ - @Serializable - enum class KeyType(val type: Type) - { - KEYSYM(Type.KEYSYM), - SCANCODE(Type.SCANCODE), - MOUSE(Type.MOUSE); - - companion object - { - fun forType(type: Type): KeyType - { - return when (type) - { - Type.KEYSYM -> KEYSYM - Type.SCANCODE -> SCANCODE - Type.MOUSE -> MOUSE - } - } - } - } - - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ModifierKeyCode") - { - element("type") - element("key_code") - element("modifier", isOptional = true) - } - - override fun deserialize(decoder: Decoder): ModifierKeyCode - { - return decoder.decodeStructure(descriptor) - { - var type: KeyType = KeyType.KEYSYM - var keyCode = 0 - var modifier: Short = 0 - while (true) - { - when (val index = decodeElementIndex(descriptor)) - { - 0 -> type = decodeSerializableElement(descriptor, index, KeyType.serializer()) - 1 -> keyCode = decodeIntElement(descriptor, index) - 2 -> modifier = decodeShortElement(descriptor, index) - CompositeDecoder.DECODE_DONE -> break - else -> error("Unexpected index: $index") - } - } - if (keyCode == -1) - ModifierKeyCode.unknown() - else - ModifierKeyCode.of(type.type.getOrCreate(keyCode), Modifier.of(modifier)) - } - } - - override fun serialize(encoder: Encoder, value: ModifierKeyCode) - { - encoder.encodeStructure(descriptor) - { - encodeSerializableElement(descriptor, 0, KeyType.serializer(), KeyType.forType(value.type)) - encodeIntElement(descriptor, 1, value.keyCode.value) - if (!value.modifier.isEmpty) - encodeShortElement(descriptor, 2, value.modifier.value) - } - } - -} - -/** A [KSerializer] for Cloth Config's [Color] that encodes/decodes as an `#AARRGGBB` hex string. */ -object ColorSerializer : KSerializer -{ - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("Color", PrimitiveKind.STRING) - - override fun deserialize(decoder: Decoder): Color - { - return Color.ofTransparent( - decoder.decodeString() - .trimStart('#') - .toLong(16) - .toInt() - ) - } - - override fun serialize(encoder: Encoder, value: Color) - { - encoder.encodeString("#${Integer.toHexString(value.color).padStart(8, '0')}") - } - -} - -/** - * A [SerializersModule] that registers Archie's Cloth Config-related type serializers - * ([ModifierKeyCodeSerializer], [ColorSerializer]) as contextual serializers. - * - * [ModifierKeyCodeSerializer] is only registered on the physical client (via [onClient]), since - * [ModifierKeyCode] is a client-only Cloth Config type that a dedicated server shouldn't - * class-load; [ColorSerializer] is registered on both sides. - */ -val BuiltInSerializersModule = SerializersModule { - onClient { - contextual(ModifierKeyCode::class, ModifierKeyCodeSerializer) - } - contextual(Color::class, ColorSerializer) -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/MinecraftSerializers.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/MinecraftSerializers.kt deleted file mode 100644 index 32e56d02d..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/MinecraftSerializers.kt +++ /dev/null @@ -1,363 +0,0 @@ -package net.kernelpanicsoft.archie.serialization.serializers - -import io.netty.buffer.Unpooled -import kotlinx.serialization.Contextual -import kotlinx.serialization.KSerializer -import kotlinx.serialization.SerializationException -import kotlinx.serialization.builtins.ByteArraySerializer -import kotlinx.serialization.descriptors.* -import kotlinx.serialization.encoding.* -import kotlinx.serialization.modules.SerializersModule -import net.kernelpanicsoft.archie.serialization.CodecSerializer -import net.minecraft.core.* -import net.minecraft.core.registries.Registries -import net.minecraft.network.FriendlyByteBuf -import net.minecraft.resources.ResourceKey -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.item.ItemStack -import net.minecraft.world.level.ChunkPos -import net.minecraft.world.level.Level -import net.minecraft.world.phys.BlockHitResult -import net.minecraft.world.phys.HitResult -import net.minecraft.world.phys.Vec3 - -/* ─────────────────────── Type aliases ─────────────────────── */ - -/** - * Contextual type-alias for [FriendlyByteBuf] that uses [FriendlyByteBufSerializer] - * when the field is annotated with `@Contextual`. - */ -typealias SFriendlyByteBuf = @Contextual FriendlyByteBuf - -/** - * Contextual type-alias for [ResourceLocation] that uses [ResourceLocationSerializer] when - * the field is annotated with `@Contextual`. - */ -typealias SResourceLocation = @Contextual ResourceLocation - -/** - * Contextual type-alias for [Vec3i] that uses [Vec3iSerializer] when the field is - * annotated with `@Contextual`. - */ -typealias SVec3i = @Contextual Vec3i - -/** - * Contextual type-alias for [Vec3] that uses [Vec3Serializer] when the field is - * annotated with `@Contextual`. - */ -typealias SVec3 = @Contextual Vec3 - -/** - * Contextual type-alias for [BlockPos] that uses [BlockPosSerializer] when the field - * is annotated with `@Contextual`. - */ -typealias SBlockPos = @Contextual BlockPos - -/** - * Contextual type-alias for [ChunkPos] that uses [ChunkPosSerializer] when the field - * is annotated with `@Contextual`. - */ -typealias SChunkPos = @Contextual ChunkPos - -/** - * Contextual type-alias for [GlobalPos] that uses [GlobalPosSerializer] when the field - * is annotated with `@Contextual`. - */ -typealias SGlobalPos = @Contextual GlobalPos - -/** - * Contextual type-alias for [BlockHitResult] that uses [BlockHitResultSerializer] when - * the field is annotated with `@Contextual`. - */ -typealias SBlockHitResult = @Contextual BlockHitResult - -/** - * Contextual type-alias for [ItemStack] that uses a [net.kernelpanicsoft.archie.serialization.CodecSerializer] - * over [ItemStack.CODEC] when the field is annotated with `@Contextual`. - */ -typealias SItemStack = @Contextual ItemStack - -/* ─────────────────────── Serializers ─────────────────────── */ - -/** - * A [KSerializer] for [FriendlyByteBuf] that encodes/decodes the buffer contents as a - * raw byte array. - * - * The reader index is preserved after serialization so the buffer can be reused. - */ -object FriendlyByteBufSerializer : KSerializer { - override val descriptor: SerialDescriptor = - SerialDescriptor("FriendlyByteBuf", ByteArraySerializer().descriptor) - - override fun serialize(encoder: Encoder, value: FriendlyByteBuf) { - val index = value.readerIndex() - val bytes = ByteArray(value.readableBytes()) - value.readBytes(bytes) - value.readerIndex(index) - encoder.encodeSerializableValue(ByteArraySerializer(), bytes) - } - - override fun deserialize(decoder: Decoder): FriendlyByteBuf = - FriendlyByteBuf(Unpooled.buffer()).apply { - writeBytes(decoder.decodeSerializableValue(ByteArraySerializer())) - } -} - -/** - * A [KSerializer] for [ResourceLocation] that encodes/decodes its `namespace:path` string form. - */ -object ResourceLocationSerializer : KSerializer -{ - override val descriptor: SerialDescriptor = PrimitiveSerialDescriptor("ResourceLocation", PrimitiveKind.STRING) - - override fun deserialize(decoder: Decoder): ResourceLocation - { - return ResourceLocation.parse(decoder.decodeString()) - } - - override fun serialize(encoder: Encoder, value: ResourceLocation) - { - encoder.encodeString(value.toString()) - } - -} - -/** - * A [KSerializer] for [Vec3i] (and its subclass [BlockPos]) that encodes/decodes the - * three integer components. - */ -object Vec3iSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Vec3i") { - element("x") - element("y") - element("z") - } - - override fun serialize(encoder: Encoder, value: Vec3i) { - encoder.encodeStructure(descriptor) { - encodeIntElement(descriptor, 0, value.x) - encodeIntElement(descriptor, 1, value.y) - encodeIntElement(descriptor, 2, value.z) - } - } - - override fun deserialize(decoder: Decoder): Vec3i = - decoder.decodeStructure(descriptor) { - var x: Int? = null; var y: Int? = null; var z: Int? = null - while (true) { - when (val index = decodeElementIndex(descriptor)) { - 0 -> x = decodeIntElement(descriptor, 0) - 1 -> y = decodeIntElement(descriptor, 1) - 2 -> z = decodeIntElement(descriptor, 2) - CompositeDecoder.DECODE_DONE -> break - else -> throw SerializationException("Unexpected index: $index") - } - } - Vec3i(x ?: throw SerializationException("Missing x"), y ?: throw SerializationException("Missing y"), z ?: throw SerializationException("Missing z")) - } -} - -/** - * A [KSerializer] for [Vec3] that encodes/decodes the three double-precision components. - */ -object Vec3Serializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Vec3") { - element("x") - element("y") - element("z") - } - - override fun serialize(encoder: Encoder, value: Vec3) { - encoder.encodeStructure(descriptor) { - encodeDoubleElement(descriptor, 0, value.x) - encodeDoubleElement(descriptor, 1, value.y) - encodeDoubleElement(descriptor, 2, value.z) - } - } - - override fun deserialize(decoder: Decoder): Vec3 = - decoder.decodeStructure(descriptor) { - var x: Double? = null; var y: Double? = null; var z: Double? = null - while (true) { - when (val index = decodeElementIndex(descriptor)) { - 0 -> x = decodeDoubleElement(descriptor, 0) - 1 -> y = decodeDoubleElement(descriptor, 1) - 2 -> z = decodeDoubleElement(descriptor, 2) - CompositeDecoder.DECODE_DONE -> break - else -> throw SerializationException("Unexpected index: $index") - } - } - Vec3(x ?: throw SerializationException("Missing x"), y ?: throw SerializationException("Missing y"), z ?: throw SerializationException("Missing z")) - } -} - -/** - * A [KSerializer] for [BlockPos] that encodes/decodes the three integer components. - */ -object BlockPosSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("BlockPos") { - element("x") - element("y") - element("z") - } - - override fun serialize(encoder: Encoder, value: BlockPos) { - encoder.encodeStructure(descriptor) { - encodeIntElement(descriptor, 0, value.x) - encodeIntElement(descriptor, 1, value.y) - encodeIntElement(descriptor, 2, value.z) - } - } - - override fun deserialize(decoder: Decoder): BlockPos = - decoder.decodeStructure(descriptor) { - var x: Int? = null; var y: Int? = null; var z: Int? = null - while (true) { - when (val index = decodeElementIndex(descriptor)) { - 0 -> x = decodeIntElement(descriptor, 0) - 1 -> y = decodeIntElement(descriptor, 1) - 2 -> z = decodeIntElement(descriptor, 2) - CompositeDecoder.DECODE_DONE -> break - else -> throw SerializationException("Unexpected index: $index") - } - } - BlockPos(x ?: throw SerializationException("Missing x"), y ?: throw SerializationException("Missing y"), z ?: throw SerializationException("Missing z")) - } -} - -/** - * A [KSerializer] for [ChunkPos] that encodes/decodes the value as a single packed [Long]. - */ -object ChunkPosSerializer : KSerializer { - override val descriptor: SerialDescriptor = - PrimitiveSerialDescriptor("ChunkPos", PrimitiveKind.LONG) - - override fun serialize(encoder: Encoder, value: ChunkPos) = encoder.encodeLong(value.toLong()) - override fun deserialize(decoder: Decoder): ChunkPos = ChunkPos(decoder.decodeLong()) -} - -/** - * A [KSerializer] for [ResourceKey] of a specific registry. - * - * The serialized form is a [ResourceLocation] string (the key's location). - * - * ### Example - * ```kotlin - * val DIMENSION_KEY_SERIALIZER = ResourceKeySerializer(Registries.DIMENSION) - * ``` - * - * @param registry The [ResourceKey] of the registry this serializer is scoped to. - */ -class ResourceKeySerializer(val registry: ResourceKey>) : - KSerializer> { - companion object { - /** Pre-built serializer for dimension [ResourceKey]s. */ - val DIMENSION = ResourceKeySerializer(Registries.DIMENSION) - } - - override val descriptor: SerialDescriptor = ResourceLocationSerializer.descriptor - - override fun serialize(encoder: Encoder, value: ResourceKey<*>) = - encoder.encodeSerializableValue(ResourceLocationSerializer, value.location()) - - override fun deserialize(decoder: Decoder): ResourceKey<*> = - ResourceKey.create(registry, decoder.decodeSerializableValue(ResourceLocationSerializer)) -} - -/** - * A [KSerializer] for [GlobalPos] that encodes the dimension [ResourceKey] and [BlockPos]. - */ -object GlobalPosSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("GlobalPos") { - element("dimension", ResourceKeySerializer.DIMENSION.descriptor) - element("pos", BlockPosSerializer.descriptor) - } - - override fun serialize(encoder: Encoder, value: GlobalPos) { - encoder.encodeStructure(descriptor) { - encodeSerializableElement(descriptor, 0, ResourceKeySerializer.DIMENSION, value.dimension()) - encodeSerializableElement(descriptor, 1, BlockPosSerializer, value.pos()) - } - } - - @Suppress("UNCHECKED_CAST") - override fun deserialize(decoder: Decoder): GlobalPos = - decoder.decodeStructure(descriptor) { - var dimension: ResourceKey? = null - var pos: BlockPos? = null - while (true) { - when (val index = decodeElementIndex(descriptor)) { - 0 -> dimension = decodeSerializableElement(descriptor, 0, ResourceKeySerializer.DIMENSION) as ResourceKey - 1 -> pos = decodeSerializableElement(descriptor, 1, BlockPosSerializer) - CompositeDecoder.DECODE_DONE -> break - else -> throw SerializationException("Unexpected index: $index") - } - } - GlobalPos(dimension ?: throw SerializationException("Missing dimension"), pos ?: throw SerializationException("Missing pos")) - } -} - -/** - * A [KSerializer] for [BlockHitResult] that encodes the hit location, face direction, - * block position, whether the hit is inside the block, and whether it was a miss. - */ -object BlockHitResultSerializer : KSerializer { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("BlockHitResult") { - element("location", Vec3Serializer.descriptor) - element("side") - element("blockPos", BlockPosSerializer.descriptor) - element("insideBlock") - element("missed") - } - - override fun serialize(encoder: Encoder, value: BlockHitResult) { - encoder.encodeStructure(descriptor) { - encodeSerializableElement(descriptor, 0, Vec3Serializer, value.location) - encodeStringElement(descriptor, 1, value.direction.name) - encodeSerializableElement(descriptor, 2, BlockPosSerializer, value.blockPos) - encodeBooleanElement(descriptor, 3, value.isInside) - encodeBooleanElement(descriptor, 4, value.type == HitResult.Type.MISS) - } - } - - override fun deserialize(decoder: Decoder): BlockHitResult { - var location: Vec3? = null; var side: Direction? = null - var pos: BlockPos? = null; var inside: Boolean? = null; var missed: Boolean? = null - decoder.decodeStructure(descriptor) { - while (true) { - when (val index = decodeElementIndex(descriptor)) { - 0 -> location = decodeSerializableElement(descriptor, 0, Vec3Serializer) - 1 -> side = Direction.byName(decodeStringElement(descriptor, 1)) - 2 -> pos = decodeSerializableElement(descriptor, 2, BlockPosSerializer) - 3 -> inside = decodeBooleanElement(descriptor, 3) - 4 -> missed = decodeBooleanElement(descriptor, 4) - CompositeDecoder.DECODE_DONE -> break - else -> throw SerializationException("Unexpected index: $index") - } - } - } - if (location == null || side == null || pos == null || inside == null || missed == null) - throw SerializationException("Properties missing when decoding BlockHitResult") - return if (missed == true) BlockHitResult.miss(location, side!!, pos) - else BlockHitResult(location, side!!, pos, inside) - } -} - -/** - * A [SerializersModule] that registers all built-in Minecraft type serializers as contextual - * serializers. - * - * Include this module in your serialization format instances to enable `@Contextual` on - * Minecraft types. - */ -val MinecraftSerializersModule = SerializersModule { - contextual(FriendlyByteBuf::class, FriendlyByteBufSerializer) - contextual(ResourceLocation::class, ResourceLocationSerializer) - contextual(Vec3i::class, Vec3iSerializer) - contextual(Vec3::class, Vec3Serializer) - contextual(BlockPos::class, BlockPosSerializer) - contextual(ChunkPos::class, ChunkPosSerializer) - contextual(GlobalPos::class, GlobalPosSerializer) - contextual(BlockHitResult::class, BlockHitResultSerializer) - contextual(ItemStack::class, CodecSerializer(ItemStack.CODEC)) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt deleted file mode 100644 index be325218f..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt +++ /dev/null @@ -1,124 +0,0 @@ -package net.kernelpanicsoft.archie.transfer - -import dev.architectury.registry.registries.RegistrySupplier -import earth.terrarium.common_storage_lib.context.ItemContext -import earth.terrarium.common_storage_lib.energy.EnergyApi -import earth.terrarium.common_storage_lib.fluid.FluidApi -import earth.terrarium.common_storage_lib.item.ItemApi -import earth.terrarium.common_storage_lib.lookup.BlockLookup -import earth.terrarium.common_storage_lib.lookup.ItemLookup -import net.minecraft.core.Direction -import net.minecraft.world.item.Item -import net.minecraft.world.item.ItemStack -import net.minecraft.world.level.block.entity.BlockEntity -import net.minecraft.world.level.block.entity.BlockEntityType - -/** - * Exposes [ArchieItemStorage]/[ArchieFluidStorage]/[ArchieEnergyStorage] to third-party mods' - * pipes/hoppers/etc, by registering against Common Storage Lib's [ItemApi]/[FluidApi]/[EnergyApi] - * `BLOCK` lookups - which are, unlike a lookup you'd build yourself via [BlockLookup.create], the - * real, already-canonical singletons Common Storage Lib itself wires straight through to each - * platform's native capability system (Fabric Transfer API's `ItemStorage.SIDED`/`FluidStorage.SIDED`, - * NeoForge's `Capabilities.ItemHandler.BLOCK`/`Capabilities.FluidHandler.BLOCK`). Registering here - * makes a block entity's storage visible to *any* mod querying those native systems directly - no - * dependency on Common Storage Lib (or Archie) required on the consuming side. - * - * Deliberately explicit opt-in, not wired into [net.kernelpanicsoft.archie.serialization.NBTHolder.itemField]/ - * `fluidField`/`energyField`: registration must happen exactly once per [BlockEntityType], while - * those field delegates run once per block entity *instance* (inside its constructor) - auto-registering - * from there would either re-register redundantly per instance or need awkward static bookkeeping. - * Call these once, at registration time, next to your `DeferredRegister`/`RegistrySupplier` declarations: - * - * ```kotlin - * object BlockEntities : ADeferredRegistryHolder>(MyMod.MOD, Registries.BLOCK_ENTITY_TYPE) { - * val TANK by register("tank") { BlockEntityType.Builder.of(::TankBlockEntity, MyBlocks.TANK).build(null) } - * } - * - * // In mod init, after BlockEntities.init(): - * BlockEntities.TANK.exposeFluidStorage { tank -> tank.fluid } - * ``` - */ -@Suppress("unused") -object ArchieCapabilityExposure - -/** See [ArchieCapabilityExposure]. Exposes this block entity type's [ArchieItemStorage] to [ItemApi.BLOCK]. */ -fun BlockEntityType.exposeItemStorage(selector: (T, Direction?) -> ArchieItemStorage?) { - exposeToBlockLookup(ItemApi.BLOCK, selector) -} - -/** [exposeItemStorage] overload for a selector that doesn't need the query direction. */ -fun BlockEntityType.exposeItemStorage(selector: (T) -> ArchieItemStorage?) { - exposeItemStorage { be, _ -> selector(be) } -} - -/** See [ArchieCapabilityExposure]. Exposes this block entity type's [ArchieFluidStorage] to [FluidApi.BLOCK]. */ -fun BlockEntityType.exposeFluidStorage(selector: (T, Direction?) -> ArchieFluidStorage?) { - exposeToBlockLookup(FluidApi.BLOCK, selector) -} - -/** [exposeFluidStorage] overload for a selector that doesn't need the query direction. */ -fun BlockEntityType.exposeFluidStorage(selector: (T) -> ArchieFluidStorage?) { - exposeFluidStorage { be, _ -> selector(be) } -} - -/** See [ArchieCapabilityExposure]. Exposes this block entity type's [ArchieEnergyStorage] to [EnergyApi.BLOCK]. */ -fun BlockEntityType.exposeEnergyStorage(selector: (T, Direction?) -> ArchieEnergyStorage?) { - exposeToBlockLookup(EnergyApi.BLOCK, selector) -} - -/** [exposeEnergyStorage] overload for a selector that doesn't need the query direction. */ -fun BlockEntityType.exposeEnergyStorage(selector: (T) -> ArchieEnergyStorage?) { - exposeEnergyStorage { be, _ -> selector(be) } -} - -/** - * Shared implementation: [BlockLookup] only supports registering by [BlockEntityType] via the - * [BlockLookup.BlockRegistrar] callback handed to [BlockLookup.onRegister] - `registerSelf` (the - * more direct-looking method) only accepts a `Block`-keyed getter, not a block-entity-keyed one. - */ -@Suppress("UNCHECKED_CAST") -private fun BlockEntityType.exposeToBlockLookup( - lookup: BlockLookup, - selector: (T, Direction?) -> S?, -) { - lookup.onRegister { registrar -> - registrar.registerBlockEntities( - BlockLookup.BlockEntityGetter { blockEntity, direction -> selector(blockEntity as T, direction) }, - this, - ) - } -} - -// ── RegistrySupplier convenience overloads ────────────────────────────────────────────────── -// So these can be chained right where the type is declared, without waiting for a separate -// registration-time call site. Architectury's RegistrySupplier.listen(...) already guarantees the -// callback runs once the entry is actually registered. - -fun RegistrySupplier>.exposeItemStorage(selector: (T, Direction?) -> ArchieItemStorage?) = - listen { it.exposeItemStorage(selector) } - -fun RegistrySupplier>.exposeItemStorage(selector: (T) -> ArchieItemStorage?) = - listen { it.exposeItemStorage(selector) } - -fun RegistrySupplier>.exposeFluidStorage(selector: (T, Direction?) -> ArchieFluidStorage?) = - listen { it.exposeFluidStorage(selector) } - -fun RegistrySupplier>.exposeFluidStorage(selector: (T) -> ArchieFluidStorage?) = - listen { it.exposeFluidStorage(selector) } - -fun RegistrySupplier>.exposeEnergyStorage(selector: (T, Direction?) -> ArchieEnergyStorage?) = - listen { it.exposeEnergyStorage(selector) } - -fun RegistrySupplier>.exposeEnergyStorage(selector: (T) -> ArchieEnergyStorage?) = - listen { it.exposeEnergyStorage(selector) } - -// ── Item-in-item exposure (stretch) ───────────────────────────────────────────────────────── -// Unlike the BLOCK lookups above, ItemApi.ITEM has no equivalent native-platform bridge - it's -// registered under Common Storage Lib's own mod id, so this is only visible to other mods that -// also depend on Common Storage Lib and query this exact same field. Still useful: it makes a -// backpack/bag's own storage pipe-accessible (by other CSL-aware mods) even while its GUI is closed. - -/** Exposes this item's [ArchieItemStorage] (e.g. a bag/backpack's contents) to [ItemApi.ITEM]. */ -fun Item.exposeItemStorage(selector: (ItemStack, ItemContext) -> ArchieItemStorage?) { - ItemApi.ITEM.registerSelf(ItemLookup.ItemGetter { stack, context -> selector(stack, context) }, this) -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt deleted file mode 100644 index c7139fd71..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt +++ /dev/null @@ -1,153 +0,0 @@ -package net.kernelpanicsoft.archie.transfer - -import earth.terrarium.common_storage_lib.storage.base.UpdateManager -import earth.terrarium.common_storage_lib.storage.base.ValueStorage -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.encoding.decodeStructure -import kotlinx.serialization.encoding.encodeStructure -import net.benwoodworth.knbt.NbtTag -import net.kernelpanicsoft.archie.serialization.NBT -import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless -import net.kernelpanicsoft.archie.serialization.encodeToNbtTagRootless -import kotlin.math.min - -/** - * Archie's platform-agnostic energy buffer: implements Common Storage Lib's [ValueStorage] - - * the energy analogue of the `CommonStorage`/`CommonStorage` - * [ArchieItemStorage]/[ArchieFluidStorage] implement - plus Archie's NBT serialization for - * save/load, mirroring their shape. - * - * Not exposed to third-party mods' pipes/hoppers by default - call - * [BlockEntityType.exposeEnergyStorage][net.kernelpanicsoft.archie.transfer.exposeEnergyStorage] - * once, at registration time, to register it with Common Storage Lib's `EnergyApi.BLOCK`, which - * (unlike a lookup you'd build yourself) is wired straight through to each platform's native - * capability system - no dependency on Common Storage Lib required on the consuming side. Any - * additional platform-specific bridge you still want on top of that (NeoForge's `IEnergyStorage`, - * Fabric's Team Reborn Energy API) is still on you to wire in your own mod. - * - * Usually created through [net.kernelpanicsoft.archie.serialization.NBTHolder.energyField] - * rather than directly. - * - * @param capacity The maximum amount of energy this storage can hold. - * @param onUpdate Invoked whenever this storage's contents change, for persistence/sync. - */ -@Serializable(with = ArchieEnergyStorage.Serializer::class) -class ArchieEnergyStorage( - private var capacity: Long, - private val onUpdate: () -> Unit = {}, -) : ValueStorage, UpdateManager -{ - private var amount: Long = 0 - - /** The amount of energy currently stored. */ - override fun getStoredAmount(): Long = amount - - /** The maximum amount of energy this storage can hold. */ - override fun getCapacity(): Long = capacity - - override fun allowsInsertion(): Boolean = true - - override fun allowsExtraction(): Boolean = true - - /** - * Inserts up to [amount] energy, returning how much was actually accepted. - * When [simulate] is `true`, no state is changed - only the acceptable amount is calculated. - */ - override fun insert(amount: Long, simulate: Boolean): Long - { - val inserted = min(amount, capacity - this.amount) - if (inserted <= 0) return 0 - if (!simulate) - { - this.amount += inserted - update() - } - return inserted - } - - /** - * Extracts up to [amount] energy, returning how much was actually removed. - * When [simulate] is `true`, no state is changed - only the extractable amount is calculated. - */ - override fun extract(amount: Long, simulate: Boolean): Long - { - val extracted = min(amount, this.amount) - if (extracted <= 0) return 0 - if (!simulate) - { - this.amount -= extracted - update() - } - return extracted - } - - /** Directly overwrites the stored amount, clamped to `0..`[getCapacity]. */ - fun set(amount: Long) - { - this.amount = amount.coerceIn(0, capacity) - update() - } - - /** Snapshots this storage's [getCapacity] and stored amount as an [NbtTag], for save/sync. */ - override fun createSnapshot(): NbtTag = NBT.encodeToNbtTagRootless(serializer(), this) - - /** - * Restores this storage's capacity and stored amount from a snapshot produced by - * [createSnapshot]. `capacity` is clamped to non-negative and `amount` to `0..capacity` - a - * malformed or stale snapshot (e.g. from before a capacity change) shouldn't be able to leave - * this storage over-capacity or negative, which would otherwise wedge [insert]/[extract]. - */ - override fun readSnapshot(snapshot: NbtTag) - { - val decoded = NBT.decodeFromNbtTagRootless(serializer(), snapshot) - this.capacity = decoded.capacity.coerceAtLeast(0) - this.amount = decoded.amount.coerceIn(0, this.capacity) - } - - override fun update() = onUpdate() - - /** Serializes an [ArchieEnergyStorage] as its capacity followed by its stored amount. */ - object Serializer : KSerializer - { - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ArchieEnergyStorage") { - element("capacity", Long.serializer().descriptor) - element("amount", Long.serializer().descriptor) - } - - override fun deserialize(decoder: Decoder): ArchieEnergyStorage - { - return decoder.decodeStructure(descriptor) - { - var capacity = 0L - var amount = 0L - while (true) - { - when (val index = decodeElementIndex(descriptor)) - { - 0 -> capacity = decodeLongElement(descriptor, 0).coerceAtLeast(0) - 1 -> amount = decodeLongElement(descriptor, 1) - CompositeDecoder.DECODE_DONE -> break - else -> error("Unexpected index: $index") - } - } - ArchieEnergyStorage(capacity).also { it.amount = amount.coerceIn(0, capacity) } - } - } - - override fun serialize(encoder: Encoder, value: ArchieEnergyStorage) - { - encoder.encodeStructure(descriptor) - { - encodeLongElement(descriptor, 0, value.capacity) - encodeLongElement(descriptor, 1, value.amount) - } - } - } -} diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt deleted file mode 100644 index 501b2d518..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt +++ /dev/null @@ -1,192 +0,0 @@ -package net.kernelpanicsoft.archie.transfer - -import dev.architectury.fluid.FluidStack -import earth.terrarium.common_storage_lib.resources.ResourceStack -import earth.terrarium.common_storage_lib.resources.fluid.FluidResource -import earth.terrarium.common_storage_lib.storage.base.StorageSlot -import earth.terrarium.common_storage_lib.storage.base.UpdateManager -import kotlinx.serialization.ExperimentalSerializationApi -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.descriptors.element -import kotlinx.serialization.descriptors.nullable -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.encoding.decodeStructure -import kotlinx.serialization.encoding.encodeStructure -import net.benwoodworth.knbt.NbtTag -import net.kernelpanicsoft.archie.serialization.NBT -import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless -import net.kernelpanicsoft.archie.serialization.encodeToNbtTagRootless -import net.kernelpanicsoft.archie.serialization.kSerializer -import net.minecraft.world.item.Item -import kotlin.math.min - -/** - * A single resource-backed slot inside an [ArchieFluidStorage], capped at [limit]. Tracks a - * [FluidResource] + amount internally while exposing plain [FluidStack] access via - * [getFluid]/[set]. - * - * @param onUpdate Invoked by [update] whenever this slot's contents should be persisted/synced. - */ -@Serializable(with = ArchieFluidSlot.Serializer::class) -class ArchieFluidSlot(private val limit: Long, private val onUpdate: () -> Unit = {}) : StorageSlot, UpdateManager -{ - private var resource: FluidResource = FluidResource.BLANK - private var amount: Long = 0 - private var stack: FluidStack - get() - { - if (resource.isBlank) - return FluidStack.empty() - return FluidStack.create(resource.type, amount) - } - set(value) - { - resource = FluidResource.of(value.fluid) - amount = value.amount - } - - private var resourceStack: ResourceStack - get() - { - return ResourceStack(resource, amount) - } - set(value) - { - resource = value.resource - amount = value.amount - } - - constructor(limit: Long, stack: FluidStack = FluidStack.empty(), onUpdate: () -> Unit = {}) : this(limit, onUpdate) - { - this.stack = stack - } - - constructor(limit: Long, resourceStack: ResourceStack, onUpdate: () -> Unit = {}) : this(limit, onUpdate) - { - this.resourceStack = resourceStack - } - - /** The [FluidStack] currently held in this slot (a copy; mutate via [set]). */ - fun getFluid(): FluidStack = stack - /** Replaces this slot's contents with [value]. */ - fun set(value: FluidStack) - { - stack = value - } - - - - override fun insert(unit: FluidResource, amount: Long, simulate: Boolean): Long - { - if (!isResourceValid(unit)) return 0 - if (this.resource.isBlank()) - { - val inserted = min(amount, limit) - if (!simulate) - { - this.resource = unit - this.amount = inserted - } - return inserted - } else if (this.resource == unit) - { - val inserted = min(amount, limit - this.amount) - if (!simulate) - { - this.amount += inserted - } - return inserted - } - return 0 - } - - override fun extract(unit: FluidResource, amount: Long, simulate: Boolean): Long - { - if (this.resource == unit) - { - val extracted = min(amount, this.amount) - if (!simulate) - { - this.amount -= extracted - if (this.amount == 0L) - { - this.resource = FluidResource.BLANK - } - } - return extracted - } - return 0 - } - - override fun getLimit(resource: FluidResource): Long = limit - - override fun isResourceValid(unit: FluidResource): Boolean = true - - override fun getResource(): FluidResource = resource - - override fun getAmount(): Long = amount - - override fun createSnapshot(): NbtTag - { - return NBT.encodeToNbtTagRootless(serializer(), this) - } - - override fun update() - { - onUpdate() - } - - override fun readSnapshot(snapshot: NbtTag) - { - this.stack = NBT.decodeFromNbtTagRootless(serializer(), snapshot).stack - } - - /** Serializes an [ArchieFluidSlot] as its [limit] followed by its [ResourceStack] (or `null` when blank). */ - @OptIn(ExperimentalSerializationApi::class) - object Serializer : KSerializer - { - private val surrogate = ResourceStack.FLUID_CODEC.kSerializer - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ArchieFluidSlot") { - element("limit", Long.serializer().descriptor) - element("resourceStack", surrogate.descriptor.nullable) - } - - override fun deserialize(decoder: Decoder): ArchieFluidSlot - { - return decoder.decodeStructure(descriptor) - { - var limit = 0L - var resourceStack: ResourceStack? = null - while (true) - { - when (val index = decodeElementIndex(descriptor)) - { - 0 -> limit = decodeLongElement(descriptor, 0) - 1 -> resourceStack = decodeNullableSerializableElement(descriptor, 1, surrogate) - CompositeDecoder.DECODE_DONE -> break - else -> error("Unexpected index: $index") - } - } - ArchieFluidSlot(limit, resourceStack ?: ResourceStack(FluidResource.BLANK, 0)) - } - } - - override fun serialize( - encoder: Encoder, - value: ArchieFluidSlot - ) - { - encoder.encodeStructure(descriptor) - { - encodeLongElement(descriptor, 0, value.limit) - encodeNullableSerializableElement(descriptor, 1, surrogate, value.resourceStack) - } - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt deleted file mode 100644 index b90779a55..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt +++ /dev/null @@ -1,126 +0,0 @@ -package net.kernelpanicsoft.archie.transfer - -import earth.terrarium.common_storage_lib.resources.fluid.FluidResource -import earth.terrarium.common_storage_lib.storage.base.CommonStorage -import earth.terrarium.common_storage_lib.storage.base.UpdateManager -import earth.terrarium.common_storage_lib.storage.util.TransferUtil -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.builtins.serializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.buildClassSerialDescriptor -import kotlinx.serialization.encoding.CompositeDecoder -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import kotlinx.serialization.encoding.decodeStructure -import kotlinx.serialization.encoding.encodeStructure -import net.benwoodworth.knbt.NbtTag -import net.kernelpanicsoft.archie.serialization.NBT -import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless -import net.kernelpanicsoft.archie.serialization.encodeToNbtTagRootless -import net.minecraft.core.NonNullList -import kotlin.math.min - -/** - * The fluid analogue of [ArchieItemStorage]: a fixed-size list of [ArchieFluidSlot]s, each - * capped at [limit], implementing Common Storage Lib's [CommonStorage] and Archie's NBT - * serialization (via [Serializer]) for save/load. - * - * @param onUpdate Invoked by [update] whenever the storage's contents should be persisted/synced. - */ -@Serializable(with = ArchieFluidStorage.Serializer::class) -open class ArchieFluidStorage private constructor( - protected val limit: Long, - protected val slots: NonNullList, - protected val onUpdate: () -> Unit = {} -) : CommonStorage, UpdateManager -{ - /** Creates a storage with [size] empty slots, each capped at [limit]. */ - constructor(limit: Long, size: Int, onUpdate: () -> Unit = {}) : this( - limit, - NonNullList.createWithCapacity(size).apply { - for (i in 0 until size) - { - add(ArchieFluidSlot(limit)) - } - }, onUpdate - ) - - override fun insert(unit: FluidResource, amount: Long, simulate: Boolean): Long - { - return TransferUtil.insertSlots(this, unit, amount, simulate) - } - - override fun extract(unit: FluidResource, amount: Long, simulate: Boolean): Long - { - return TransferUtil.extractSlots(this, unit, amount, simulate) - } - - /** The number of slots in this storage. */ - override fun size(): Int = slots.size - - /** The [ArchieFluidSlot] at [slot]. */ - override fun get(slot: Int): ArchieFluidSlot - { - return slots[slot] - } - - override fun createSnapshot(): NbtTag - { - return NBT.encodeToNbtTagRootless(serializer(), this) - } - - override fun update() - { - onUpdate() - } - - override fun readSnapshot(snapshot: NbtTag) - { - val slots = NBT.decodeFromNbtTagRootless(serializer(), snapshot).slots - for (i in 0 until min(this.slots.size, slots.size)) - { - this.slots[i] = slots[i] - } - } - - /** Serializes an [ArchieFluidStorage] as its [limit] followed by the list of its [ArchieFluidSlot]s. */ - object Serializer : KSerializer - { - private val surrogate = ListSerializer(ArchieFluidSlot.serializer()) - override val descriptor: SerialDescriptor = buildClassSerialDescriptor("ArchieFluidStorage") { - element("limit", Long.serializer().descriptor) - element("slots", surrogate.descriptor) - } - - override fun deserialize(decoder: Decoder): ArchieFluidStorage - { - return decoder.decodeStructure(descriptor) - { - var limit = 0L - var slots: List = emptyList() - while (true) - { - when (val index = decodeElementIndex(descriptor)) - { - 0 -> limit = decodeLongElement(descriptor, 0) - 1 -> slots = decodeSerializableElement(descriptor, 1, surrogate) - CompositeDecoder.DECODE_DONE -> break - else -> error("Unexpected index: $index") - } - } - ArchieFluidStorage(limit, NonNullList.of(ArchieFluidSlot(limit), *slots.toTypedArray())) - } - } - - override fun serialize(encoder: Encoder, value: ArchieFluidStorage) - { - encoder.encodeStructure(descriptor) - { - encodeLongElement(descriptor, 0, value.limit) - encodeSerializableElement(descriptor, 1, surrogate, value.slots) - } - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemMenuSlot.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemMenuSlot.kt deleted file mode 100644 index 3dcf42195..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemMenuSlot.kt +++ /dev/null @@ -1,64 +0,0 @@ -package net.kernelpanicsoft.archie.transfer - -import earth.terrarium.common_storage_lib.storage.base.UpdateManager -import net.kernelpanicsoft.archie.gui.ComposeContainerMenuBase -import net.minecraft.world.SimpleContainer -import net.minecraft.world.inventory.Slot -import net.minecraft.world.item.ItemStack -import java.util.function.Predicate - -/** - * A vanilla [Slot] that bridges one slot of an [ArchieItemStorage] into a [ComposeContainerMenuBase], - * so `net.minecraft.world.inventory` machinery (shift-click, drag, etc.) can operate on it - * directly. Created by [ComposeContainerMenuBase] from a `handler(group, storage, filter)` - * registration; not usually constructed directly. - * - * @param filter Restricts which stacks [mayPlace] into this slot. - */ -class ArchieItemMenuSlot( - private val storage: ArchieItemStorage, - val filter: Predicate = Predicate { true }, - slot: Int, x: Int, y: Int, - private val owningMenu: ComposeContainerMenuBase<*>, -) : Slot(SimpleContainer(0), slot, x, y) -{ - override fun isActive(): Boolean = owningMenu.isSlotVisible(index) - - - override fun getItem(): ItemStack - { - val slot = storage[containerSlot] - return slot.getItem() - } - - override fun set(stack: ItemStack) - { - val slot = storage[containerSlot] - slot.set(stack) - setChanged() - } - - override fun getMaxStackSize(): Int - { - val slot = storage[containerSlot] - return slot.getMaxStackSize() - } - - override fun setChanged() - { - UpdateManager.batch(storage) - } - - override fun remove(amount: Int): ItemStack - { - val slot = storage[containerSlot] - val ret = slot.remove(amount) - setChanged() - return ret - } - - override fun mayPlace(stack: ItemStack): Boolean - { - return filter.test(stack) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemSlot.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemSlot.kt deleted file mode 100644 index 2bf6f01d3..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemSlot.kt +++ /dev/null @@ -1,175 +0,0 @@ -package net.kernelpanicsoft.archie.transfer - -import net.kernelpanicsoft.archie.serialization.NBT -import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless -import net.kernelpanicsoft.archie.serialization.encodeToNbtTagRootless -import earth.terrarium.common_storage_lib.resources.ResourceStack -import earth.terrarium.common_storage_lib.resources.item.ItemResource -import earth.terrarium.common_storage_lib.storage.base.StorageSlot -import earth.terrarium.common_storage_lib.storage.base.UpdateManager -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.descriptors.nullable -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import net.benwoodworth.knbt.NbtTag -import net.kernelpanicsoft.archie.serialization.kSerializer -import net.minecraft.world.item.Item -import net.minecraft.world.item.ItemStack -import kotlin.math.min - -/** - * A single resource-backed slot inside an [ArchieItemStorage]. Tracks an [ItemResource] + - * amount internally (for Common Storage Lib's resource-based [insert]/[extract]) while exposing - * plain [ItemStack] access via [getItem]/[set]. - * - * @param onUpdate Invoked by [update] whenever this slot's contents should be persisted/synced. - */ -@Serializable(with = ArchieItemSlot.Serializer::class) -class ArchieItemSlot(private val onUpdate: () -> Unit = {}) : StorageSlot, UpdateManager -{ - private var resource: ItemResource = ItemResource.BLANK - private var amount: Long = 0 - private var stack: ItemStack - get() - { - if (resource.isBlank) - return ItemStack.EMPTY - return resource.toStack(amount.toInt()) - } - set(value) - { - resource = ItemResource.of(value) - amount = value.count.toLong() - } - - private var resourceStack: ResourceStack - get() - { - return ResourceStack(resource, amount) - } - set(value) - { - resource = value.resource - amount = value.amount - } - - constructor(stack: ItemStack = ItemStack.EMPTY, onUpdate: () -> Unit = {}) : this(onUpdate) - { - this.stack = stack - } - - constructor(resourceStack: ResourceStack, onUpdate: () -> Unit = {}) : this(onUpdate) - { - this.resourceStack = resourceStack - } - - /** The [ItemStack] currently held in this slot (a copy; mutate via [set]). */ - fun getItem(): ItemStack = stack - /** Replaces this slot's contents with [value]. */ - fun set(value: ItemStack) - { - stack = value - } - - /** Splits up to [amount] items off this slot's stack and returns them, leaving the rest in place. */ - fun remove(amount: Int): ItemStack - { - return if (!stack.isEmpty && amount > 0) stack.let { - val ret = it.split(amount) - stack = it - ret - } else ItemStack.EMPTY - } - - /** The maximum stack size for the resource currently held (or [Item.ABSOLUTE_MAX_STACK_SIZE] if empty). */ - fun getMaxStackSize(): Int = getLimit(resource).toInt() - - - - override fun insert(unit: ItemResource, amount: Long, simulate: Boolean): Long - { - if (!isResourceValid(unit)) return 0 - if (this.resource.isBlank) - { - val inserted = - min(amount.toDouble(), unit.cachedStack.maxStackSize.toDouble()).toLong() - if (!simulate) - { - this.resource = unit - this.amount = inserted - } - return inserted - } else if (this.resource.test(unit.toStack())) - { - val inserted = - min(amount.toDouble(), (getLimit(resource) - this.amount).toDouble()).toLong() - if (!simulate) - { - this.amount += inserted - } - return inserted - } - return 0 - } - - override fun extract(unit: ItemResource, amount: Long, simulate: Boolean): Long - { - if (this.resource.test(unit.toStack())) - { - val extracted = min(amount.toDouble(), this.amount.toDouble()).toLong() - if (!simulate) - { - this.amount -= extracted - if (this.amount == 0L) - { - this.resource = ItemResource.BLANK - } - } - return extracted - } - return 0 - } - - override fun getLimit(resource: ItemResource): Long = - if (resource.isBlank) Item.ABSOLUTE_MAX_STACK_SIZE.toLong() - else resource.cachedStack.maxStackSize.toLong() - - override fun isResourceValid(unit: ItemResource): Boolean = true - - override fun getResource(): ItemResource = resource - - override fun getAmount(): Long = amount - - override fun createSnapshot(): NbtTag - { - return NBT.encodeToNbtTagRootless(serializer(), this) - } - - override fun update() - { - onUpdate() - } - - override fun readSnapshot(snapshot: NbtTag) - { - this.stack = NBT.decodeFromNbtTagRootless(serializer(), snapshot).stack - } - - /** Serializes an [ArchieItemSlot] as its underlying [ResourceStack], or `null` when blank. */ - object Serializer : KSerializer - { - private val surrogate = ResourceStack.ITEM_CODEC.kSerializer - override val descriptor: SerialDescriptor = surrogate.descriptor.nullable - override fun deserialize(decoder: Decoder): ArchieItemSlot - { - return ArchieItemSlot(surrogate.deserialize(decoder)) - } - - override fun serialize(encoder: Encoder, value: ArchieItemSlot) - { - surrogate.serialize(encoder, value.resourceStack) - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemStorage.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemStorage.kt deleted file mode 100644 index 59f68507a..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemStorage.kt +++ /dev/null @@ -1,102 +0,0 @@ -package net.kernelpanicsoft.archie.transfer - -import net.kernelpanicsoft.archie.serialization.NBT -import net.kernelpanicsoft.archie.serialization.decodeFromNbtTagRootless -import net.kernelpanicsoft.archie.serialization.encodeToNbtTagRootless -import earth.terrarium.common_storage_lib.resources.item.ItemResource -import earth.terrarium.common_storage_lib.storage.base.CommonStorage -import earth.terrarium.common_storage_lib.storage.base.UpdateManager -import earth.terrarium.common_storage_lib.storage.util.TransferUtil -import kotlinx.serialization.KSerializer -import kotlinx.serialization.Serializable -import kotlinx.serialization.builtins.ListSerializer -import kotlinx.serialization.descriptors.SerialDescriptor -import kotlinx.serialization.encoding.Decoder -import kotlinx.serialization.encoding.Encoder -import net.benwoodworth.knbt.NbtTag -import net.minecraft.core.NonNullList -import kotlin.math.min - -/** - * Archie's platform-agnostic item container: a fixed-size list of [ArchieItemSlot]s that - * implements Common Storage Lib's [CommonStorage] (resource-based [insert]/[extract] for - * capability interop) and Archie's NBT serialization (via [Serializer]) for save/load. - * - * Usually created through [net.kernelpanicsoft.archie.serialization.NBTHolder.itemField] rather - * than directly. Its public surface is deliberately small; read or mutate the [net.minecraft.world.item.ItemStack] in a - * slot through the [ArchieItemSlot] returned by [get], not on the storage itself. - * - * @param onUpdate Invoked by [update] whenever the storage's contents should be persisted/synced. - */ -@Serializable(with = ArchieItemStorage.Serializer::class) -open class ArchieItemStorage private constructor( - protected var slots: NonNullList, - protected val onUpdate: () -> Unit = {} -) : CommonStorage, UpdateManager -{ - /** Creates a storage with [size] empty slots. */ - constructor(size: Int, onUpdate: () -> Unit = {}) : this( - NonNullList.createWithCapacity(size).apply { - for (i in 0 until size) - { - add(ArchieItemSlot()) - } - }, onUpdate - ) - - override fun insert(unit: ItemResource, amount: Long, simulate: Boolean): Long - { - return TransferUtil.insertSlots(this, unit, amount, simulate) - } - - override fun extract(unit: ItemResource, amount: Long, simulate: Boolean): Long - { - return TransferUtil.extractSlots(this, unit, amount, simulate) - } - - /** The number of slots in this storage. */ - override fun size(): Int = slots.size - - /** The [ArchieItemSlot] at [slot], for reading/mutating its [net.minecraft.world.item.ItemStack]. */ - override fun get(slot: Int): ArchieItemSlot - { - return slots[slot] - } - - override fun createSnapshot(): NbtTag - { - return NBT.encodeToNbtTagRootless(serializer(), this) - } - - override fun update() - { - onUpdate() - } - - override fun readSnapshot(snapshot: NbtTag) - { - val slots = NBT.decodeFromNbtTagRootless(serializer(), snapshot).slots - for (i in 0 until min(this.slots.size, slots.size)) - { - this.slots[i] = slots[i] - } - } - - /** Serializes an [ArchieItemStorage] as the plain list of its [ArchieItemSlot]s. */ - object Serializer : KSerializer - { - private val surrogate = ListSerializer(ArchieItemSlot.serializer()) - override val descriptor: SerialDescriptor = surrogate.descriptor - - override fun deserialize(decoder: Decoder): ArchieItemStorage - { - return ArchieItemStorage(NonNullList.of(ArchieItemSlot(), *surrogate.deserialize(decoder).toTypedArray())) - } - - override fun serialize(encoder: Encoder, value: ArchieItemStorage) - { - surrogate.serialize(encoder, value.slots) - } - - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/VanillaMenuSlot.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/VanillaMenuSlot.kt deleted file mode 100644 index 6c94705cb..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/VanillaMenuSlot.kt +++ /dev/null @@ -1,69 +0,0 @@ -package net.kernelpanicsoft.archie.transfer - -import earth.terrarium.common_storage_lib.item.impl.vanilla.AbstractVanillaContainer -import earth.terrarium.common_storage_lib.item.impl.vanilla.VanillaDelegatingSlot -import earth.terrarium.common_storage_lib.storage.base.UpdateManager -import net.kernelpanicsoft.archie.gui.ComposeContainerMenuBase -import net.minecraft.world.SimpleContainer -import net.minecraft.world.inventory.Slot -import net.minecraft.world.item.ItemStack -import java.util.function.Predicate - -/** - * The [ArchieItemMenuSlot] equivalent for adapting an existing vanilla-style - * [AbstractVanillaContainer] (rather than an [ArchieItemStorage]) into a [ComposeContainerMenuBase]. - * Created by [ComposeContainerMenuBase] from a `handler(group, storage, filter)` registration; not - * usually constructed directly. - * - * @param filter Restricts which stacks [mayPlace] into this slot. - */ -class VanillaMenuSlot( - private val storage: AbstractVanillaContainer, - val filter: Predicate = Predicate { true }, - slot: Int, x: Int, y: Int, - private val owningMenu: ComposeContainerMenuBase<*>, -) : Slot(SimpleContainer(0), slot, x, y) -{ - override fun isActive(): Boolean = owningMenu.isSlotVisible(index) - - - override fun getItem(): ItemStack - { - val slot = storage[containerSlot] as VanillaDelegatingSlot - return slot.createSnapshot() - } - - override fun set(stack: ItemStack) - { - val slot = storage[containerSlot] as VanillaDelegatingSlot - slot.readSnapshot(stack) - setChanged() - } - - override fun getMaxStackSize(): Int - { - val slot = storage[containerSlot] as VanillaDelegatingSlot - return slot.getLimit(slot.resource).toInt() - } - - override fun setChanged() - { - UpdateManager.batch(storage) - } - - override fun remove(amount: Int): ItemStack - { - // set(it) already calls setChanged() - an unconditional call here would double up the - // UpdateManager.batch() dispatch, and would also fire when nothing was actually removed. - return if (!item.isEmpty && amount > 0) item.let { - val ret = it.split(amount) - set(it) - ret - } else ItemStack.EMPTY - } - - override fun mayPlace(stack: ItemStack): Boolean - { - return filter.test(stack) - } -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Array.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Array.kt deleted file mode 100644 index 6d5222793..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Array.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.kernelpanicsoft.archie.util - -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract -import kotlin.experimental.ExperimentalTypeInference - -/** Builds a reference [Array] of [T] using the [buildList] DSL via [builderAction]. */ -@OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class) -inline fun buildArray(@BuilderInference builderAction: MutableList.() -> Unit): Array -{ - contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } - return buildList(builderAction).toTypedArray() -} - -/** Like [buildArray], but pre-sizes the backing list to [capacity]. */ -@OptIn(ExperimentalTypeInference::class, ExperimentalContracts::class) -inline fun buildArray(capacity: Int, @BuilderInference builderAction: MutableList.() -> Unit): Array -{ - contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } - return buildList(capacity, builderAction).toTypedArray() -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Component.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Component.kt deleted file mode 100644 index 7e8427008..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Component.kt +++ /dev/null @@ -1,244 +0,0 @@ -@file:Suppress("unused") -@file:OptIn(ExperimentalContracts::class) -package net.kernelpanicsoft.archie.util - -import net.minecraft.network.chat.* -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.entity.EntityType -import net.minecraft.world.entity.player.Player -import net.minecraft.world.item.ItemStack -import java.util.* -import kotlin.contracts.ExperimentalContracts -import kotlin.contracts.InvocationKind -import kotlin.contracts.contract - -@DslMarker -annotation class ComponentBuilderDsl - -@ComponentBuilderDsl -class ComponentBuilder @PublishedApi internal constructor(private val component: MutableComponent = Component.empty()) -{ - fun build(): Component - { - return component - } - - fun text(text: String, block: ComponentBuilder.() -> Unit = {}) - { - component.append(ComponentBuilder(Component.literal(text)).apply(block).build()) - } - - fun translate(key: String, vararg args: Any, block: ComponentBuilder.() -> Unit = {}) - { - component.append(ComponentBuilder(Component.translatable(key, *args)).apply(block).build()) - } - - fun style(style: Style) - { - component.withStyle(style) - } - - fun style(block: StyleBuilder.() -> Unit) - { - component.withStyle(StyleBuilder(component.style).apply(block).build()) - } -} - -@ComponentBuilderDsl -class StyleBuilder @PublishedApi internal constructor(private var style: Style = Style.EMPTY) -{ - fun build(): Style - { - return style - } - - var color: TextColor? - get() = style.color - set(color) - { - style = style.withColor(color) - } - - var bold: Boolean? - get() = style.isBold - set(bold) - { - style = style.withBold(bold) - } - - var italic: Boolean? - get() = style.isItalic - set(italic) - { - style = style.withItalic(italic) - } - - var underlined: Boolean? - get() = style.isUnderlined - set(underlined) - { - style = style.withUnderlined(underlined) - } - - var strikethrough: Boolean? - get() = style.isStrikethrough - set(strikethrough) - { - style = style.withStrikethrough(strikethrough) - } - - var obfuscated: Boolean? - get() = style.isObfuscated - set(obfuscated) - { - style = style.withObfuscated(obfuscated) - } - - var clickEvent: ClickEvent? - get() = style.clickEvent - set(clickEvent) - { - style = style.withClickEvent(clickEvent) - } - - fun clickEvent(block: ClickEventBuilder.() -> Unit) - { - clickEvent = ClickEventBuilder().apply(block).build() - } - - var hoverEvent: HoverEvent? - get() = style.hoverEvent - set(hoverEvent) - { - style = style.withHoverEvent(hoverEvent) - } - - fun hoverEvent(block: HoverEventBuilder.() -> Unit) - { - hoverEvent = HoverEventBuilder().apply(block).build() - } - - var insertion: String? - get() = style.insertion - set(insertion) - { - style = style.withInsertion(insertion) - } - - var font: ResourceLocation? - get() = style.font - set(font) - { - style = style.withFont(font) - } -} - -@ComponentBuilderDsl -class ClickEventBuilder @PublishedApi internal constructor() -{ - private lateinit var action: ClickEvent.Action - private lateinit var value: String - - fun build(): ClickEvent - { - return ClickEvent(action, value) - } - - fun openUrl(value: String) - { - this.action = ClickEvent.Action.OPEN_URL - this.value = value - } - - fun openFile(value: String) - { - this.action = ClickEvent.Action.OPEN_FILE - this.value = value - } - - fun runCommand(value: String) - { - this.action = ClickEvent.Action.RUN_COMMAND - this.value = value - } - - fun suggestCommand(value: String) - { - this.action = ClickEvent.Action.SUGGEST_COMMAND - this.value = value - } - - fun changePage(value: String) - { - this.action = ClickEvent.Action.CHANGE_PAGE - this.value = value - } - - fun copyToClipboard(value: String) - { - this.action = ClickEvent.Action.COPY_TO_CLIPBOARD - this.value = value - } -} - -@ComponentBuilderDsl -class HoverEventBuilder @PublishedApi internal constructor() -{ - private lateinit var action: HoverEvent.Action - private lateinit var value: Any - - fun build(): HoverEvent - { - @Suppress("UNCHECKED_CAST") - return HoverEvent(action as HoverEvent.Action, value) - } - - fun text(block: ComponentBuilder.() -> Unit) - { - action = HoverEvent.Action.SHOW_TEXT - value = buildComponent(block) - } - - fun item(stack: ItemStack) - { - action = HoverEvent.Action.SHOW_ITEM - value = HoverEvent.ItemStackInfo(stack) - } - - fun entity(type: EntityType<*>, uuid: UUID, name: Component? = null, block: (ComponentBuilder.() -> Unit)? = null) - { - action = HoverEvent.Action.SHOW_ENTITY - value = HoverEvent.EntityTooltipInfo(type, uuid, name ?: block?.let { ComponentBuilder().apply(it).build() }) - } -} - -inline operator fun Component.invoke( - builderAction: ComponentBuilder.() -> Unit -): Component -{ - return ComponentBuilder(copy()).apply(builderAction).build() -} - -inline fun Player.sendSystemMessage( - builderAction: ComponentBuilder.() -> Unit -) -{ - contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } - sendSystemMessage(buildComponent(builderAction)) -} - -inline fun buildComponent( - builderAction: ComponentBuilder.() -> Unit -): Component -{ - contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } - return ComponentBuilder().apply(builderAction).build() -} - -inline fun buildStyle( - builderAction: StyleBuilder.() -> Unit -): Style -{ - contract { callsInPlace(builderAction, InvocationKind.EXACTLY_ONCE) } - return StyleBuilder().apply(builderAction).build() -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Env.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Env.kt deleted file mode 100644 index a13bb2155..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Env.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.kernelpanicsoft.archie.util - -import dev.architectury.platform.Platform -import dev.architectury.utils.Env -import dev.architectury.utils.EnvExecutor -import dev.architectury.utils.GameInstance -import net.minecraft.client.Minecraft -import net.minecraft.server.MinecraftServer -import java.util.Optional -import java.util.function.Supplier - -/** - * Runs [client] on the physical client and [server] on a dedicated server, returning whichever - * ran. Only the branch matching the current [Env] is ever class-loaded, so [client] can safely - * reference client-only classes even when this is called from common code. - */ -inline fun foldEnv(crossinline client: () -> T, crossinline server: () -> T): T = EnvExecutor.getEnvSpecific({ Supplier { - client() -}}, { Supplier { - server() -}}) - -/** - * Runs [client] and returns its result, only on the physical client; returns [Optional.empty] - * on a dedicated server without ever class-loading [client]. - */ -inline fun onClient(crossinline client: () -> T): Optional = EnvExecutor.getInEnv(Env.CLIENT) { Supplier { client() } } - -/** - * Runs [server] and returns its result, only on a dedicated server; returns [Optional.empty] - * on the physical client without ever class-loading [server]. - */ -inline fun onServer(crossinline server: () -> T): Optional = EnvExecutor.getInEnv(Env.SERVER) { Supplier { server() } } - -inline val isClient: Boolean get() = Platform.getEnvironment() == Env.CLIENT -inline val isServer: Boolean get() = Platform.getEnvironment() == Env.SERVER - -inline val minecraftClient: Minecraft get() = GameInstance.getClient() -inline val minecraftServer: MinecraftServer? get() = GameInstance.getServer() \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/MutableEntry.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/MutableEntry.kt deleted file mode 100644 index a01a31754..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/MutableEntry.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.kernelpanicsoft.archie.util - -/** A [Map.Entry] whose [key] and [value] can be reassigned, unlike the standard read-only entry. */ -data class MutableEntry( - override var key: K, - override var value: V -) : Map.Entry - -/** Converts a [Pair] into a [MutableEntry]. */ -fun Pair.toMutableEntry(): MutableEntry = MutableEntry(first, second) -/** Copies a [Map.Entry] into a standalone, mutable [MutableEntry]. */ -fun Map.Entry.toMutableEntry(): MutableEntry = MutableEntry(key, value) diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Properties.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Properties.kt deleted file mode 100644 index e2c07fea1..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Properties.kt +++ /dev/null @@ -1,35 +0,0 @@ -package net.kernelpanicsoft.archie.util - -import dev.architectury.extensions.injected.InjectedItemPropertiesExtension -import dev.architectury.registry.registries.DeferredSupplier -import net.minecraft.resources.ResourceKey -import net.minecraft.world.item.CreativeModeTab -import net.minecraft.world.item.Item -import net.minecraft.world.level.block.state.BlockBehaviour - -/** - * Builds a [BlockBehaviour.Properties] via [block], optionally starting from a full copy of - * [parent]'s properties instead of the defaults. - */ -fun blockProperties(parent: BlockBehaviour? = null, block: BlockBehaviour.Properties.() -> Unit): BlockBehaviour.Properties -{ - return (parent?.let { BlockBehaviour.Properties.ofFullCopy(it) } ?: BlockBehaviour.Properties.of()).apply(block) -} - -/** Builds an [Item.Properties] via [block]. */ -fun itemProperties(block: Item.Properties.() -> Unit): Item.Properties -{ - return Item.Properties().apply(block) -} - -/** Assigns [tab] as this item's creative tab, via Architectury's injected item-properties extension. */ -@Suppress("UnstableApiUsage") -fun Item.Properties.tab(tab: CreativeModeTab): Item.Properties = (this as InjectedItemPropertiesExtension).`arch$tab`(tab) - -/** Assigns [tab] as this item's creative tab, via Architectury's injected item-properties extension. */ -@Suppress("UnstableApiUsage") -fun Item.Properties.tab(tab: DeferredSupplier): Item.Properties = (this as InjectedItemPropertiesExtension).`arch$tab`(tab) - -/** Assigns [tab] as this item's creative tab, via Architectury's injected item-properties extension. */ -@Suppress("UnstableApiUsage") -fun Item.Properties.tab(tab: ResourceKey): Item.Properties = (this as InjectedItemPropertiesExtension).`arch$tab`(tab) \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Reflect.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Reflect.kt deleted file mode 100644 index 3d2f3601e..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Reflect.kt +++ /dev/null @@ -1,39 +0,0 @@ -package net.kernelpanicsoft.archie.util - -/** Extension form of [getReflection]; reads [field] (searching up the class hierarchy) from this instance. */ -@JvmName("getReflectionExtension") -inline fun T.getReflection(field: String): R = getReflection(this, field) -/** Extension form of [setReflection]; writes [field] (searching up the class hierarchy) on this instance. */ -@JvmName("setReflectionExtension") -inline fun T.setReflection(field: String, value: R) = setReflection(this, field, value) - -/** - * Reads a private/inaccessible declared field named [field] off [instance] via reflection, - * searching [T] and its superclasses. - * - * @throws NoSuchFieldException if no field named [field] is found anywhere in the hierarchy. - */ -inline fun getReflection(instance: T, field: String): R -{ - val f = generateSequence((instance?.javaClass ?: T::class.java) as Class<*>) { it.superclass } - .firstNotNullOfOrNull { clazz -> runCatching { clazz.getDeclaredField(field) }.getOrNull() } - ?: throw NoSuchFieldException(field) - f.isAccessible = true - @Suppress("UNCHECKED_CAST") - return f.get(instance) as R -} - -/** - * Writes [value] to a private/inaccessible declared field named [field] on [instance] via - * reflection, searching [T] and its superclasses. - * - * @throws NoSuchFieldException if no field named [field] is found anywhere in the hierarchy. - */ -inline fun setReflection(instance: T, field: String, value: R) -{ - val f = generateSequence((instance?.javaClass ?: T::class.java) as Class<*>) { it.superclass } - .firstNotNullOfOrNull { clazz -> runCatching { clazz.getDeclaredField(field) }.getOrNull() } - ?: throw NoSuchFieldException(field) - f.isAccessible = true - f.set(instance, value) -} \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt deleted file mode 100644 index a188af77f..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.kernelpanicsoft.archie.util - -import dev.architectury.platform.Mod -import net.kernelpanicsoft.archie.Archie -import net.minecraft.resources.ResourceLocation - -/** Builds a [ResourceLocation] with `this` as the namespace and [other] as the path, e.g. `"mymod" % "my_item"`. */ -operator fun String.rem(other: String): ResourceLocation = ResourceLocation.fromNamespaceAndPath(this, other) -/** Builds a [ResourceLocation] namespaced under this [Mod]'s id, with [other] as the path, e.g. `MyMod.MOD % "my_item"`. */ -operator fun Mod.rem(other: String): ResourceLocation = ResourceLocation.fromNamespaceAndPath(this.modId, other) -/** Builds a [ResourceLocation] namespaced under [Archie.MOD_ID], with [other] as the path, e.g. `Archie % "main"`. */ -operator fun Archie.rem(other: String): ResourceLocation = ResourceLocation.fromNamespaceAndPath(MOD_ID, other) - -/** Appends `/[other]` to this location's path. */ -operator fun ResourceLocation.div(other: String): ResourceLocation = withSuffix("/$other") -/** Appends `/` plus [other]'s path (namespace of [other] is ignored) to this location's path. */ -operator fun ResourceLocation.div(other: ResourceLocation): ResourceLocation = withSuffix("/${other.path}") -/** Prepends `this/` to [other]'s path, keeping [other]'s namespace. */ -operator fun String.div(other: ResourceLocation): ResourceLocation = other.withPrefix("$this/") \ No newline at end of file diff --git a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Tile.kt b/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Tile.kt deleted file mode 100644 index 108bdd20b..000000000 --- a/Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Tile.kt +++ /dev/null @@ -1,18 +0,0 @@ -package net.kernelpanicsoft.archie.util - -import com.mojang.datafixers.types.templates.Const -import com.mojang.serialization.Codec -import net.minecraft.world.level.block.Block -import net.minecraft.world.level.block.entity.BlockEntity -import net.minecraft.world.level.block.entity.BlockEntityType -import net.minecraft.world.level.block.entity.BlockEntityType.BlockEntitySupplier - -/** - * Builds a [BlockEntityType] for [factory], valid for the set of [Block]s declared via - * [builder] (e.g. `{ add(MyBlocks.MY_BLOCK.get()) }`). - */ -fun blockEntityType(factory: BlockEntitySupplier, builder: MutableList.() -> Unit): BlockEntityType -{ - return BlockEntityType.Builder.of(factory, *buildList(builder).toTypedArray()) - .build(Const.PrimitiveType(Codec.unit(Unit))) -} \ No newline at end of file diff --git a/Archie/common/src/main/mixin/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenDepthMixin.java b/Archie/common/src/main/mixin/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenDepthMixin.java deleted file mode 100644 index 34adb0021..000000000 --- a/Archie/common/src/main/mixin/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenDepthMixin.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.client.gui; - -import com.mojang.blaze3d.systems.RenderSystem; -import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthProvider; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; -import org.lwjgl.opengl.GL11; -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(AbstractContainerScreen.class) -public abstract class AbstractContainerScreenDepthMixin -{ - @Inject(method = "render", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;disableDepthTest()V", shift = At.Shift.AFTER)) - private void archie$restoreDepth(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick, CallbackInfo ci) - { - if (this instanceof SlotLayerDepthProvider) - { - RenderSystem.enableDepthTest(); - RenderSystem.depthMask(true); - RenderSystem.depthFunc(GL11.GL_LEQUAL); - } - } -} - diff --git a/Archie/common/src/main/mixin/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java b/Archie/common/src/main/mixin/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java deleted file mode 100644 index 14c7664d7..000000000 --- a/Archie/common/src/main/mixin/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java +++ /dev/null @@ -1,112 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.client.gui; - -import net.kernelpanicsoft.archie.Archie; -import net.kernelpanicsoft.archie.gui.access.SlotHighlightClipProvider; -import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthContext; -import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthProvider; -import net.kernelpanicsoft.archie.gui.layout.IntRect; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; -import net.minecraft.client.gui.screens.inventory.MenuAccess; -import net.minecraft.network.chat.Component; -import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.inventory.Slot; -import net.minecraft.world.item.ItemStack; -import org.spongepowered.asm.mixin.Debug; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.ModifyArgs; -import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.invoke.arg.Args; - -@Debug(export = true) -@Mixin(AbstractContainerScreen.class) -public abstract class AbstractContainerScreenMixin extends Screen implements MenuAccess -{ - @Unique - private Float archie$slotDepthOverride; - - protected AbstractContainerScreenMixin(Component title) - { - super(title); - } - - @ModifyArgs(method = "renderSlot", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;translate(FFF)V")) - private void archie$adjustSlotLayer(Args args, GuiGraphics guiGraphics, Slot slot) - { - float originalZ = args.get(2); - float adjustedZ = originalZ; - archie$slotDepthOverride = null; - if (this instanceof SlotLayerDepthProvider provider) - { - Float custom = provider.slotRenderLayerOffset(slot); - archie$slotDepthOverride = custom; - if (custom != null) - { - adjustedZ = custom; - Archie.LOGGER.debug("Adjusting slot layer depth for {} to {}", slot, custom); - } - } - args.set(2, adjustedZ); - } - - @Redirect(method = "renderSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;renderItem(Lnet/minecraft/world/item/ItemStack;III)V")) - private void archie$wrapSlotItemRender(GuiGraphics guiGraphics, ItemStack stack, int x, int y, int seed) - { - boolean pushed = false; - if (archie$slotDepthOverride != null) - { - SlotLayerDepthContext.push(archie$slotDepthOverride); - pushed = true; - } - try - { - guiGraphics.renderItem(stack, x, y, seed); - } - finally - { - if (pushed) - { - SlotLayerDepthContext.pop(); - } - archie$slotDepthOverride = null; - } - } - - /** - * Vanilla's per-slot hover highlight is drawn via a static helper that only takes the - * slot's raw x/y/blitOffset - there's no per-slot instance override point to clip it the - * way {@link #archie$adjustSlotLayer} clips the item icon, so this redirects the call - * site directly instead. - */ - @Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;renderSlotHighlight(Lnet/minecraft/client/gui/GuiGraphics;III)V")) - private void archie$clipSlotHighlight(GuiGraphics guiGraphics, int x, int y, int blitOffset) - { - IntRect clip = null; - if (this instanceof SlotHighlightClipProvider provider) - { - clip = provider.slotHighlightClipRect(x, y); - if (clip == null) - { - return; - } - } - if (clip != null) - { - guiGraphics.enableScissor(clip.getMinX(), clip.getMinY(), clip.getMaxX(), clip.getMaxY()); - } - try - { - AbstractContainerScreen.renderSlotHighlight(guiGraphics, x, y, blitOffset); - } - finally - { - if (clip != null) - { - guiGraphics.disableScissor(); - } - } - } -} diff --git a/Archie/common/src/main/mixin/net/kernelpanicsoft/archie/mixin/client/gui/GuiGraphicsMixin.java b/Archie/common/src/main/mixin/net/kernelpanicsoft/archie/mixin/client/gui/GuiGraphicsMixin.java deleted file mode 100644 index d054a103d..000000000 --- a/Archie/common/src/main/mixin/net/kernelpanicsoft/archie/mixin/client/gui/GuiGraphicsMixin.java +++ /dev/null @@ -1,40 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.client.gui; - -import net.kernelpanicsoft.archie.Archie; -import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthContext; -import net.minecraft.client.gui.GuiGraphics; -import org.spongepowered.asm.mixin.Debug; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.ModifyArgs; -import org.spongepowered.asm.mixin.injection.invoke.arg.Args; - -@Debug(export = true) -@Mixin(GuiGraphics.class) -public abstract class GuiGraphicsMixin -{ - @Unique - private static final float ITEM_TRANSLATE_Z = 150.0F; - - @ModifyArgs( - method = "renderItem(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;IIII)V", - at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;translate(FFF)V") - ) - private void archie$flattenSlotItemDepth(Args args) - { - if (!SlotLayerDepthContext.isActive()) - { - return; - } - float originalZ = args.get(2); - float adjusted = originalZ - ITEM_TRANSLATE_Z; - Archie.LOGGER.debug( - "Slot depth context active: target={}, translate={} -> {}", - SlotLayerDepthContext.currentDepth(), - originalZ, - adjusted - ); - args.set(2, adjusted); - } -} diff --git a/Archie/common/src/main/resources/archie-common.mixins.json b/Archie/common/src/main/resources/archie-common.mixins.json deleted file mode 100644 index 04e18058b..000000000 --- a/Archie/common/src/main/resources/archie-common.mixins.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "required": true, - "package": "net.kernelpanicsoft.archie.mixin", - "compatibilityLevel": "JAVA_17", - "minVersion": "0.8", - "client": [ - "client.gui.AbstractContainerScreenDepthMixin", - "client.gui.AbstractContainerScreenMixin", - "client.gui.GuiGraphicsMixin" - ], - "mixins": [ - ], - "injectors": { - "defaultRequire": 1 - } -} \ No newline at end of file diff --git a/Archie/common/src/main/resources/archie.accesswidener b/Archie/common/src/main/resources/archie.accesswidener deleted file mode 100644 index 72a381745..000000000 --- a/Archie/common/src/main/resources/archie.accesswidener +++ /dev/null @@ -1,335 +0,0 @@ -accessWidener v2 named -accessible field net/minecraft/data/DataGenerator vanillaPackOutput Lnet/minecraft/data/PackOutput; -mutable field net/minecraft/data/DataGenerator vanillaPackOutput Lnet/minecraft/data/PackOutput; -accessible field net/minecraft/data/recipes/RecipeProvider recipePathProvider Lnet/minecraft/data/PackOutput$PathProvider; -accessible field net/minecraft/data/recipes/RecipeProvider advancementPathProvider Lnet/minecraft/data/PackOutput$PathProvider; -extendable method net/minecraft/data/recipes/RecipeProvider run (Lnet/minecraft/data/CachedOutput;)Ljava/util/concurrent/CompletableFuture; -accessible field net/minecraft/data/tags/TagsProvider$TagAppender builder Lnet/minecraft/tags/TagBuilder; -extendable method net/minecraft/data/tags/TagsProvider$TagAppender add (Lnet/minecraft/resources/ResourceKey;)Lnet/minecraft/data/tags/TagsProvider$TagAppender; -extendable method net/minecraft/data/tags/TagsProvider$TagAppender add ([Lnet/minecraft/resources/ResourceKey;)Lnet/minecraft/data/tags/TagsProvider$TagAppender; -accessible field net/minecraft/data/tags/TagsProvider builders Ljava/util/Map; -accessible field net/minecraft/data/loot/BlockLootSubProvider map Ljava/util/Map; -extendable method net/minecraft/tags/TagEntry (Lnet/minecraft/resources/ResourceLocation;ZZ)V -accessible field net/minecraft/tags/TagEntry id Lnet/minecraft/resources/ResourceLocation; -accessible field net/minecraft/tags/TagEntry tag Z -accessible field net/minecraft/tags/TagEntry required Z -extendable method net/minecraft/data/PackOutput$PathProvider (Lnet/minecraft/data/PackOutput;Lnet/minecraft/data/PackOutput$Target;Ljava/lang/String;)V -accessible field net/minecraft/data/PackOutput$PathProvider root Ljava/nio/file/Path; -accessible field net/minecraft/data/PackOutput$PathProvider kind Ljava/lang/String; -accessible method net/minecraft/data/DataGenerator$PackGenerator (Lnet/minecraft/data/DataGenerator;ZLjava/lang/String;Lnet/minecraft/data/PackOutput;)V -accessible field net/minecraft/data/registries/VanillaRegistries BUILDER Lnet/minecraft/core/RegistrySetBuilder; -accessible method net/minecraft/data/registries/VanillaRegistries validateThatAllBiomeFeaturesHaveBiomeFilter (Lnet/minecraft/core/HolderLookup$Provider;)V -accessible field net/minecraft/core/RegistrySetBuilder entries Ljava/util/List; -accessible class net/minecraft/core/RegistrySetBuilder$RegistryStub -accessible field net/minecraft/world/level/storage/loot/parameters/LootContextParamSets REGISTRY Lcom/google/common/collect/BiMap; -accessible class net/minecraft/client/renderer/block/model/ItemTransform$Deserializer -accessible field net/minecraft/client/renderer/block/model/ItemTransform$Deserializer DEFAULT_ROTATION Lorg/joml/Vector3f; -accessible field net/minecraft/client/renderer/block/model/ItemTransform$Deserializer DEFAULT_TRANSLATION Lorg/joml/Vector3f; -accessible field net/minecraft/client/renderer/block/model/ItemTransform$Deserializer DEFAULT_SCALE Lorg/joml/Vector3f; -accessible class net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager$Source -accessible method net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager$Source (Ljava/util/function/Function;Ljava/util/function/Supplier;)V -transitive-accessible field net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager sources Ljava/util/List; -transitive-mutable field net/minecraft/world/level/levelgen/structure/templatesystem/StructureTemplateManager sources Ljava/util/List; -extendable class net/minecraft/world/item/crafting/Ingredient -extendable method net/minecraft/world/item/crafting/Ingredient (Ljava/util/stream/Stream;)V -accessible method net/minecraft/client/gui/screens/MenuScreens register (Lnet/minecraft/world/inventory/MenuType;Lnet/minecraft/client/gui/screens/MenuScreens$ScreenConstructor;)V -extendable class net/minecraft/client/renderer/block/model/BlockElementFace -mutable field net/minecraft/world/inventory/Slot x I -mutable field net/minecraft/world/inventory/Slot y I -transitive-accessible field net/minecraft/world/inventory/AbstractContainerMenu remoteSlots Lnet/minecraft/core/NonNullList; -transitive-accessible field net/minecraft/world/inventory/AbstractContainerMenu lastSlots Lnet/minecraft/core/NonNullList; -accessible field net/minecraft/resources/DelegatingOps delegate Lcom/mojang/serialization/DynamicOps; -transitive-accessible method net/minecraft/data/BlockFamilies familyBuilder (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/data/BlockFamily$Builder; -transitive-accessible field net/minecraft/data/models/BlockModelGenerators blockStateOutput Ljava/util/function/Consumer; -transitive-accessible field net/minecraft/data/models/BlockModelGenerators modelOutput Ljava/util/function/BiConsumer; -transitive-accessible field net/minecraft/data/models/ItemModelGenerators output Ljava/util/function/BiConsumer; -transitive-accessible method net/minecraft/data/models/model/TextureSlot create (Ljava/lang/String;)Lnet/minecraft/data/models/model/TextureSlot; -transitive-accessible method net/minecraft/data/models/model/TextureSlot create (Ljava/lang/String;Lnet/minecraft/data/models/model/TextureSlot;)Lnet/minecraft/data/models/model/TextureSlot; -transitive-extendable method net/minecraft/data/tags/TagsProvider$TagAppender add ([Lnet/minecraft/resources/ResourceKey;)Lnet/minecraft/data/tags/TagsProvider$TagAppender; -transitive-accessible method net/minecraft/data/models/model/TexturedModel createDefault (Ljava/util/function/Function;Lnet/minecraft/data/models/model/ModelTemplate;)Lnet/minecraft/data/models/model/TexturedModel$Provider; -transitive-accessible class net/minecraft/data/models/BlockModelGenerators$TintState -transitive-accessible class net/minecraft/data/models/BlockModelGenerators$BlockFamilyProvider -transitive-accessible class net/minecraft/data/models/BlockModelGenerators$WoodProvider -transitive-accessible class net/minecraft/data/models/BlockModelGenerators$BlockEntityModelGenerator -#transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider HAS_SILK_TOUCH Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder; -#transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider HAS_NO_SILK_TOUCH Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder; -transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider HAS_SHEARS Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder; -#transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider HAS_SHEARS_OR_SILK_TOUCH Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder; -#transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider HAS_NO_SHEARS_OR_SILK_TOUCH Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder; -transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider NORMAL_LEAVES_SAPLING_CHANCES [F -transitive-accessible field net/minecraft/data/loot/BlockLootSubProvider NORMAL_LEAVES_STICK_CHANCES [F -transitive-accessible method net/minecraft/data/recipes/RecipeProvider buildAdvancement (Lnet/minecraft/data/CachedOutput;Lnet/minecraft/core/HolderLookup$Provider;Lnet/minecraft/advancements/AdvancementHolder;)Ljava/util/concurrent/CompletableFuture; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider buildRecipes (Lnet/minecraft/data/recipes/RecipeOutput;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider generateForEnabledBlockFamilies (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/flag/FeatureFlagSet;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider oneToOneConversionRecipe (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;Ljava/lang/String;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider oneToOneConversionRecipe (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;Ljava/lang/String;I)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider oreSmelting (Lnet/minecraft/data/recipes/RecipeOutput;Ljava/util/List;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;FILjava/lang/String;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider oreBlasting (Lnet/minecraft/data/recipes/RecipeOutput;Ljava/util/List;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;FILjava/lang/String;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider oreCooking (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/item/crafting/RecipeSerializer;Lnet/minecraft/world/item/crafting/AbstractCookingRecipe$Factory;Ljava/util/List;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;FILjava/lang/String;Ljava/lang/String;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider netheriteSmithing (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/item/Item;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/item/Item;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider trimSmithing (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/item/Item;Lnet/minecraft/resources/ResourceLocation;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider twoByTwoPacker (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider threeByThreePacker (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;Ljava/lang/String;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider threeByThreePacker (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider planksFromLog (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/tags/TagKey;I)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider planksFromLogs (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/tags/TagKey;I)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider woodFromLogs (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider woodenBoat (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider chestBoat (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider buttonBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider doorBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider fenceBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider fenceGateBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider pressurePlate (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider pressurePlateBuilder (Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider slab (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider slabBuilder (Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider stairBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider trapdoorBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider signBuilder (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider hangingSign (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider colorBlockWithDye (Lnet/minecraft/data/recipes/RecipeOutput;Ljava/util/List;Ljava/util/List;Ljava/lang/String;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider carpet (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider bedFromPlanksAndWool (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider banner (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider stainedGlassFromGlassAndDye (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider stainedGlassPaneFromStainedGlass (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider stainedGlassPaneFromGlassPaneAndDye (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider coloredTerracottaFromTerracottaAndDye (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider concretePowder (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider candle (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider wall (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider wallBuilder (Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider polished (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider polishedBuilder (Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/RecipeBuilder; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider cut (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider cutBuilder (Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/ShapedRecipeBuilder; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider chiseled (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider mosaicBuilder (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider chiseledBuilder (Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/item/crafting/Ingredient;)Lnet/minecraft/data/recipes/ShapedRecipeBuilder; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider stonecutterResultFromBase (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider stonecutterResultFromBase (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;I)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider smeltingResultFromBase (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider nineBlockStorageRecipes (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider nineBlockStorageRecipesWithCustomPacking (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Ljava/lang/String;Ljava/lang/String;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider nineBlockStorageRecipesRecipesWithCustomUnpacking (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Ljava/lang/String;Ljava/lang/String;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider nineBlockStorageRecipes (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/data/recipes/RecipeCategory;Lnet/minecraft/world/level/ItemLike;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider copySmithingTemplate (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/tags/TagKey;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider copySmithingTemplate (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider cookRecipes (Lnet/minecraft/data/recipes/RecipeOutput;Ljava/lang/String;Lnet/minecraft/world/item/crafting/RecipeSerializer;Lnet/minecraft/world/item/crafting/AbstractCookingRecipe$Factory;I)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider simpleCookingRecipe (Lnet/minecraft/data/recipes/RecipeOutput;Ljava/lang/String;Lnet/minecraft/world/item/crafting/RecipeSerializer;Lnet/minecraft/world/item/crafting/AbstractCookingRecipe$Factory;ILnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;F)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider waxRecipes (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/flag/FeatureFlagSet;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider grate (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider copperBulb (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider generateRecipes (Lnet/minecraft/data/recipes/RecipeOutput;Lnet/minecraft/data/BlockFamily;Lnet/minecraft/world/flag/FeatureFlagSet;)V -transitive-accessible method net/minecraft/data/recipes/RecipeProvider getBaseBlock (Lnet/minecraft/data/BlockFamily;Lnet/minecraft/data/BlockFamily$Variant;)Lnet/minecraft/world/level/block/Block; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider insideOf (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/advancements/Criterion; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider has (Lnet/minecraft/advancements/critereon/MinMaxBounds$Ints;Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/advancements/Criterion; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider has (Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/advancements/Criterion; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider has (Lnet/minecraft/tags/TagKey;)Lnet/minecraft/advancements/Criterion; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider inventoryTrigger ([Lnet/minecraft/advancements/critereon/ItemPredicate$Builder;)Lnet/minecraft/advancements/Criterion; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider inventoryTrigger ([Lnet/minecraft/advancements/critereon/ItemPredicate;)Lnet/minecraft/advancements/Criterion; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider getHasName (Lnet/minecraft/world/level/ItemLike;)Ljava/lang/String; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider getItemName (Lnet/minecraft/world/level/ItemLike;)Ljava/lang/String; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider getSimpleRecipeName (Lnet/minecraft/world/level/ItemLike;)Ljava/lang/String; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider getConversionRecipeName (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/ItemLike;)Ljava/lang/String; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider getSmeltingRecipeName (Lnet/minecraft/world/level/ItemLike;)Ljava/lang/String; -transitive-accessible method net/minecraft/data/recipes/RecipeProvider getBlastingRecipeName (Lnet/minecraft/world/level/ItemLike;)Ljava/lang/String; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createMirroredCubeGenerator (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/data/models/model/TextureMapping;Ljava/util/function/BiConsumer;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNorthWestMirroredCubeGenerator (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/data/models/model/TextureMapping;Ljava/util/function/BiConsumer;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createMirroredColumnGenerator (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/data/models/model/TextureMapping;Ljava/util/function/BiConsumer;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators skipAutoItemBlock (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators delegateItemModel (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators delegateItemModel (Lnet/minecraft/world/item/Item;Lnet/minecraft/resources/ResourceLocation;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSimpleFlatItemModel (Lnet/minecraft/world/item/Item;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSimpleFlatItemModel (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSimpleFlatItemModel (Lnet/minecraft/world/level/block/Block;Ljava/lang/String;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createHorizontalFacingDispatch ()Lnet/minecraft/data/models/blockstates/PropertyDispatch; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createHorizontalFacingDispatchAlt ()Lnet/minecraft/data/models/blockstates/PropertyDispatch; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTorchHorizontalDispatch ()Lnet/minecraft/data/models/blockstates/PropertyDispatch; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFacingDispatch ()Lnet/minecraft/data/models/blockstates/PropertyDispatch; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedVariant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/MultiVariantGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedVariants (Lnet/minecraft/resources/ResourceLocation;)[Lnet/minecraft/data/models/blockstates/Variant; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedVariant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/MultiVariantGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createBooleanModelDispatch (Lnet/minecraft/world/level/block/state/properties/BooleanProperty;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/PropertyDispatch; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedMirroredVariantBlock (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedVariantBlock (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createBrushableBlock (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createButton (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators configureDoorHalf (Lnet/minecraft/data/models/blockstates/PropertyDispatch$C4;Lnet/minecraft/world/level/block/state/properties/DoubleBlockHalf;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/PropertyDispatch$C4; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createDoor (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCustomFence (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFence (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createWall (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFenceGate (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Z)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createStairs (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createOrientableTrapdoor (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTrapdoor (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSimpleBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/MultiVariantGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedPillar ()Lnet/minecraft/data/models/blockstates/PropertyDispatch; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPillarBlockUVLocked (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/model/TextureMapping;Ljava/util/function/BiConsumer;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAxisAlignedPillarBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAxisAlignedPillarBlockCustomModel (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createHorizontallyRotatedBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/model/TexturedModel$Provider;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedPillarWithHorizontalVariant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatedPillarWithHorizontalVariant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/model/TexturedModel$Provider;Lnet/minecraft/data/models/model/TexturedModel$Provider;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSuffixedVariant (Lnet/minecraft/world/level/block/Block;Ljava/lang/String;Lnet/minecraft/data/models/model/ModelTemplate;Ljava/util/function/Function;)Lnet/minecraft/resources/ResourceLocation; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPressurePlate (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSlab (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTrivialBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/model/TextureMapping;Lnet/minecraft/data/models/model/ModelTemplate;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators family (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/data/models/BlockModelGenerators$BlockFamilyProvider; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createDoor (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators copyDoorModel (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createOrientableTrapdoor (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTrapdoor (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators copyTrapdoorModel (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators woodProvider (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/data/models/BlockModelGenerators$WoodProvider; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNonTemplateModelBlock (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNonTemplateModelBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCrossBlockWithDefaultItem (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCrossBlockWithDefaultItem (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;Lnet/minecraft/data/models/model/TextureMapping;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCrossBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCrossBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;Lnet/minecraft/data/models/model/TextureMapping;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCrossBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;Lnet/minecraft/world/level/block/state/properties/Property;[I)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPlant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCoralFans (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createStems (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCoral (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createDoublePlant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createDoubleBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPassiveRail (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createActiveRail (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators blockEntityModels (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/data/models/BlockModelGenerators$BlockEntityModelGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators blockEntityModels (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/data/models/BlockModelGenerators$BlockEntityModelGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAirLikeBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/item/Item;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAirLikeBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFullAndCarpetBlocks (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFlowerBed (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createColoredBlockWithRandomRotations (Lnet/minecraft/data/models/model/TexturedModel$Provider;[Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createColoredBlockWithStateRotations (Lnet/minecraft/data/models/model/TexturedModel$Provider;[Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createGlassBlocks (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCommandBlock (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAnvil (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createBambooModels (I)Ljava/util/List; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createColumnWithFacing ()Lnet/minecraft/data/models/blockstates/PropertyDispatch; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createEmptyOrFullDispatch (Lnet/minecraft/world/level/block/state/properties/Property;Ljava/lang/Comparable;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/PropertyDispatch; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createBeeNest (Lnet/minecraft/world/level/block/Block;Ljava/util/function/Function;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCropBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/state/properties/Property;[I)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFurnace (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/model/TexturedModel$Provider;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCampfires ([Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAzalea (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPottedAzalea (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createMushroomBlock (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCraftingTableLike (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Ljava/util/function/BiFunction;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPumpkinVariant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/model/TextureMapping;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createDispenserBlock (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCopperBulb (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCopperBulb (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)Lnet/minecraft/data/models/blockstates/BlockStateGenerator; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators copyCopperBulbModel (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createAmethystCluster (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPointedDripstoneVariant (Lnet/minecraft/core/Direction;Lnet/minecraft/world/level/block/state/properties/DripstoneThickness;)Lnet/minecraft/data/models/blockstates/Variant; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNyliumBlock (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createRotatableColumn (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createFloorFireModels (Lnet/minecraft/world/level/block/Block;)Ljava/util/List; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createSideFireModels (Lnet/minecraft/world/level/block/Block;)Ljava/util/List; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTopFireModels (Lnet/minecraft/world/level/block/Block;)Ljava/util/List; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators wrapModels (Ljava/util/List;Ljava/util/function/UnaryOperator;)Ljava/util/List; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createLantern (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createGrassLikeBlock (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/data/models/blockstates/Variant;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createWeightedPressurePlate (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators copyModel (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNonTemplateHorizontalBlock (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createPistonVariant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/data/models/model/TextureMapping;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNormalTorch (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTurtleEggModel (ILjava/lang/String;Lnet/minecraft/data/models/model/TextureMapping;)Lnet/minecraft/resources/ResourceLocation; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createTurtleEggModel (Ljava/lang/Integer;Ljava/lang/Integer;)Lnet/minecraft/resources/ResourceLocation; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createMultiface (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators addSlotStateAndRotationVariants (Lnet/minecraft/data/models/blockstates/MultiPartGenerator;Lnet/minecraft/data/models/blockstates/Condition$TerminalCondition;Lnet/minecraft/data/models/blockstates/VariantProperties$Rotation;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators addBookSlotModel (Lnet/minecraft/data/models/blockstates/MultiPartGenerator;Lnet/minecraft/data/models/blockstates/Condition$TerminalCondition;Lnet/minecraft/data/models/blockstates/VariantProperties$Rotation;Lnet/minecraft/world/level/block/state/properties/BooleanProperty;Lnet/minecraft/data/models/model/ModelTemplate;Z)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createShulkerBox (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createGrowingPlant (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;Lnet/minecraft/data/models/BlockModelGenerators$TintState;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createBedItem (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createNetherRoots (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/models/BlockModelGenerators applyRotation (Lnet/minecraft/core/FrontAndTop;Lnet/minecraft/data/models/blockstates/Variant;)Lnet/minecraft/data/models/blockstates/Variant; -transitive-accessible method net/minecraft/data/models/BlockModelGenerators createCandleAndCandleCake (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider applyExplosionDecay (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/storage/loot/functions/FunctionUserBuilder;)Lnet/minecraft/world/level/storage/loot/functions/FunctionUserBuilder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider applyExplosionCondition (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/storage/loot/predicates/ConditionUserBuilder;)Lnet/minecraft/world/level/storage/loot/predicates/ConditionUserBuilder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSelfDropDispatchTable (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder;Lnet/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer$Builder;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSilkTouchDispatchTable (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer$Builder;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createShearsDispatchTable (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer$Builder;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSilkTouchOrShearsDispatchTable (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/storage/loot/entries/LootPoolEntryContainer$Builder;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSingleItemTableWithSilkTouch (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSingleItemTable (Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/storage/loot/providers/number/NumberProvider;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSingleItemTableWithSilkTouch (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/ItemLike;Lnet/minecraft/world/level/storage/loot/providers/number/NumberProvider;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSilkTouchOnlyTable (Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createPotFlowerItemTable (Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSlabItemTable (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createSinglePropConditionTable (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/state/properties/Property;Ljava/lang/Comparable;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createNameableBlockEntityTable (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createShulkerBoxDrop (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createCopperOreDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createLapisOreDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createRedstoneOreDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createBannerDrop (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createBeeNestDrop (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createBeeHiveDrop (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createCaveVinesDrop (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createOreDrop (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/item/Item;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createMushroomBlockDrop (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createGrassDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createShearsOnlyDrop (Lnet/minecraft/world/level/ItemLike;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createMultifaceBlockDrops (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createLeavesDrops (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;[F)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createOakLeavesDrops (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;[F)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createMangroveLeavesDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createCropDrops (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/item/Item;Lnet/minecraft/world/item/Item;Lnet/minecraft/world/level/storage/loot/predicates/LootItemCondition$Builder;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createDoublePlantShearsDrop (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createDoublePlantWithSeedDrops (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createCandleDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createPetalsDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createCandleCakeDrops (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider generate ()V -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider addNetherVinesDropTable (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider createDoorTable (Lnet/minecraft/world/level/block/Block;)Lnet/minecraft/world/level/storage/loot/LootTable$Builder; -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider dropPottedContents (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider otherWhenSilkTouch (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider dropOther (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/ItemLike;)V -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider dropWhenSilkTouch (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider dropSelf (Lnet/minecraft/world/level/block/Block;)V -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider add (Lnet/minecraft/world/level/block/Block;Ljava/util/function/Function;)V -transitive-accessible method net/minecraft/data/loot/BlockLootSubProvider add (Lnet/minecraft/world/level/block/Block;Lnet/minecraft/world/level/storage/loot/LootTable$Builder;)V -transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateFlatItem (Lnet/minecraft/world/item/Item;Lnet/minecraft/data/models/model/ModelTemplate;)V -transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateFlatItem (Lnet/minecraft/world/item/Item;Ljava/lang/String;Lnet/minecraft/data/models/model/ModelTemplate;)V -transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateFlatItem (Lnet/minecraft/world/item/Item;Lnet/minecraft/world/item/Item;Lnet/minecraft/data/models/model/ModelTemplate;)V -transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateCompassItem (Lnet/minecraft/world/item/Item;)V -transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateClockItem (Lnet/minecraft/world/item/Item;)V -transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateLayeredItem (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)V -transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateLayeredItem (Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;Lnet/minecraft/resources/ResourceLocation;)V -transitive-accessible method net/minecraft/data/models/ItemModelGenerators getItemModelForTrimMaterial (Lnet/minecraft/resources/ResourceLocation;Ljava/lang/String;)Lnet/minecraft/resources/ResourceLocation; -transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateBaseArmorTrimTemplate (Lnet/minecraft/resources/ResourceLocation;Ljava/util/Map;Lnet/minecraft/core/Holder;)Lcom/google/gson/JsonObject; -transitive-accessible method net/minecraft/data/models/ItemModelGenerators generateArmorTrims (Lnet/minecraft/world/item/ArmorItem;)V -transitive-extendable method net/minecraft/data/metadata/PackMetadataGenerator getName ()Ljava/lang/String; -transitive-extendable method net/minecraft/data/structures/SnbtToNbt getName ()Ljava/lang/String; -transitive-extendable method net/minecraft/data/models/ModelProvider getName ()Ljava/lang/String; -transitive-extendable method net/minecraft/data/structures/NbtToSnbt getName ()Ljava/lang/String; -transitive-extendable method net/minecraft/data/info/BlockListReport getName ()Ljava/lang/String; -transitive-extendable method net/minecraft/data/info/CommandsReport getName ()Ljava/lang/String; -transitive-extendable method net/minecraft/data/registries/RegistriesDatapackGenerator getName ()Ljava/lang/String; -transitive-extendable method net/minecraft/data/info/RegistryDumpReport getName ()Ljava/lang/String; -transitive-extendable method net/minecraft/data/info/BiomeParametersDumpReport getName ()Ljava/lang/String; -transitive-extendable method net/minecraft/data/advancements/AdvancementProvider getName ()Ljava/lang/String; -transitive-extendable method net/minecraft/data/loot/LootTableProvider getName ()Ljava/lang/String; -transitive-extendable method net/minecraft/data/recipes/RecipeProvider getName ()Ljava/lang/String; -transitive-extendable method net/minecraft/data/tags/TagsProvider getName ()Ljava/lang/String; -transitive-accessible field net/minecraft/world/item/CreativeModeTabs BUILDING_BLOCKS Lnet/minecraft/resources/ResourceKey; -transitive-accessible field net/minecraft/world/item/CreativeModeTabs COLORED_BLOCKS Lnet/minecraft/resources/ResourceKey; -transitive-accessible field net/minecraft/world/item/CreativeModeTabs NATURAL_BLOCKS Lnet/minecraft/resources/ResourceKey; -transitive-accessible field net/minecraft/world/item/CreativeModeTabs FUNCTIONAL_BLOCKS Lnet/minecraft/resources/ResourceKey; -transitive-accessible field net/minecraft/world/item/CreativeModeTabs REDSTONE_BLOCKS Lnet/minecraft/resources/ResourceKey; -transitive-accessible field net/minecraft/world/item/CreativeModeTabs TOOLS_AND_UTILITIES Lnet/minecraft/resources/ResourceKey; -transitive-accessible field net/minecraft/world/item/CreativeModeTabs COMBAT Lnet/minecraft/resources/ResourceKey; -transitive-accessible field net/minecraft/world/item/CreativeModeTabs FOOD_AND_DRINKS Lnet/minecraft/resources/ResourceKey; -transitive-accessible field net/minecraft/world/item/CreativeModeTabs INGREDIENTS Lnet/minecraft/resources/ResourceKey; -transitive-accessible field net/minecraft/world/item/CreativeModeTabs SPAWN_EGGS Lnet/minecraft/resources/ResourceKey; -transitive-accessible field net/minecraft/world/item/CreativeModeTabs OP_BLOCKS Lnet/minecraft/resources/ResourceKey; -accessible method net/minecraft/client/gui/screens/Screen removeWidget (Lnet/minecraft/client/gui/components/events/GuiEventListener;)V diff --git a/Archie/common/src/main/resources/archie.common.json b/Archie/common/src/main/resources/archie.common.json deleted file mode 100644 index be85295c1..000000000 --- a/Archie/common/src/main/resources/archie.common.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "accessWidener": "archie.accesswidener" -} \ No newline at end of file diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java.theme.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java.theme.json deleted file mode 100644 index c6f6e3df4..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java.theme.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "variants": ["", "dark"], - "default_variant": "", - "aliases": { - "default": "" - } -} - diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/button.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/button.json deleted file mode 100644 index 6a445ec51..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/button.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/button", - "texture_size": { - "width": 64, - "height": 64 - }, - "width": 64, - "height": 20 - }, - "hovered": { - "texture": "archie:java/button_highlighted" - }, - "disabled": { - "texture": "archie:java/button_disabled" - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/checkbox.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/checkbox.json deleted file mode 100644 index 2cb0de488..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/checkbox.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/checkbox", - "texture_size": { - "width": 20, - "height": 20 - }, - "width": 20, - "height": 20 - }, - "hovered": { - "texture": "archie:java/checkbox_hovered" - }, - "clicked": { - "texture": "archie:java/checkbox_clicked" - }, - "clicked_and_hovered": { - "texture": "archie:java/checkbox_clicked_and_hovered" - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/dark/surface.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/dark/surface.json deleted file mode 100644 index 2b79ea1c4..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/dark/surface.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/surface_dark", - "texture_size": { - "width": 16, - "height": 16 - }, - "width": 16, - "height": 16 - } - }, - "variants": { - "inset": { - "default": { - "texture": "archie:java/surface_inset_dark" - } - } - } -} - diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json deleted file mode 100644 index be64d72bc..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/energy_bar", - "texture_size": { - "width": 32, - "height": 16 - }, - "width": 32, - "height": 16 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json deleted file mode 100644 index 931ca1fe0..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/fluid_tank", - "texture_size": { - "width": 18, - "height": 54 - }, - "width": 18, - "height": 54 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json deleted file mode 100644 index b9ab69729..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/progress_bar", - "texture_size": { - "width": 32, - "height": 16 - }, - "width": 32, - "height": 16 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/radio.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/radio.json deleted file mode 100644 index 7540bd2a6..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/radio.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/radio", - "texture_size": { - "width": 20, - "height": 20 - }, - "width": 20, - "height": 20 - }, - "hovered": { - "texture": "archie:java/radio_hovered" - }, - "clicked": { - "texture": "archie:java/radio_clicked" - }, - "clicked_and_hovered": { - "texture": "archie:java/radio_clicked_and_hovered" - }, - "disabled": { - "texture": "archie:java/radio_disabled" - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/slider.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/slider.json deleted file mode 100644 index dfd75c3ff..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/slider.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/slider", - "texture_size": { - "width": 200, - "height": 20 - }, - "width": 200, - "height": 20 - }, - "hovered": { - "texture": "archie:java/slider_highlighted" - }, - "clicked": { - "texture": "archie:java/slider_highlighted" - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/slider_handle.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/slider_handle.json deleted file mode 100644 index 1b2f9943b..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/slider_handle.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/slider_handle", - "texture_size": { - "width": 8, - "height": 20 - }, - "width": 8, - "height": 20 - }, - "hovered": { - "texture": "archie:java/slider_handle_highlighted" - }, - "clicked": { - "texture": "archie:java/slider_handle_highlighted" - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/slot.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/slot.json deleted file mode 100644 index 90c4dfe0f..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/slot.json +++ /dev/null @@ -1,15 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/slot", - "texture_size": { - "width": 18, - "height": 18 - }, - "width": 18, - "height": 18, - "uWidth": 18, - "vHeight": 18 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/small_checkbox.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/small_checkbox.json deleted file mode 100644 index 310c2af63..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/small_checkbox.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/small_checkbox", - "texture_size": { - "width": 13, - "height": 13 - }, - "width": 13, - "height": 13 - }, - "clicked": { - "texture": "archie:java/small_checkbox_clicked" - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/surface.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/surface.json deleted file mode 100644 index d9dd37b9e..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/surface.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/surface", - "texture_size": { - "width": 16, - "height": 16 - }, - "width": 16, - "height": 16 - } - }, - "variants": { - "inset": { - "default": { - "texture": "archie:java/surface_inset" - } - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/switch_thumb.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/switch_thumb.json deleted file mode 100644 index 035f925c4..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/switch_thumb.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/switch_thumb", - "texture_size": { - "width": 14, - "height": 14 - }, - "width": 14, - "height": 14 - }, - "disabled": { - "texture": "archie:java/switch_thumb_disabled" - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/switch_track.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/switch_track.json deleted file mode 100644 index 43d558043..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/switch_track.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/switch_track", - "texture_size": { - "width": 34, - "height": 18 - }, - "width": 34, - "height": 18 - }, - "hovered": { - "texture": "archie:java/switch_track_hovered" - }, - "clicked": { - "texture": "archie:java/switch_track_clicked" - }, - "clicked_and_hovered": { - "texture": "archie:java/switch_track_clicked_and_hovered" - }, - "disabled": { - "texture": "archie:java/switch_track_disabled" - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/tab_game.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/tab_game.json deleted file mode 100644 index 5cf2141c8..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/tab_game.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/tab_game", - "texture_size": { - "width": 26, - "height": 32 - }, - "width": 26, - "height": 32 - }, - "hovered": { - "texture": "archie:java/tab_game_hovered" - }, - "clicked": { - "texture": "archie:java/tab_game_selected" - }, - "clicked_and_hovered": { - "texture": "archie:java/tab_game_selected_highlighted" - }, - "disabled": { - "texture": "archie:java/tab_game_disabled" - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/tab_menu.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/tab_menu.json deleted file mode 100644 index f005d6b8c..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/tab_menu.json +++ /dev/null @@ -1,25 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/tab_menu", - "texture_size": { - "width": 130, - "height": 24 - }, - "width": 130, - "height": 24 - }, - "hovered": { - "texture": "archie:java/tab_menu_hovered" - }, - "clicked": { - "texture": "archie:java/tab_menu_selected" - }, - "clicked_and_hovered": { - "texture": "archie:java/tab_menu_selected_highlighted" - }, - "disabled": { - "texture": "archie:java/tab_menu_disabled" - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/archie_themes/java/text_field.json b/Archie/common/src/main/resources/assets/archie/archie_themes/java/text_field.json deleted file mode 100644 index 9494a37e4..000000000 --- a/Archie/common/src/main/resources/assets/archie/archie_themes/java/text_field.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "states": { - "default": { - "texture": "archie:java/text_field", - "texture_size": { - "width": 16, - "height": 16 - }, - "width": 16, - "height": 16 - }, - "clicked": { - "texture": "archie:java/text_field_highlighted" - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/atlases/java.json b/Archie/common/src/main/resources/assets/archie/atlases/java.json deleted file mode 100644 index 4f66968a2..000000000 --- a/Archie/common/src/main/resources/assets/archie/atlases/java.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "sources": [ - { - "type": "directory", - "source": "gui/sprites/java", - "prefix": "java/" - } - ] -} diff --git a/Archie/common/src/main/resources/assets/archie/banner.png b/Archie/common/src/main/resources/assets/archie/banner.png deleted file mode 100644 index 50caab899680004e7c27edf8b5577d6705e562ee..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 32385 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 diff --git a/Archie/common/src/main/resources/assets/archie/icon.png b/Archie/common/src/main/resources/assets/archie/icon.png deleted file mode 100644 index cc7c01afdbaf4991edb19b7bb81f6cc3b9a3fee2..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 68643 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 diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png.mcmeta deleted file mode 100644 index 6d45dddfb..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png.mcmeta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 64, - "height": 64, - "border": 3 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png deleted file mode 100644 index 3a59250c0ef543bf15fec8452388a79cb2b50059..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1223 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 diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png.mcmeta deleted file mode 100644 index 6d45dddfb..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png.mcmeta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 64, - "height": 64, - "border": 3 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox.png deleted file mode 100644 index 3aa7ee3bbe4511399249f2935b38e860069d4505..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 408 zcmV;J0cZY+P))l4>UGF8n(RmbBvk|i}^K9+tY)2aLRAyP!<3~}*rJaCcW>iUlTjyPsCC6D1JS;LXw-JC^)V4&$A&^v__fy8 zfMmytL-Tt;xDD*)b}9q8D0S=#4GqgBo0vCf<$X)4=bCbpp|yDKDCWK_DnJ<0m%5r zhyc*GEdW4O5kdeFfr$JY0E7@AA`lTUv#*yi48u2|Ert*P06AymoU3Od@`m#uBG2ct zNJOec17K$4oWab#X27b7aU7iqTL&WYVgrsbLRF!vn5N060N~VPj9_MO7U$!av`X5h z?L1dRsBBNNw?REnIbY17)Vu@~3mB(gIu>_if zIb{=-h|n~Rzc)?u254X@C9KzLZ}Rcj5D_ky3-2>Yj>jWJ1kdNw^?f`Z0007zSF6<(y-jM-&|j&sb62EXa5pq-J4U4{!iAkoKLPI{PQp1 Y3(fyZy5B`4cK`qY07*qoM6N<$g8y98zW@LL diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked_and_hovered.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked_and_hovered.png deleted file mode 100644 index eb01e5b864e163297c2d3173766f87e07b509b0a..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 478 zcmV<40U`d0P)-Tt;x16J>V>!rr+^KJQ*eVyEGV1>iPrX8eAy)B=}YhFdDS-d*fYtbWM+mx zj0^zl^%?-6sz@n;h(JW~4FFO~5D|z7m^s$V7>3~=pf9GB003qNAKagbC>qX#h$5fM zA`z()9e|l(W?<%6b6{1)IF7-Dy#oa=wQ3u4x7y;ntXd~hzQr~75n`jW){c6b$dJ>0d_ndp{g;+VzGDw zIz=->+qUSs4!7G4hr=NX?)Q6uolYl+2wtyO==*#=0RUuXHrGK_W17D2(RE!sZ@1gZ z8Nl0?!!XnnhOo>b_8iO`mWO#9$1uZ(Q~Z6tgO|%Cz(fSoG`;P{9JTC8o~Wv^Nvev~ zYK6Y71J^Py|>pq;~^#5l)&-u<(^w+Q1M~^OLGau3r0?bHeCVprJ0Jqx>01y#SRfq^gq}~C5 zs>0pj?igc~zuWzOe+Q&fRRI8*2@>bMh*ZL}iAdo`KjiKkkO9UR$V`ke%E!PFfwfju z^1(17s$@Q&laNG2z};bHm2BW-W>$x~j>mBnOKLdh#PxdJG~G(>W%50*Y*sgS$69N{ zjx&JFdRA41nH7eIDa%&j7JUpv#5O)X8FeCtT6df=5bYa?Ms4Su9~1F)Y$!B^pL2c< zNOr79L~yxW{uz=@W|qhRYQB1R*>P2^8STya7#k+&x{3(g9mjFJS(w4y@p`>V0ygb~ zAsdjHFtcq#y1rfawN{k~^?}AiA#fF~+802NU#sKDVy+ zy%mvKJu=<~yRX=})NkLz%*5mI*sA;arpT{<0e|rjLx8Pe;lTg^002ovPDHLkV1gK7 Bw9Ehi diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png deleted file mode 100644 index eca5f3cf91876a83f7abd4799ebd03323f077ad1..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 136 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Et!3HGD8EPYel)tBoV@SoEWD!BZpXUu48af*r z8xynFiQLs~yLpm>o6(R_ZkN@AtL^$cJUkZ!Bs>gWH3YKvSS4IxT+Ek{n9vZyb8qte ji&v9o%kii%A68RMSgV%3}$u25n UZ{IoLKgd)DPgg&ebxsLQ00)aIl>h($ diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta deleted file mode 100644 index ea42a1b16..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 18, - "height": 54, - "border": 2 - } - } -} \ No newline at end of file diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png deleted file mode 100644 index 98e37d595bde493d362f6c34553f9933d4a6b226..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 115 zcmeAS@N?(olHy`uVBq!ia0vp^3P3Et!3HGD8EPYel$EE8V@SoEWD!BZpXUu48Zs4h z(-VN8(eTQZD=)G>u8rOFyX~z};H~fWHi@3K4;|7S1_XYc)f%5Rqn)nN2`MFy};O{xBpfGUIdt za(c`9=Ur7b5TEw&o=jQvjcjheGPA?G6(|6>dl$Mp=A7MJRWLJ*F}jAq-Qhh9tE$bv zeNxr7gYVjW)#vwz;heJz2|VZQ>GwLZj^0HC%qpH|@0|3@q{{zx^CdYe9i5ztQ@lpkE6yFPe`4jL5{m-lG4BOIU00000 LNkvXXu0mjf{KVDW diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked.png deleted file mode 100644 index 59a40bda9e8fa429ddf32cec6ac0a1ef8864a289..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 468 zcmV;_0W1EAP)D`rNm zH9vB9+?}-+s)~rP*2-^s@3hv~_ZPeWEACFM^^`#c7-OVAV+^)!1K{y^d?~i? zd*07EDW$x680P7o6K{dueM7Ufo068_$^vCIl zVJM}XCneI2v5Sai#uy`Mj_JiA676$pY!S)X?7e5Bx_grM-kEbg&Hp4E*9z~w6dOw^ zPd`4$Lntb$s@VyxwO7G7=c2dFET5w%<9!q^vWRms-V6Wy3wQ%Nn`29Ocw7(w0000< KMNUMnLSTZ;rqjp( diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked_and_hovered.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked_and_hovered.png deleted file mode 100644 index ab9ab302eedd8e2149bd2c449dace6d9f5b7d5d0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 463 zcmV;=0WkiFP)fGJ063ja0D!d?YOOFc z)LL^>L?9yI?od^T2;4pQj4{xA$Mt%J0)U7hBA}{p_XHOam>JeuFf*ts%q+pyT6xLF z(B1QJDP=oQYfXl#n%_AmYOR=aW)}cH8Ab%wTIjvQ-O+nbhGZck$->N%fQX>A1~Ypw zBr7uG?F9Vp8?}pw1ln5W?PiA78X^Ms`~9Vxm&*kafm-X{yhB zl^Rm~cS}{FswvmTkZl5Z8<^UywNOgg0VzHWIp*rO5|IRoh#ioW$!v`AC!o7yjPcY_ zQaQTyjwvFN(PH3%!=%s{08iQasR-ciIbjJV0FYC|&>yFR!%#}uCxz{D z?0BP@Va}Osj_vUgVSH#xP6}spjDb>0J_uw!#=u(ZA^taDek<^qB_1rLJoNZtP9Qp} zs+ok|`!gWlIcCeu@}HUHI*nr&d?&ds5fQiB?N3+Vtq&|0T|=7ks(1hZ002ovPDHLk FV1g+v%n1Mh diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_disabled.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_disabled.png deleted file mode 100644 index 133e677cf0d39885b131e9acf8aaccf6b34b36c0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 424 zcmV;Z0ayNsP)8{Lhnf~rD9P*tcZ zhzL{_-JiOAGX?<644H}N^9g0$IS3=#&22)Y}p zip&J#_-$qok=e(hPWYs}6LC&7?mjdbvjI#(GaLBSi^*7!gW3@_*G87D-%iG=RfX<` zna!Mq31rKn8*)S(8!~e=%1o&0ulQH-ooo``8xs-JfUQ+#qPyYl2W593w7#_?MRt&A zxtY>rCft42D90q^M*buXcOPr8*f#l&C5u5w;n@_<~{`iqp@2Cgi?iiC3<=u1J5&JLkr}5G z@Za3%T~##@pZ4&cOj-1eY;M0Yv%|X;C;+&77rHy7`_d2nT-bDn=>;FK@#k_DYwM6)>Fg*43FUSjqCG-A|ibo_TDhF4q?vOK}E!q`VGu;aF^I< zX6*-=6Nqc0s@Qu20M=Un1JZaV$9qbN9CZKjQUz}m-^*`*0{#Hb@Ui9Poj3RZ0000< KMNUMnLSTaKaMY~; diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png deleted file mode 100644 index 782246740e548fd2896ca82a75ece382e4092664..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1158 zcmV;11bO?3P)s}(9y>C;>*wzFq6 z$B^wCau*IwuMY(cT7fqR1OnJO7$DnPqtH$p+Bz$aklh^FwclHYO3Daym6KF`s8Wdi z10Xo(ik8U|uv-KHeUOq{)~ia0R%1y`*lS^7;Gi@Q*1##8yNWg2FL-%e+XZ;A8SdxxujA=`bvQ}#e#gg(%#Rlc4^yVLV&xI%m6{|>*9z7Zx$-=4d7oBe;c*?$cF Y1&$A31Mhs9r~m)}07*qoM6N<$g5O~jE&u=k diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png.mcmeta deleted file mode 100644 index 6e42b4335..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png.mcmeta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 200, - "height": 20, - "border": 1 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png deleted file mode 100644 index 844cc35f74d6e78152d5dddba96d19b9137623b0..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 242 zcmeAS@N?(olHy`uVBq!ia0vp^96&6>!3-qZkM=|YDenNE5LY0*YSpUz{QR7poQ#Z& zprD|1k90*~|7%PV!3-qZkM=|YDenNE5ZC|z|8Ll^Vg35`>(;GX zvt~_ob#+x$Rb^#md3kwRSy^dmX-P>*adB}`QBh%GVSavoUS3{&e0*G7Tx@J?OiWC4 zbaZ57WO#UZNJz+1c9XL}V`DvC978H@xgK=vb28*%HF&;tzT=($Ym^nenj#W^pS*tf zS>D?H|9#gkc0H@DdHaFOlB}%GZL2slJ1YI-qeFauaRk3l=f3h*ZA*6F**#K;u~(J7 k{BCWNf7j8yjo}A__cF!QfUu}dK#LeWUHx3vIVCg!0Qn4H<^TWy diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png.mcmeta deleted file mode 100644 index ac6b6f93d..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png.mcmeta +++ /dev/null @@ -1,15 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 8, - "height": 20, - "border": { - "left": 2, - "top": 2, - "right": 2, - "bottom": 3 - } - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png deleted file mode 100644 index 3a41aca2a1855be0f1a61167ce6047425def6f13..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1165 zcmV;81akX{P)Dfo;g$M1Ba(}@N>2fi! zn}f5LA^2}03{w7%CoQFF@067p2^s6uW-{KMd{VVH=cgs!jTE3=KNKMbf?ByhP{gi2 zCgDLyG`g6OlVmy{=kAx*DIVfr2L-0sPS1z&3TB6XD;Z9*k zW>qPF*{!vvkP7N>TVG-bOKUBBnC*<6{gjYvE7TZ62OugQGKX1@`91?(^JOG*pCkmUAe3 zlP1XhffPCW#Foh*of4H2UnuaF_3lQo)mX3z`&e2Uc{%dOrEywlH*@ZAf6}cb<>}Z$ zp=2PmRTI969j_A7xywSiD@!GFp=pVM8AzJbLXo;+?I>+YXf4S_N|c*}S62D))xpxa zG-J~GxRkz!b*JB~@9Q@i^i;T|R{7n~ZTo%OA*ra}cO#KK)TFdc=5zeUbLz-i3}Q)$ z>ivMX!JpoTO7cTKJp62<(cR_SY)nahZu)2c_1f0*?YgJET{;gH-UaH1`pz&M8}Zd0 zC!SK=egE#+ILP2_+=a;L(8`sybL~Rm94f6go>FE!=|j49iJ{FdM-%10xfX_J4!G#ma^ZSOK`CUHVnAw$dz+p{ritu+N_B+?nt z9a( zO|xqLAMSO=nm)c${u(qt)M}(?gBBB_?D412#ob52Js+W94u~XlLP6ZxYty2mAk% f{eN(>{~`DfKBN+D-E%m;00000NkvXXu0mjfIt)Z~ diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png.mcmeta deleted file mode 100644 index 6e42b4335..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png.mcmeta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 200, - "height": 20, - "border": 1 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slot.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/slot.png deleted file mode 100644 index 406db70fe396a8c409bed04ab731fd4c25119fc7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 507 zcmV4Tx0C=2zkv&MmKp2MKrbfqw6tAnc`2>yULJ2)x2NQvJig%&X$+}*=_-}`d+9UwF+OtZS;fTr7K zI++l&xm7XriZBKcq;*tgmN6$uDfo`Bdj$A?7vov}b$^aNHE%H>AQH!!VcNtS#50?= z!FiuJ!b-AAd`>)J(glehxvqHp#<}RSz%wIeCOuCaAr^}rtaLCdnHuplaa7fG$``U8 ztDLtuYn2*n-IKpCoYz;DxlVHgNi1Rs5=1Ddp^OS_#Aw$^v5=Klt5St1va`C500}_lx6vi~*rtpjmgE?_`CI2m^;tnJohCA7hL|N_Lh+L?G#1lycmiIp^we`&`fSEV4u1 zE-B-Ara@nwawaoF(qT|3<*jCCl*F9#uzn{@BT6PG(v-bmck^o|Go#juBqUL5{aJJ@ pHzMf0ucY4lT0s&4&{~Us@d7C{tq82R{}=!O002ovPDHLkV1j3BW5WOd diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox_clicked.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox_clicked.png deleted file mode 100644 index 03ec5a166659fa835073f61e962f60bed1717d0c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 320 zcmV-G0l)rD91h(r&-%vhF%*XuNg_e6FOFQg}Qbs4DJ`s{S3V za&pd$;}|T@=M#XoZ7VPV{BSr#<955T-EO1qx(Ese#9qa8xm?(6HgsJVEaNx^U}c1=qN<1p!!Q7lbLMzFRz|Fr zSI<>dcDo%ZB}4=>W1eT)wk79GDTUAHQ$N?;nWibU&+|+vg_KgL0if@D|24jzQo-Kk So=Bzu0000c8lw+*ZtlX@;M+|_#f@`o2n@f(>Oe<{mr&Mcn} Pw3xxu)z4*}Q$iB}D6Kzx diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png.mcmeta deleted file mode 100644 index 85fa1f8a0..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png.mcmeta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 16, - "height": 16, - "border": 5 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png deleted file mode 100644 index aef13cda62f5abbdfbf26a250e3b00c1ea5b7fff..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 173 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`C7v#hAr*7pPBs)etiZz}er&GX zV)GA0c8km}tPHbazkQ=IM01nunY-siCsYWZndsK8vD9a4_F8t&I8EjiS|!=rix+JD zreR?0!cxw!@O0{@jz5;4#Q#h?a7M5EcW~I%#0Pxk>>huQa|lLiedjJa`J|!veRbR) Weq}XlLHk~y{S2P2elF{r5}E*;5J0K` diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png.mcmeta deleted file mode 100644 index 85fa1f8a0..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png.mcmeta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 16, - "height": 16, - "border": 5 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png deleted file mode 100644 index eddefa2ed76b1631a0acf5b73f79232a9e5f4218..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 366 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf66p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>dx@v7EBh-JCLs&uz8h{p zg$$}Ct`Q|Ei6yC4$wjF^iowXh$V}J3MAyJ5#L(Qz*u=`zT-(6F%D~|5bXFe}4Y~O# znQ4`{HOx7+_XALa2Hb{{%-q!ClEmBs6g?JJre;>grVvX4<-Jb<^#pmkIEHAPzdCUz zZ-WC5OS7i}_dyk7G3T*LJv?-e?{oY@Z>@!eLx xU{DnDOKrx7_YT#$@6H|ck93W`^kb4b-;xM5lXJoEih!mwc)I$ztaD0e0ssfja}odm diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png.mcmeta deleted file mode 100644 index 85fa1f8a0..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png.mcmeta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 16, - "height": 16, - "border": 5 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png deleted file mode 100644 index f9b2ba1bfeb4a1a472bdbe94bce08bd773173d2c..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 372 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!60wlNoGJgf66p}rHd>I(3)EF2VS{N990fib~ zFff!FFfhDIU|_JC!N4G1FlSew4N!t9$=lt9;eUJonf*W>dx@v7EBh-JW+okd-YCh> zKq1u<*NBpo#FA92xq2lo{*R;|A@8H>4sy<@+b54i*E7+6$$@S2in2l>FVdQ&MBb@ E0K4OFeEkwg6}#gQu&X%Q~loCIB21CpG{8 diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb_disabled.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb_disabled.png deleted file mode 100644 index 96fb12a78393693d6afac81cf6fedaff0b19c688..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 127 zcmeAS@N?(olHy`uVBq!ia0vp^d?3uh1|;P@bT0xaH%}MGkcv6UJzZVD&O5LrBqk&z zoH%t#$@B7-%*zT=k`fXU_x4mePg!PN{>}glCZ>oANnEwCF*y{>r{gNXEpgKK2?NI+ We)WeguJ?c@GI+ZBxvXs4C7m0D$Lt zkeP^x-irvB8ScHP`ifz#1pv7BLPStiFf&vY?!7NNA_6l508|ws0-v9sPykTXHg?W| zhyVaG6RO(ts(O8!S?{m4;O?&|MFj4Sa}F}|1)9NfC_nDK004K#S_^BfW`JzacxFOG zpsFu$5zzqYm}hA-Gn+}W+dwA8_ud9|?`>8hqQR+ZM=v6sCel9BJi*XzRl(ii?udvV zAot!j003sz5i`Gxo(>}dd+!$;1kCw4r@^0d;O^~XGWAMLMD%7=wYWG4l!gd&YKRfj z4x{HUGvV%?UZxWGuA1KhBLg$j&vu;h6^Gm)7;Yc%fey$GN+F;&hpBGM^*o~Qc(0a6>h%d}=@YmPqt z(@0fy1av}1m{-nc=@JB~ex3*J-utW!I;9QXd!>eD%P5GPosgm>nAstx7P!teR*Q(> zobv+j?$~=b+ud2|Xm&>eoa;0OpgX3SH6u({H>z(2sS)-Q0;C+2l|g~dwoIe*!eoaG z?!9r&fthu$pd(hsWSPxu4y9362`T2m*+ctsv=iI}#OUV($Y5E4Gu;3n_#=Oe Z{sGxxb-v1k;r9Rl002ovPDHLkV1hIn8`l5; diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked.png deleted file mode 100644 index 59118c6bdfee3e4176b02604d03c3566759e7e18..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 965 zcmV;$13LVPP)H^!Yx+D*^C0tuzScJNbnXq3ksUW4R|Qbo5S#lC)A)&7cU7WBTs0IOq$T6}o!`Eq$UiLlcEK=eeK(rDAkpe*j}Un%nXDzW zT!MCk&6|cvngeY-g$PWtVa9{9I2B}?T;P2`X72+nc8o1-<0wYS=%N-rznL{H-Z0sq z2+$H$utxEDqXvcmnz+d&2+Y%kYq)E|#l)fjZV@%KL2d|$NUVV_ilQCp{f)@Fd2Wyn z9O8^vU=g~@MsYDFmfyF`h*=24P1UZEOH(l#oCl4;TTHChgO&7?R7q-7;6K_7O9*1`H#9h%JL z&HYUXrjD?JD3LT~SS`$OwCo13bZS*CwuMFZ&+&HBGF!E2JQ_AJz~XV(tZk7^6)%!gYX4?tXVYn%oy9WEJxU z#?%YaPlA7DwZTQ4!L!6;(!(n;dB|L{4HD{0RzMuC?l(#Y#7kTVzEg2AwS(78fjfx! zC?|y_QibaM5mL2u2Q!d}y~83Rfmyuz-W$N>-9kC&3bVLC1DnZMD8+d%)B&1VAr{}H n&Ue2$4R?R74!=GHe?a{URH}P#{?aaR00000NkvXXu0mjfUka)@ diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked_and_hovered.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked_and_hovered.png deleted file mode 100644 index c0b6c2659a00d7f42b0db7e62d7ada6f2e823498..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 964 zcmV;#13UbQP)71j6fai zV?zdyLu8n*xSB&^ zo7_c}m?bA+783fJ(8UScfoc#yh#VseGklc>>1E%8&LSnw0Yob?5Lxi`3skbP42NC_ z4@dy$2AdGi(n@s9oIop1EfEJ6^nzEk0&2+9WDJ=Q4^)dCQ42>RR^Y+?{s@tWlF3>^ z%Oz+x*t}_&q&d*WQ;5JM8)iHxi&H_S$pzjAWcEJLV#nCRHjZMHj4o>7^P5@I;ti7x ziU2K91#1+aH)>!ApoyDYg1|goxQ4qXTudwq;1*Fs8{~$7h{PJ`qA1#d-rtC)JxNUCIaG==)EFVIK2sZz`XjbMSWJjZkbO}tHv=V*~ zJya&VLL{MsPa%uY;dw5^5+tf-87P=66ixaNWg-@yAu%p^&q4j*ToeyBRGOHVV4(Hm z5oa?7FVtz)rZL$E=S3^L!P?|Nhe-l%p=>Y`a9f(lVuw8`B4{6rC9vV>QJE`%8usPMp- zn*Yxci(W$wzDtz2YaS(O;}xpGByFQID47_jj}UaBbq6_OkDRU){u-yFv;=`(X{SA?98nh%wrPBU}f#MOY6q{G0(TJc zQBDd=qzcvhBcy8S4rU+`dxu3t0<(Da{WpNiyM=Pl6=rdP1~!wiP>S!LM*;X moo|128t(oY9nhb082t;bcY$ee-=(Mk0000psu)JQc!hbLf0N(F6stT$KRmI*L z0C3+oG7}Nedl3OM!#M|4Uol+Q1pqkbKtxbgFf&vY&N&Y|A_6l508|ws0-v9sPykTX zHn#VMhyVaG6RO(ts(SpJS?^!h1$Tc$DI#!p?7fkh56}#jL-}*g0RXr=uIs{eUCjX5 zpz+Lvh(J{z;3A>{(lO7{W@a{%WVeA#iqAO>>YUT8L_~vA)s9|7I!&Z~rg?&)-Kv7S z!`%@PKS0hoZ2$nwtRrTA89g0F1lC#)HVByWy?2A(d&Aw^$7Je}nuzGls%mj@5GV~1 z=+qD+rX5DlFEioponEFA_^z7Y0wV)6?~`iIr@p8G4l*}w@hs1)YHR5MgypBIop)B$ zRJob8W|$HY!CDKzSJ`u)Ot9p6KQob;KWjAZ?!5@0H8EArGa}L{yzjgF0Rd7Qyvwv^ zW^0Z<{?bTQbp&)mMwnO5XXz3IslM+U?%w;X3_7I^-g~5mWy>gtoSl%OC79VErxv)* zHCBs=VDJ3^@9tP@HQU`;>1cLG0-WnK2B15pnKdI!S2wC}2B{JD69S|hl$Ak&&bCaW z^TK3@46e1X_lB8uub?AV#$=hzY!0PSRtYKQ!P#FJJ=ZB(zB98AO@`=Wig>Nn&q)KB zG)@o%H#;i-&8$m$I%K8L!7Ovyq(C`uAitXq0IanhFYE6Yn6wkz1jOj)1IS=mfivA* fH6I@z_}AkP0`_+!$2k2u00000NkvXXu0mjfI|&-5 diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_hovered.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_hovered.png deleted file mode 100644 index e0410aae6be380722840e7154d67cd5b1ab55b03..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 637 zcmV-@0)qXCP)Od!dj~tAR9EEnGg}E z>I+;%G(bA$S=!9ZW|HhSkV)~qw?W-|o0W)YaH`tTi%6%5w9hn8Ftl4$aCf*nBH{E9bLx34&BV&jWYweO3mY(gyFnQp2)k6hzKWNYN6^?2uCnT<03AMMQAU zd4YF#?7f@q?yPh)yCVV4bs7WE9n;L35vHpf)i;CG2>S^EQVz<>pg?C^rqOv}vO@;< z-Z-mjPS_X|wg32p*n^z#8^u<DZUB(K^2g{4 XGd+8_Q~lvh00000NkvXXu0mjfTN*8K diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png deleted file mode 100644 index da18e3a9739434e449853f09a30881d064e97082..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 147 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsVGku$B>FSZ!hfRWiaG$xyb+4 zS^eD+-;F2brU|Q;ZJO2T#`IC;LH@>t*H8ZTU%BdU-^{&j^PL!!nBMu{8BHA@$-Tq|$-1!xO{r>mdKI;Vst00r_fxBvhE diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png.mcmeta deleted file mode 100644 index 49e89779d..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png.mcmeta +++ /dev/null @@ -1,15 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 26, - "height": 32, - "border": { - "left": 7, - "top": 8, - "right": 7, - "bottom": 8 - } - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png deleted file mode 100644 index c7f9552aa298a3c2b389ba7c1a6f2ccbf667fe78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsVq+y$B>FSZ?7409WdZ%eR%(l zd{O@sxn~h&nz<(~xwapwp1k10!pX;GE7+Hb2~K=J<7V;}EtUg!AMTqn_d=95@2kDc z2G5tg?e%%$^IJ|(#bbhkV-pLf&=>XL diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png.mcmeta deleted file mode 100644 index 49e89779d..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png.mcmeta +++ /dev/null @@ -1,15 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 26, - "height": 32, - "border": { - "left": 7, - "top": 8, - "right": 7, - "bottom": 8 - } - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png deleted file mode 100644 index 9c5c6eaae327db015a9a316d3e80b089a09f9c39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsZ387$B>FSZ?7409WdZ%eRyBS zPTjLFWnJ?0OKm!_B^rCoIl1JmGWT$PN=_FP{Mk3t)@`*D!yVao-ZwedZoRbNo;5?U z->rKmT+VHIFX-6B!YQQUF+stxzG2n9hqi)&`!7l`GZcUSHMM~|sY+#zp}zMoprs6+ Lu6{1-oD!M<)>}BR diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png.mcmeta deleted file mode 100644 index 49e89779d..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png.mcmeta +++ /dev/null @@ -1,15 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 26, - "height": 32, - "border": { - "left": 7, - "top": 8, - "right": 7, - "bottom": 8 - } - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png deleted file mode 100644 index 0c23cf347a3794813e8d2db15186e12e6f6c5f7e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 159 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsWeX)$B>FSZ!c}+J)pqja*_X3 z!W;J41};I>n#+}xR`R-*s7Dq(P51UYaI)=*EXM)Euc-(BWScx*^yr<-^*uFmC%Udg z$?GuMIj`MnaN-QZi@p2jD+>KgyM3KgNX278+R03^e@O1Ta JS?83{1OUu8IX?gZ diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png.mcmeta deleted file mode 100644 index 49e89779d..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png.mcmeta +++ /dev/null @@ -1,15 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 26, - "height": 32, - "border": { - "left": 7, - "top": 8, - "right": 7, - "bottom": 8 - } - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png deleted file mode 100644 index b136a0bb2b1c5f08506b468b373e0183b44fede7..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 148 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsc26Z$B>FSZ!hfRWiaG$xyb+b zctE3fVQkK(_NPZW=dMXxG||C^^G9^fi_%a3f>&kjPflC^aJfK3N5k)6Gv}E*d0*{i xz7Tcz_(8vvzV+cOoI)xd6BHbqSU7(bG4j3=-+lbtF)N@o44$rjF6*2Ung9*EHpBn` diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png.mcmeta deleted file mode 100644 index 49e89779d..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png.mcmeta +++ /dev/null @@ -1,15 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 26, - "height": 32, - "border": { - "left": 7, - "top": 8, - "right": 7, - "bottom": 8 - } - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png deleted file mode 100644 index c7f9552aa298a3c2b389ba7c1a6f2ccbf667fe78..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 163 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsVq+y$B>FSZ?7409WdZ%eR%(l zd{O@sxn~h&nz<(~xwapwp1k10!pX;GE7+Hb2~K=J<7V;}EtUg!AMTqn_d=95@2kDc z2G5tg?e%%$^IJ|(#bbhkV-pLf&=>XL diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png.mcmeta deleted file mode 100644 index 49e89779d..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png.mcmeta +++ /dev/null @@ -1,15 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 26, - "height": 32, - "border": { - "left": 7, - "top": 8, - "right": 7, - "bottom": 8 - } - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png deleted file mode 100644 index 9c5c6eaae327db015a9a316d3e80b089a09f9c39..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 162 zcmeAS@N?(olHy`uVBq!ia0vp^Qb4T0!3HFGR%fsRsZ387$B>FSZ?7409WdZ%eRyBS zPTjLFWnJ?0OKm!_B^rCoIl1JmGWT$PN=_FP{Mk3t)@`*D!yVao-ZwedZoRbNo;5?U z->rKmT+VHIFX-6B!YQQUF+stxzG2n9hqi)&`!7l`GZcUSHMM~|sY+#zp}zMoprs6+ Lu6{1-oD!M<)>}BR diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png.mcmeta deleted file mode 100644 index 49e89779d..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png.mcmeta +++ /dev/null @@ -1,15 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 26, - "height": 32, - "border": { - "left": 7, - "top": 8, - "right": 7, - "bottom": 8 - } - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png deleted file mode 100644 index e171115189a94067f99885bb4badfd3593e28971..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^O+YNc!3HGF{;A#oQk9-Ajv*Cu-rm|Mc!+_A<)HB6 zzB@dv0d3q1xxK2@xn?YEP~UNON^QYHhe;|guQSa5#_?5_!87iy;;Wf6U;nVWch>Ng z$0wGGE$uJtC7KwVFBq{fwk=5IVB&E+Ex;ntG)l>JD?{QtY`}>Q)*_Erl|N3m9emujiJfGq8 z7lFU*&H@P>TtpIGyYjB>iL6=n`~S~*oA*C^mT`gc17l^o-~-9*3lcylF?hQAxvX
diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png.mcmeta deleted file mode 100644 index 12147d0c5..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png.mcmeta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 130, - "height": 24, - "border": 2 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png deleted file mode 100644 index e171115189a94067f99885bb4badfd3593e28971..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 178 zcmeAS@N?(olHy`uVBq!ia0vp^O+YNc!3HGF{;A#oQk9-Ajv*Cu-rm|Mc!+_A<)HB6 zzB@dv0d3q1xxK2@xn?YEP~UNON^QYHhe;|guQSa5#_?5_!87iy;;Wf6U;nVWch>Ng z$0wGGE$uJtC7KwVFBq{fwk=5IVB&E+Ex;ntG)F=6MhPF?BqG;r671f;o{S|=5E#76+9<>$!*wd>r~6`FsZt-y({fmVU5kavxcuc zidc7SX@6lZ(Zt|MEV|AD2^>*v3JLE8AN-YF`QOAd i@2%jg&mq1Hdl+P*_l>JD?{QtY`}>Q)*_Erl|N3m9emujiJfGq8 z7lFU*&H@P>TtpIGyYjB>iL6=n`~S~*oA*C^mT`gc17l^o-~-9*3lcylF?hQAxvX diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png.mcmeta deleted file mode 100644 index 12147d0c5..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png.mcmeta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 130, - "height": 24, - "border": 2 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png deleted file mode 100644 index 86ec7d4209eb03cb8074b4060b05e20a424b0ebe..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 111 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%`W}YsNAr*6yV>ToltYN3kd1|bL0N*6spo=pB+vu~Pgg&ebxsLQ E0Af8F*Z=?k diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png.mcmeta deleted file mode 100644 index 85fa1f8a0..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png.mcmeta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 16, - "height": 16, - "border": 5 - } - } -} diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png deleted file mode 100644 index 486af70956ae482d5368fc46fb9a0821634d1d04..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 104 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4#%``kpS1Ar*6y|NQ^|zn;yAQ5guh zr#Nm(o3YpD$t^zS#cVxN20lCqi3tuVJ&cSD+jzL-ySF&b0BU9MboFyt=akR{090ok A3IG5A diff --git a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png.mcmeta b/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png.mcmeta deleted file mode 100644 index 85fa1f8a0..000000000 --- a/Archie/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png.mcmeta +++ /dev/null @@ -1,10 +0,0 @@ -{ - "gui": { - "scaling": { - "type": "nine_slice", - "width": 16, - "height": 16, - "border": 5 - } - } -} diff --git a/Archie/common/src/main/resources/data/archie/structure/gametest/empty.nbt b/Archie/common/src/main/resources/data/archie/structure/gametest/empty.nbt deleted file mode 100644 index c5562f150067080e54d49b0d5a62698fe269dd36..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 123 zcmb2|=3oGW|F)+$@-;aKv^-R7Kb6??J~`JnbN?j6n+)Yo%CzUn1r|=)KdVMpAaJ%G z55u-qdpp`rUY**OD|r0*jNb=uy_bHLWBp72L4w}7`4(r5F3pv{wBTKelHO#sZ?185 b!n*lI`?^mzA2m^bkQKq`>znU54QM9-1z 1f) { "OutBack should overshoot near the end, got $sample" } - } - - @Test - fun testLinearMidpoint() { - val sample = Easings.Linear.transform(0.5f) - assertTrue(abs(sample - 0.5f) < 0.0001f) { "Expected 0.5, got $sample" } - } -} \ No newline at end of file diff --git a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ArrayUtilsTests.kt b/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ArrayUtilsTests.kt deleted file mode 100644 index 63e999c57..000000000 --- a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ArrayUtilsTests.kt +++ /dev/null @@ -1,56 +0,0 @@ -package net.kernelpanicsoft.archie.testing - -import net.kernelpanicsoft.archie.util.buildArray -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class ArrayUtilsTests { - @Test - fun testBuildArrayCreatesCorrectArray() { - val array = buildArray { - add(1) - add(2) - add(3) - } - assertEquals(3, array.size) - assertEquals(1, array[0]) - assertEquals(2, array[1]) - assertEquals(3, array[2]) - } - - @Test - fun testBuildArrayWithCapacity() { - val array = buildArray(5) { - add("a") - add("b") - } - assertEquals(2, array.size) - assertEquals("a", array[0]) - assertEquals("b", array[1]) - } - - @Test - fun testBuildArrayWithEmptyList() { - val array = buildArray { - // Empty - } - assertEquals(0, array.size) - } - - @Test - fun testBuildArrayPreservesOrder() { - val array = buildArray { - add(5) - add(4) - add(3) - add(2) - add(1) - } - assertEquals(5, array[0]) - assertEquals(4, array[1]) - assertEquals(3, array[2]) - assertEquals(2, array[3]) - assertEquals(1, array[4]) - } -} - diff --git a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/CommonTests.kt b/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/CommonTests.kt deleted file mode 100644 index 5d037ddda..000000000 --- a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/CommonTests.kt +++ /dev/null @@ -1,18 +0,0 @@ -package net.kernelpanicsoft.archie.testing - -import net.kernelpanicsoft.archie.Archie -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class CommonTests { - @Test - fun testModIdConstant() { - assertEquals("archie", Archie.MOD_ID) - } - - @Test - fun testModIdNotBlank() { - assertTrue(Archie.MOD_ID.isNotBlank()) - } -} diff --git a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt b/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt deleted file mode 100644 index 9d4ebd97b..000000000 --- a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.kernelpanicsoft.archie.testing - -import net.kernelpanicsoft.archie.events.AEvents -import net.kernelpanicsoft.archie.gametest.internal.archieGameTests -import net.kernelpanicsoft.archie.gametest.junit.GameTestRunner -import org.junit.jupiter.api.DynamicContainer -import org.junit.jupiter.api.TestFactory -import org.junit.jupiter.api.parallel.Execution -import org.junit.jupiter.api.parallel.ExecutionMode - -@Execution(ExecutionMode.CONCURRENT) -class GameTests -{ - @TestFactory - fun tests(): Collection = GameTestRunner.tests("archie", AEvents.ArchieGameTestBuilder::archieGameTests) -} \ No newline at end of file diff --git a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GuiClientHarnessTests.kt b/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GuiClientHarnessTests.kt deleted file mode 100644 index ce8e2b3a7..000000000 --- a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GuiClientHarnessTests.kt +++ /dev/null @@ -1,62 +0,0 @@ -package net.kernelpanicsoft.archie.testing - -import net.kernelpanicsoft.archie.gui.composables.containers.resolveScrollableContentAxis -import net.kernelpanicsoft.archie.gui.composables.containers.resolveScrollableViewportAxis -import net.kernelpanicsoft.archie.gui.composables.input.normalizeSliderValue -import net.kernelpanicsoft.archie.gui.composables.input.resolveSliderThumbX -import net.kernelpanicsoft.archie.gui.composables.input.resolveSwitchThumbOffset -import net.kernelpanicsoft.archie.gui.composables.input.snapSliderValue -import net.kernelpanicsoft.archie.gui.layout.IntCoordinates -import net.kernelpanicsoft.archie.gui.layout.IntRect -import net.kernelpanicsoft.archie.gui.layout.Size -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertNotNull -import org.junit.jupiter.api.Test - -class GuiClientHarnessTests { - @Test - fun testSliderNormalization() { - assertEquals(0f, normalizeSliderValue(-0.1f)) - assertEquals(0.5f, normalizeSliderValue(0.5f)) - assertEquals(1f, normalizeSliderValue(1.5f)) - } - - @Test - fun testSliderStepSnapping() { - assertEquals(0f, snapSliderValue(0.1f, steps = 4)) - assertEquals(0.5f, snapSliderValue(0.49f, steps = 4)) - assertEquals(1f, snapSliderValue(0.99f, steps = 4)) - } - - @Test - fun testScrollableAxisResolution() { - assertEquals(140, resolveScrollableViewportAxis(childSize = 24, min = 0, max = 140)) - assertEquals(24, resolveScrollableViewportAxis(childSize = 24, min = 0, max = Int.MAX_VALUE)) - assertEquals(24, resolveScrollableContentAxis(childSize = 24, min = 0, max = 140)) - } - - @Test - fun testClipRectIntersection() { - val a = IntRect.fromPositionAndSize(IntCoordinates(10, 10), Size(30, 20)) - val b = IntRect.fromPositionAndSize(IntCoordinates(25, 20), Size(20, 20)) - val intersection = a.intersect(b) - - assertNotNull(intersection) - assertEquals(IntRect(25, 20, 40, 30), intersection) - } - - @Test - fun testSwitchThumbOffsetNarrowTrack() { - assertEquals(2, resolveSwitchThumbOffset(thumbOffset = 30, trackWidth = 0)) - assertEquals(2, resolveSwitchThumbOffset(thumbOffset = 30, trackWidth = 12)) - assertEquals(18, resolveSwitchThumbOffset(thumbOffset = 18, trackWidth = 34)) - } - - @Test - fun testSliderThumbXNarrowWidth() { - assertEquals(15, resolveSliderThumbX(rawThumbX = 40, sliderX = 15, sliderWidth = 0)) - assertEquals(15, resolveSliderThumbX(rawThumbX = -5, sliderX = 15, sliderWidth = 6)) - assertEquals(22, resolveSliderThumbX(rawThumbX = 22, sliderX = 15, sliderWidth = 15)) - } -} - diff --git a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/InputPrimitiveTests.kt b/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/InputPrimitiveTests.kt deleted file mode 100644 index b9a9c194e..000000000 --- a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/InputPrimitiveTests.kt +++ /dev/null @@ -1,40 +0,0 @@ -package net.kernelpanicsoft.archie.testing - -import net.kernelpanicsoft.archie.gui.composables.input.normalizeSliderValue -import net.kernelpanicsoft.archie.gui.composables.input.resolveSliderThumbX -import net.kernelpanicsoft.archie.gui.composables.input.resolveSwitchThumbOffset -import net.kernelpanicsoft.archie.gui.composables.input.snapSliderValue -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class InputPrimitiveTests { - @Test - fun testSliderNormalizeClamps() { - assertEquals(0f, normalizeSliderValue(-1f)) - assertEquals(0.25f, normalizeSliderValue(0.25f)) - assertEquals(1f, normalizeSliderValue(2f)) - } - - @Test - fun testSliderSnapRespectsSteps() { - assertEquals(0.5f, snapSliderValue(0.49f, steps = 4)) - assertEquals(0.75f, snapSliderValue(0.74f, steps = 4)) - assertEquals(1f, snapSliderValue(1.4f, steps = 4)) - } - - @Test - fun testSwitchThumbOffsetHandlesNarrowTrack() { - assertEquals(2, resolveSwitchThumbOffset(thumbOffset = 10, trackWidth = 0)) - assertEquals(2, resolveSwitchThumbOffset(thumbOffset = 10, trackWidth = 10)) - assertEquals(2, resolveSwitchThumbOffset(thumbOffset = -5, trackWidth = 18)) - assertEquals(4, resolveSwitchThumbOffset(thumbOffset = 4, trackWidth = 20)) - } - - @Test - fun testSliderThumbXHandlesNarrowWidth() { - assertEquals(15, resolveSliderThumbX(rawThumbX = 20, sliderX = 15, sliderWidth = 0)) - assertEquals(15, resolveSliderThumbX(rawThumbX = -10, sliderX = 15, sliderWidth = 2)) - assertEquals(19, resolveSliderThumbX(rawThumbX = 19, sliderX = 15, sliderWidth = 12)) - } -} - diff --git a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/LayoutCoreTests.kt b/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/LayoutCoreTests.kt deleted file mode 100644 index 38a95b5b0..000000000 --- a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/LayoutCoreTests.kt +++ /dev/null @@ -1,68 +0,0 @@ -package net.kernelpanicsoft.archie.testing - -import net.kernelpanicsoft.archie.gui.layout.Alignment -import net.kernelpanicsoft.archie.gui.layout.IntCoordinates -import net.kernelpanicsoft.archie.gui.layout.IntSize -import net.kernelpanicsoft.archie.gui.layout.LayoutDirection -import net.kernelpanicsoft.archie.gui.layout.offset as layoutOffset -import net.kernelpanicsoft.archie.gui.layout.pos -import net.kernelpanicsoft.archie.gui.layout.size -import net.kernelpanicsoft.archie.gui.modifiers.Constraints -import net.kernelpanicsoft.archie.gui.modifiers.offset -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class LayoutCoreTests { - @Test - fun testIntCoordinatesMath() { - val a = IntCoordinates(20, 6) - val b = IntCoordinates(5, 4) - assertEquals(IntCoordinates(25, 10), a + b) - assertEquals(IntCoordinates(15, 2), a - b) - } - - @Test - fun testAliasesConstructExpectedCoordinates() { - assertEquals(IntCoordinates(2, 3), pos(2, 3)) - assertEquals(IntCoordinates(7, 9), layoutOffset(7, 9)) - assertEquals(IntSize(10, 11), size(10, 11)) - } - - @Test - fun testConstraintsCopyNormalizesBounds() { - val c = Constraints(minWidth = 90, maxWidth = 10, minHeight = 50, maxHeight = 12) - val normalized = c.copy() - assertEquals(10, normalized.minWidth) - assertEquals(90, normalized.maxWidth) - assertEquals(12, normalized.minHeight) - assertEquals(50, normalized.maxHeight) - } - - @Test - fun testConstraintsOffsetKeepsMaxUnbounded() { - val c = Constraints(maxWidth = Int.MAX_VALUE, maxHeight = Int.MAX_VALUE) - val shifted = c.offset(horizontal = -20, vertical = -30) - assertEquals(Int.MAX_VALUE, shifted.maxWidth) - assertEquals(Int.MAX_VALUE, shifted.maxHeight) - } - - @Test - fun testConstraintsOffsetCoercesAtZero() { - val c = Constraints(minWidth = 4, maxWidth = 8, minHeight = 3, maxHeight = 9) - val shifted = c.offset(horizontal = -20, vertical = -20) - assertEquals(0, shifted.minWidth) - assertEquals(0, shifted.maxWidth) - assertEquals(0, shifted.minHeight) - assertEquals(0, shifted.maxHeight) - } - - @Test - fun testAlignmentStartEndByDirection() { - val child = IntSize(20, 20) - val space = IntSize(100, 100) - assertEquals(IntCoordinates(0, 0), Alignment.TopStart.align(child, space, LayoutDirection.Ltr)) - assertEquals(IntCoordinates(80, 0), Alignment.TopStart.align(child, space, LayoutDirection.Rtl)) - assertEquals(IntCoordinates(40, 40), Alignment.Center.align(child, space, LayoutDirection.Ltr)) - } -} - diff --git a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/MutableEntryTests.kt b/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/MutableEntryTests.kt deleted file mode 100644 index 47f3b70c6..000000000 --- a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/MutableEntryTests.kt +++ /dev/null @@ -1,64 +0,0 @@ -package net.kernelpanicsoft.archie.testing - -import net.kernelpanicsoft.archie.util.MutableEntry -import net.kernelpanicsoft.archie.util.toMutableEntry -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class MutableEntryTests { - @Test - fun testMutableEntryCreation() { - val entry = MutableEntry("key", "value") - assertEquals("key", entry.key) - assertEquals("value", entry.value) - } - - @Test - fun testMutableEntryKeyMutation() { - val entry = MutableEntry("original", 42) - entry.key = "modified" - assertEquals("modified", entry.key) - assertEquals(42, entry.value) - } - - @Test - fun testMutableEntryValueMutation() { - val entry = MutableEntry("key", 10) - entry.value = 20 - assertEquals("key", entry.key) - assertEquals(20, entry.value) - } - - @Test - fun testPairToMutableEntry() { - val pair = Pair("pairKey", "pairValue") - val entry = pair.toMutableEntry() - assertEquals("pairKey", entry.key) - assertEquals("pairValue", entry.value) - } - - @Test - fun testMapEntryToMutableEntry() { - val map = mapOf("mapKey" to 100) - val mapEntry = map.entries.first() - val mutableEntry = mapEntry.toMutableEntry() - assertEquals("mapKey", mutableEntry.key) - assertEquals(100, mutableEntry.value) - } - - @Test - fun testMutableEntryEquality() { - val entry1 = MutableEntry("key", "value") - val entry2 = MutableEntry("key", "value") - assertEquals(entry1, entry2) - } - - @Test - fun testMutableEntryToString() { - val entry = MutableEntry("test", 123) - val str = entry.toString() - assertTrue("test" in str && "123" in str) { "toString should contain key and value" } - } -} - diff --git a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/NetworkChannelValidationTests.kt b/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/NetworkChannelValidationTests.kt deleted file mode 100644 index 1b255fd9e..000000000 --- a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/NetworkChannelValidationTests.kt +++ /dev/null @@ -1,40 +0,0 @@ -package net.kernelpanicsoft.archie.testing - -import net.kernelpanicsoft.archie.networking.NetworkChannel -import net.kernelpanicsoft.archie.util.rem -import net.minecraft.resources.ResourceLocation -import org.junit.jupiter.api.Assertions.assertThrows -import org.junit.jupiter.api.Test - -class NetworkChannelValidationTests { - private class NotDataClass(val value: Int) - private data class DataWithoutSerializer(val value: Int) - - @kotlinx.serialization.Serializable - private data class ValidPacket(val value: Int) - - @Test - fun testRejectsNonDataClass() { - val channel = NetworkChannel("archie" % "gametest_non_data") - assertThrows(IllegalArgumentException::class.java) { - channel.serverbound { _, _ -> } - } - } - - @Test - fun testRejectsDataWithoutSerializer() { - val channel = NetworkChannel("archie" % "gametest_no_serializer") - assertThrows(IllegalArgumentException::class.java) { - channel.clientbound { _, _ -> } - } - } - - @Test - fun testRejectsDuplicateRegistrations() { - val channel = NetworkChannel("archie" % "gametest_duplicate") - channel.clientbound { _, _ -> } - assertThrows(IllegalArgumentException::class.java) { - channel.clientbound { _, _ -> } - } - } -} diff --git a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/PaddingMarginTests.kt b/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/PaddingMarginTests.kt deleted file mode 100644 index e4ec0645c..000000000 --- a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/PaddingMarginTests.kt +++ /dev/null @@ -1,125 +0,0 @@ -package net.kernelpanicsoft.archie.testing - -import net.kernelpanicsoft.archie.gui.modifiers.Constraints -import net.kernelpanicsoft.archie.gui.modifiers.position.MarginModifier -import net.kernelpanicsoft.archie.gui.modifiers.position.MarginValues -import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier -import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingValues -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class PaddingMarginTests { - @Test - fun testPaddingModifierReducesConstraints() { - val padding = PaddingValues(left = 10, right = 10, top = 5, bottom = 5) - val modifier = PaddingModifier(padding) - - val constraints = Constraints( - minWidth = 100, - maxWidth = 200, - minHeight = 50, - maxHeight = 100, - ) - - val modified = modifier.modifyInnerConstraints(constraints) - - assertEquals(180, modified.maxWidth) - assertEquals(90, modified.maxHeight) - } - - @Test - fun testMarginModifierHorizontal() { - val margin = MarginValues(left = 5, right = 5, top = 3, bottom = 3) - val modifier = MarginModifier(margin) - - assertEquals(10, modifier.horizontal) - assertEquals(6, modifier.vertical) - } - - @Test - fun testPaddingValuesGetOffset() { - val padding = PaddingValues(left = 8, right = 12, top = 4, bottom = 6) - val offset = padding.getOffset() - - assertEquals(8, offset.x) - assertEquals(4, offset.y) - } - - @Test - fun testPaddingModifierNeverNegative() { - val largePadding = PaddingValues(left = 100, right = 100, top = 100, bottom = 100) - val modifier = PaddingModifier(largePadding) - - val constraints = Constraints( - minWidth = 0, - maxWidth = 50, - minHeight = 0, - maxHeight = 50, - ) - - val modified = modifier.modifyInnerConstraints(constraints) - - assertTrue(modified.maxWidth >= 0) { "Max width should never be negative" } - assertTrue(modified.maxHeight >= 0) { "Max height should never be negative" } - } - - @Test - fun testAsymmetricPadding() { - val padding = PaddingValues( - left = 5, - right = 15, - top = 10, - bottom = 20, - ) - val modifier = PaddingModifier(padding) - - assertEquals(20, modifier.horizontal) - assertEquals(30, modifier.vertical) - } - - @Test - fun testPaddingMerge() { - val padding1 = PaddingValues(left = 5, top = 5, right = 0, bottom = 0) - val padding2 = PaddingValues(left = 0, top = 0, right = 5, bottom = 5) - - val merged = padding1 + padding2 - - assertEquals(5, merged.left) - assertEquals(5, merged.right) - assertEquals(5, merged.top) - assertEquals(5, merged.bottom) - } - - @Test - fun testMarginMerge() { - val margin1 = MarginValues(left = 2, top = 2, right = 0, bottom = 0) - val margin2 = MarginValues(left = 0, top = 0, right = 3, bottom = 3) - - val merged = margin1 + margin2 - - assertEquals(2, merged.left) - assertEquals(3, merged.right) - assertEquals(2, merged.top) - assertEquals(3, merged.bottom) - } - - @Test - fun testPaddingReducesMinConstraints() { - val padding = PaddingValues(left = 10, right = 10, top = 10, bottom = 10) - val modifier = PaddingModifier(padding) - - val constraints = Constraints( - minWidth = 50, - maxWidth = 200, - minHeight = 50, - maxHeight = 200, - ) - - val modified = modifier.modifyInnerConstraints(constraints) - - assertEquals(30, modified.minWidth) - assertEquals(30, modified.minHeight) - } -} - diff --git a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ReflectionUtilsTests.kt b/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ReflectionUtilsTests.kt deleted file mode 100644 index ea856bf4b..000000000 --- a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ReflectionUtilsTests.kt +++ /dev/null @@ -1,66 +0,0 @@ -package net.kernelpanicsoft.archie.testing - -import net.kernelpanicsoft.archie.util.getReflection -import net.kernelpanicsoft.archie.util.setReflection -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertThrows -import org.junit.jupiter.api.Test - -class ReflectionUtilsTests { - private class TestClass { - @Suppress("unused") - private var privateField: String = "initial" - - @Suppress("unused") - private var numberField: Int = 42 - - fun getPrivateField(): String = privateField - } - - @Test - fun testGetReflection() { - val obj = TestClass() - val value: String = obj.getReflection("privateField") - assertEquals("initial", value) - } - - @Test - fun testSetReflection() { - val obj = TestClass() - obj.setReflection("privateField", "modified") - val retrieved: String = obj.getReflection("privateField") - assertEquals("modified", retrieved) - } - - @Test - fun testReflectionWithDifferentType() { - val obj = TestClass() - val value: Int = obj.getReflection("numberField") - assertEquals(42, value) - } - - @Test - fun testSetReflectionWithDifferentType() { - val obj = TestClass() - obj.setReflection("numberField", 99) - val retrieved: Int = obj.getReflection("numberField") - assertEquals(99, retrieved) - } - - @Test - fun testReflectionModifiesObjectState() { - val obj = TestClass() - assertEquals("initial", obj.getPrivateField()) - obj.setReflection("privateField", "changed") - assertEquals("changed", obj.getPrivateField()) - } - - @Test - fun testReflectionNonExistentFieldThrows() { - val obj = TestClass() - assertThrows(NoSuchFieldException::class.java) { - obj.getReflection("nonexistent") - } - } -} - diff --git a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ResourceLocationTests.kt b/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ResourceLocationTests.kt deleted file mode 100644 index f69a1ea37..000000000 --- a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ResourceLocationTests.kt +++ /dev/null @@ -1,78 +0,0 @@ -package net.kernelpanicsoft.archie.testing - -import net.kernelpanicsoft.archie.util.div -import net.kernelpanicsoft.archie.util.rem -import net.minecraft.resources.ResourceLocation -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class ResourceLocationTests { - @Test - fun testNamespacePathOperator() { - val id = "archie_test" % "path/value" - assertEquals("archie_test", id.namespace) - assertEquals("path/value", id.path) - } - - @Test - fun testArchieNamespaceOperatorEquivalent() { - val id = "archie" % "main" - assertEquals("archie", id.namespace) - assertEquals("main", id.path) - } - - @Test - fun testResourceLocationDivisionOperators() { - val base = ResourceLocation.fromNamespaceAndPath("archie", "root") - val byString = base / "child" - val byResource = base / ResourceLocation.fromNamespaceAndPath("other", "leaf") - val withPrefix = "prefix" / base - - assertEquals("root/child", byString.path) - assertEquals("root/leaf", byResource.path) - assertEquals("prefix/root", withPrefix.path) - } - - @Test - fun testDivisionOperatorPreservesNamespace() { - val base = ResourceLocation.fromNamespaceAndPath("archie", "root") - val result = base / "child" - assertEquals("archie", result.namespace) - } - - @Test - fun testDivisionOperatorWithNestedPaths() { - val base = ResourceLocation.fromNamespaceAndPath("archie", "a") - val result = base / "b" / "c" / "d" - assertEquals("archie", result.namespace) - assertEquals("a/b/c/d", result.path) - } - - @Test - fun testRemOperatorWithEmptyPath() { - val id = "namespace" % "" - assertEquals("namespace", id.namespace) - assertEquals("", id.path) - } - - @Test - fun testRemOperatorWithComplexPaths() { - val id = "my_mod" % "blocks/custom_block" - assertEquals("my_mod", id.namespace) - assertEquals("blocks/custom_block", id.path) - } - - @Test - fun testResourceLocationCombinations() { - val id1 = "archie" % "test" - val id2 = id1 / "sub" - val id3 = "prefix" / id2 - - assertEquals("archie", id1.namespace) - assertEquals("test", id1.path) - assertEquals("archie", id2.namespace) - assertEquals("test/sub", id2.path) - assertEquals("archie", id3.namespace) - assertEquals("prefix/test/sub", id3.path) - } -} diff --git a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableLayoutTests.kt b/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableLayoutTests.kt deleted file mode 100644 index 3c84cffc9..000000000 --- a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableLayoutTests.kt +++ /dev/null @@ -1,24 +0,0 @@ -package net.kernelpanicsoft.archie.testing - -import net.kernelpanicsoft.archie.gui.composables.containers.resolveScrollableContentAxis -import net.kernelpanicsoft.archie.gui.composables.containers.resolveScrollableViewportAxis -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Test - -class ScrollableLayoutTests { - @Test - fun testHorizontalScrollableWrapsHeight() { - val resolved = resolveScrollableContentAxis(childSize = 24, min = 0, max = 198) - assertEquals(24, resolved) - } - - @Test - fun testScrollableViewportAxisFillsBounds() { - val finite = resolveScrollableViewportAxis(childSize = 32, min = 0, max = 150) - assertEquals(150, finite) - - val unbounded = resolveScrollableViewportAxis(childSize = 32, min = 0, max = Int.MAX_VALUE) - assertEquals(32, unbounded) - } -} - diff --git a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableStateSmokeTests.kt b/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableStateSmokeTests.kt deleted file mode 100644 index a9d7c2534..000000000 --- a/Archie/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableStateSmokeTests.kt +++ /dev/null @@ -1,32 +0,0 @@ -package net.kernelpanicsoft.archie.testing - -import net.kernelpanicsoft.archie.gui.composables.containers.ScrollableState -import org.junit.jupiter.api.Assertions.assertEquals -import org.junit.jupiter.api.Assertions.assertTrue -import org.junit.jupiter.api.Test - -class ScrollableStateSmokeTests { - @Test - fun testScrollByClampsToRange() { - val state = ScrollableState().apply { maxScroll = 100 } - - state.scrollBy(250.0) - assertEquals(100.0, state.scrollOffset) - - state.scrollBy(-500.0) - assertEquals(0.0, state.scrollOffset) - } - - @Test - fun testScrollByUpdatesInteractionTimestamp() { - val state = ScrollableState().apply { maxScroll = 100 } - val before = state.lastInteractTime - - state.scrollBy(1.0) - - assertTrue(state.lastInteractTime >= before) { - "Expected interaction timestamp to increase after scrollBy" - } - } -} - diff --git a/Archie/common/src/test/resources/junit-platform.properties b/Archie/common/src/test/resources/junit-platform.properties deleted file mode 100644 index 222e4cb1a..000000000 --- a/Archie/common/src/test/resources/junit-platform.properties +++ /dev/null @@ -1,17 +0,0 @@ -# Parallel execution is enabled but defaults to same_thread, so every existing test class stays -# sequential unless it explicitly opts in via @Execution(CONCURRENT) (see GameTests). -junit.jupiter.execution.parallel.enabled=true -junit.jupiter.execution.parallel.mode.default=same_thread -junit.jupiter.execution.parallel.mode.classes.default=same_thread - -# Worker threads here spend most of their time blocked waiting on external Gradle/Minecraft -# processes rather than doing CPU work, so size the pool by a fixed count instead of core count -# (which on small CI runners could be lower than the GameTest matrix size). Each invocation test -# and each individual method test blocks its own thread until its specific result appears in the -# invocation's log (which can be minutes into a 20-minute run, since the game processes its own -# tests sequentially) - too small a pool here means most tests just queue for a thread that won't -# free up soon, never even starting even though their result may already be available. Sized well -# past worst-case demand (invocation count + every matching test method across the whole matrix) -# since these threads are cheap to over-provision. -junit.jupiter.execution.parallel.config.strategy=fixed -junit.jupiter.execution.parallel.config.fixed.parallelism=128 diff --git a/Archie/fabric/build.gradle.kts b/Archie/fabric/build.gradle.kts deleted file mode 100644 index edb2a48e5..000000000 --- a/Archie/fabric/build.gradle.kts +++ /dev/null @@ -1,247 +0,0 @@ -import net.kernelpanicsoft.archie.plugin.bundleMod -import net.kernelpanicsoft.archie.plugin.bundleRuntimeLibrary -import net.kernelpanicsoft.archie.plugin.runtimeLibrary -import org.jetbrains.kotlin.konan.properties.loadProperties - - -plugins { - alias(libs.plugins.shadow) - alias(libs.plugins.archie) -} - -architectury { - platformSetupLoomIde() - fabric() -} - -actualizer { - actualizes(project(":common")) -} - -// If a `jar { from(project(":common").sourceSets.main.get().output) }` merge (like neoforge's) -// ever gets added here too, exclude("net/kernelpanicsoft/archie/**") from it - that output is -// common's own stub-linked classes, and it won the duplicatesStrategy race on NeoForge (crashed -// at runtime). See neoforge/build.gradle.kts's jar task. - -val localProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("local.properties") - val sharedPropsFile = rootDir.resolve("../local.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val sharedProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("gradle.properties") - val sharedPropsFile = rootDir.resolve("../gradle.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val String.prop: String? - get() = sharedProperties?.get(this)?.toString() - -val String.local: String? - get() = localProperties?.get(this)?.toString() - -val String.env: String? - get() = System.getenv(this) - -val String.localOrEnv: String? - get() = localProperties?.get(this)?.toString() ?: System.getenv(this.uppercase()) - - -configurations { - create("common") - create("shadowCommon") - compileClasspath.get().extendsFrom(configurations["common"]) - runtimeClasspath.get().extendsFrom(configurations["common"]) - testCompileClasspath.get().extendsFrom(compileClasspath.get()) - testRuntimeClasspath.get().extendsFrom(runtimeClasspath.get()) -// getByName("developmentFabric").extendsFrom(configurations["common"]) -} - -loom { - log4jConfigs.from(project(":common").loom.log4jConfigs) - accessWidenerPath.set(project(":common").loom.accessWidenerPath) - - mods { - maybeCreate("main").apply { - sourceSet(sourceSets.main.get()) - } - } - - runs { - getByName("client") { - name = "Minecraft Client" - source(sourceSets.main.get()) - vmArg("-XX:+AllowEnhancedClassRedefinition") - } - getByName("server") { - name = "Minecraft Server" - source(sourceSets.main.get()) - vmArgs("-XX:+AllowEnhancedClassRedefinition") - } - // This adds a new gradle task that runs the datagen API: "gradlew runDatagen" - create("datagen") { - client() - name = "Minecraft Datagen" - property("archie.datagen", "true") - property("archie.datagen.client", providers.gradleProperty("client_datagen").orElse("true").get()) - property("archie.datagen.server", providers.gradleProperty("server_datagen").orElse("true").get()) - property("fabric-api.datagen") - property("fabric-api.datagen.modid", providers.gradleProperty("mod_id").orElse("archie").get()) - property("fabric-api.datagen.output-dir", file("src/main/generated").absolutePath) - - runDir = "build/datagen" - } - create("gametest") { - server() - name = "Minecraft GameTest" - property("fabric-api.gametest") - property("archie.gametest", "true") - property("archie.gametest.side", "server") - property("archie.gametest.modid", providers.gradleProperty("mod_id").orElse("archie").get()) - } - create("gametestClient") { - client() - name = "Minecraft GameTest Client" - property("fabric-api.gametest") - property("archie.gametest", "true") - property("archie.gametest.side", "client") - property("archie.gametest.modid", providers.gradleProperty("mod_id").orElse("archie").get()) - } - } -} - -fabricApi.configureDataGeneration { - createRunConfiguration = false - outputDirectory.set(file("src/main/generated")) -} - -sourceSets { - main { - resources { - } - kotlin { - srcDir("src/main/gametest") - } - java { - srcDir("src/main/mixin") - } - } -} - -dependencies { - modImplementation(libs.fabric.loader) - modApi(libs.fabric.api) - modApi(libs.architectury.fabric) - modImplementation(libs.kotlin.fabric) - compileOnly(libs.kotlinx.serialization) - bundleRuntimeLibrary(libs.kotlinx.serialization) - bundleRuntimeLibrary(libs.kotlinx.serialization.json) - bundleRuntimeLibrary(libs.kotlinx.serialization.nbt) - bundleRuntimeLibrary(libs.kotlinx.serialization.toml) - bundleRuntimeLibrary(libs.kotlinx.serialization.json5) - bundleRuntimeLibrary(libs.kotlinx.serialization.cbor) - bundleRuntimeLibrary(compose.runtime) - modLocalRuntime(libs.rei.fabric) - modCompileOnlyApi(libs.modmenu) - modCompileOnlyApi(libs.catalogue.fabric) - modLocalRuntime(libs.catalogue.fabric) - modLocalRuntime(libs.menulogue.fabric) - modCompileOnlyApi(libs.clothConfig.fabric) - modLocalRuntime(libs.clothConfig.fabric) - bundleMod(libs.storage.fabric) - - implementation(libs.junit.jupiter.api) - testImplementation(libs.junit.jupiter.api) - testRuntimeOnly(libs.junit.jupiter.engine) - // dev/test-only, not shipped - see the compileOnly note in common/build.gradle.kts - runtimeLibrary(libs.kotlinx.coroutines.test) - - "common"(project(":common", "namedElements")) { isTransitive = false } - "shadowCommon"(project(":common", "transformProductionFabric")) { isTransitive = false } -} - -modResources { - filesMatching.add("fabric.mod.json") -} - - -tasks { - base.archivesName.set(base.archivesName.get() + "-fabric") - - test { - useJUnitPlatform() - } - - processResources { - from(project(":common").sourceSets.main.get().resources) { - include("assets/${"mod_id".prop}/**") - include("data/${"mod_id".prop}/**") - include("${"mod_id".prop}-common.mixins.json") - include("${"mod_id".prop}.common.json") - include("${"mod_id".prop}.accesswidener") - } - dependsOn(processTestResources) - } - - processTestResources { - } - - classes { - finalizedBy(testClasses) - } - - shadowJar { - configurations = - listOf(project.configurations.getByName("shadowCommon"), project.configurations.getByName("shadow")) - archiveClassifier.set("dev-shadow") - } - - remapJar { - injectAccessWidener.set(true) - inputFile.set(shadowJar.get().archiveFile) - dependsOn(shadowJar) - } - - jar.get().archiveClassifier.set("dev") - - sourcesJar { - val commonSources = project(":common").tasks.sourcesJar - dependsOn(commonSources) - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from(commonSources.get().archiveFile.map { zipTree(it) }) - } -} - -publishing { - publications.create("mavenFabric") { - artifactId = base.archivesName.get() - from(components["java"]) - } - - repositories { - mavenLocal() - maven { - name = "Reposilite" - val releasesUrl = "https://maven.kernelpanicsoft.net/releases" - val snapshotsUrl = "https://maven.kernelpanicsoft.net/snapshots" - - url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsUrl else releasesUrl) - - credentials { - username = localProperties?.getProperty("reposilite.username") - ?: System.getenv("REPOSILITE_USERNAME") - password = localProperties?.getProperty("reposilite.password") - ?: System.getenv("REPOSILITE_PASSWORD") - } - } - } -} \ No newline at end of file diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.fabric.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.fabric.kt deleted file mode 100644 index eb8de4a8a..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.fabric.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.kernelpanicsoft.archie - -import dev.architectury.platform.Mod -import dev.architectury.platform.Platform - -/** Fabric implementation of [APlatform]. */ -actual object APlatform -{ - actual val platform: String = "fabric" - -} \ No newline at end of file diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt deleted file mode 100644 index 640a5648d..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt +++ /dev/null @@ -1,25 +0,0 @@ -package net.kernelpanicsoft.archie - -import net.fabricmc.api.ClientModInitializer -import net.fabricmc.api.ModInitializer -import net.kernelpanicsoft.archie.gametest.ThreadingImpl - -/** - * Fabric entrypoint for the mod (`fabric.mod.json` `main`/`client` entrypoints). - * - * Delegates all real initialization to [Archie]; this object only wires that shared logic into - * Fabric's initializer callbacks and registers the Fabric-specific client tick pump used by - * [ThreadingImpl]. - */ -object ArchieFabric : ModInitializer, ClientModInitializer { - override fun onInitialize() - { - Archie.init() - Archie.initCommon() - } - - override fun onInitializeClient() - { - Archie.initClient() - } -} diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorFabric.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorFabric.kt deleted file mode 100644 index 13a96d563..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorFabric.kt +++ /dev/null @@ -1,22 +0,0 @@ -package net.kernelpanicsoft.archie.data - -import dev.architectury.platform.Mod -import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator -import net.minecraft.data.DataGenerator -import net.minecraft.data.DataProvider - -/** Fabric [ADataGenerator], registering providers with a Fabric [FabricDataGenerator] pack. */ -class ADataGeneratorFabric(private val fabricDataGenerator: FabricDataGenerator, override val mod: Mod) : ADataGenerator() -{ - /** Reflectively forces the created provider's `toRun` flag since Fabric's pack API always runs a provider once added. */ - override fun addProvider(run: Boolean, factory: ARegistryAwareDataProviderFactory): T - { - val pack = fabricDataGenerator.createPack() - val toRun = DataGenerator.PackGenerator::class.java.getDeclaredField("toRun") - toRun.isAccessible = true - toRun.setBoolean(pack, run) - return pack.addProvider { output, registriesFuture -> - factory(output, registriesFuture) - } - } -} \ No newline at end of file diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt deleted file mode 100644 index 771eb2f1f..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.kernelpanicsoft.archie.data - -/** Fabric implementation of [ADataGeneratorPlatform]. */ -@Suppress("unused") -actual object ADataGeneratorPlatform -{ - /** True when launched via `fabric:runDatagen`, which sets the `archie.datagen` system property. */ - actual val isDataGen: Boolean - get() = System.getProperty("archie.datagen").toBoolean() -} diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt deleted file mode 100644 index a56592555..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt +++ /dev/null @@ -1,64 +0,0 @@ -package net.kernelpanicsoft.archie.data - -import com.llamalad7.mixinextras.sugar.ref.LocalRef -import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint -import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator -import net.fabricmc.loader.api.FabricLoader -import net.fabricmc.loader.api.ModContainer -import net.fabricmc.loader.api.entrypoint.EntrypointContainer -import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform.isDataGen -import net.kernelpanicsoft.archie.events.AEvents -import net.minecraft.core.RegistrySetBuilder - -/** - * Backs `FabricDataGenHelperMixin`, which calls [addEntrypoints] mid-way through - * `FabricDataGenHelper.runInternal()` to splice in synthetic datagen entrypoints. - * - * Archie mods don't declare a `fabric-datagen` entrypoint in `fabric.mod.json`; instead they - * register with [AEvents.MODS] at init time. This object bridges that registration into the - * `EntrypointContainer` list Fabric's data generator actually consumes. - */ -internal object ADataGeneratorPlatformInternal -{ - /** - * Appends one [EntrypointContainer] per mod in [AEvents.MODS] to [dataGeneratorInitializers], - * each of which fires [AEvents.GATHER_DATA] with an [ADataGeneratorFabric] for that mod. No-op - * outside a datagen run ([ADataGeneratorPlatform.isDataGen] false). - */ - @JvmStatic - @JvmName("addEntrypoints") - internal fun addEntrypoints(dataGeneratorInitializers: LocalRef>>) - { - if (!isDataGen) return - - // Fabric expects datagen entrypoints; inject one per registered Archie mod. - val result = dataGeneratorInitializers.get().toMutableList() - for (mod in AEvents.MODS) - { - result.add(object : EntrypointContainer - { - override fun getEntrypoint(): DataGeneratorEntrypoint - { - return object : DataGeneratorEntrypoint - { - override fun onInitializeDataGenerator(fabricDataGenerator: FabricDataGenerator) - { - AEvents.GATHER_DATA.invoker()(ADataGeneratorFabric(fabricDataGenerator, mod)) - } - - override fun buildRegistry(registryBuilder: RegistrySetBuilder?) - { - super.buildRegistry(registryBuilder) - } - } - } - - override fun getProvider(): ModContainer - { - return FabricLoader.getInstance().getModContainer(mod.modId).orElse(null) - } - }) - } - dataGeneratorInitializers.set(result) - } -} \ No newline at end of file diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.fabric.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.fabric.kt deleted file mode 100644 index 76ca0324a..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.fabric.kt +++ /dev/null @@ -1,141 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.mojang.serialization.Codec -import com.mojang.serialization.MapCodec -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider -import kotlinx.serialization.json.Json -import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput -import net.fabricmc.fabric.api.datagen.v1.provider.FabricRecipeProvider -import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition -import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditionType -import net.fabricmc.fabric.api.resource.conditions.v1.ResourceConditions -import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper -import net.kernelpanicsoft.archie.serialization.kSerializer -import net.minecraft.advancements.Advancement -import net.minecraft.advancements.AdvancementHolder -import net.minecraft.core.Holder -import net.minecraft.core.HolderLookup -import net.minecraft.core.Registry -import net.minecraft.core.RegistryAccess -import net.minecraft.core.registries.BuiltInRegistries -import net.minecraft.data.recipes.RecipeOutput -import net.minecraft.data.recipes.RecipeProvider -import net.minecraft.resources.ResourceKey -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.item.crafting.Recipe -import java.util.concurrent.CompletableFuture - -/** - * Fabric implementation of [AConditionsPlatform], adapting Archie's platform-neutral [IACondition] - * onto Fabric's `ResourceCondition` API (`fabric-resource-conditions-api-v1`). - * - * Every [IACondition] is wrapped as a [FabricCondition] to cross into Fabric's condition system, - * and unwrapped again via [ResourceCondition.archie] when Archie code needs the original back. - */ -actual object AConditionsPlatform -{ - /** Fabric condition types registered via [register], keyed by their [IACondition.identifier]. */ - private val registry: MutableMap> = mutableMapOf() - - /** Registers [identifier] as a Fabric [ResourceConditionType], backed by [codec] via the [FabricCondition] wrapper. */ - actual fun register(identifier: ResourceLocation, codec: MapCodec) - { - @Suppress("UNCHECKED_CAST") - registry[identifier] = ResourceConditionType.create(identifier, (codec as MapCodec).xmap({ - it.fabric - }, { - it.condition - })) - ResourceConditions.register(registry[identifier]) - } - - /** Attaches [condition] to whatever [output] accepts next, via [FabricDataGenHelper.addConditions]. */ - actual fun withCondition(output: RecipeOutput, condition: IACondition): RecipeOutput - { - return object : RecipeOutput - { - @Suppress("UnstableApiUsage") - override fun accept(identifier: ResourceLocation, recipe: Recipe<*>, advancementEntry: AdvancementHolder?) - { - FabricDataGenHelper.addConditions(recipe, arrayOf(condition.fabric)) - Archie.LOGGER.info(Json.encodeToString(ResourceCondition.CODEC.kSerializer, condition.fabric)) - output.accept(identifier, recipe, advancementEntry) - } - - override fun advancement(): Advancement.Builder - { - return output.advancement() - } - } - } - - /** Codec for [IACondition] backed by [ResourceCondition.CODEC], round-tripping through [fabric]/[archie]. */ - actual fun codec(): Codec - { - return ResourceCondition.CODEC.xmap( - { resourceCondition -> - resourceCondition.archie - }, { iCondition -> - iCondition.fabric - } - ) - } - - /** Wraps [child] in a [FabricRecipeProvider] so its recipes go through Fabric's condition-aware output. */ - actual fun fabricRecipeProvider( - child: ARecipeProvider, - registries: CompletableFuture - ): RecipeProvider? - { - return object : FabricRecipeProvider(child.output as FabricDataOutput, registries) - { - override fun buildRecipes(exporter: RecipeOutput) - { - child.buildRecipes(exporter) - } - } - } - - /** Wraps this condition as a Fabric [ResourceCondition]. */ - val IACondition.fabric - get() = FabricCondition(this) - - /** Unwraps a Fabric [ResourceCondition] back to its originating [IACondition]. Throws if it wasn't created via [fabric]. */ - val ResourceCondition.archie - get() = ((this as? FabricCondition) ?: throw AssertionError()).condition - - /** Adapts an [IACondition] to Fabric's [ResourceCondition] interface, delegating [getType] and [test] to it. */ - class FabricCondition( - val condition: IACondition - ) : ResourceCondition - { - override fun getType(): ResourceConditionType<*> - { - return registry[condition.identifier]!! - } - - override fun test(registryLookup: HolderLookup.Provider?): Boolean - { - return registryLookup?.let { - condition.test(ConditionContext(it)) - } ?: false - } - } - - /** [IACondition.IContext] backed directly by a Fabric registry lookup, used when Fabric evaluates a condition. */ - class ConditionContext(private val registryLookup: HolderLookup.Provider) : - IACondition.IContext - { - override fun getAllTags(registry: ResourceKey>): Map>> - { - return registryLookup.lookupOrThrow(registry).listTags().toList() - .associateBy({ it.key().location }, { it.toList() }) - } - - override fun getRegistry(registry: ResourceKey>): Registry - { - return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY).registryOrThrow(registry) - } - } -} \ No newline at end of file diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.fabric.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.fabric.kt deleted file mode 100644 index 927dc88a9..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.fabric.kt +++ /dev/null @@ -1,48 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -import net.kernelpanicsoft.archie.data.common.crafting.ingredients.ACustomIngredientSerializerPlatform.fabric -import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredient -import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer -import net.minecraft.world.item.ItemStack -import net.minecraft.world.item.crafting.Ingredient - -/** Fabric implementation of [ACustomIngredientPlatform], adapting [IACustomIngredient] onto Fabric's `CustomIngredient` API. */ -actual object ACustomIngredientPlatform -{ - /** Wraps [custom] as a Fabric [CustomIngredient] and converts it to a vanilla [Ingredient]. */ - actual fun vanillaOf(custom: IACustomIngredient): Ingredient - { - return custom.fabric.toVanilla() - } - - /** Wraps this ingredient as a Fabric [CustomIngredient]. */ - val T.fabric: CustomIngredient - get() = FabricCustomIngredient(this) - - /** Adapts an [IACustomIngredient] to Fabric's [CustomIngredient] interface, delegating all matching logic to it. */ - class FabricCustomIngredient - ( - override val custom: T - ) : CustomIngredient, IACustomIngredientHolder - { - override fun test(stack: ItemStack): Boolean - { - return custom.test(stack) - } - - override fun getMatchingStacks(): MutableList - { - return custom.matchingStacks - } - - override fun requiresTesting(): Boolean - { - return custom.requiresTesting - } - - override fun getSerializer(): CustomIngredientSerializer<*> - { - return custom.serializer.fabric - } - } -} \ No newline at end of file diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.fabric.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.fabric.kt deleted file mode 100644 index 57da49326..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.fabric.kt +++ /dev/null @@ -1,69 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -import com.mojang.serialization.* -import net.fabricmc.fabric.api.recipe.v1.ingredient.CustomIngredientSerializer -import net.minecraft.network.RegistryFriendlyByteBuf -import net.minecraft.network.codec.StreamCodec -import net.minecraft.resources.ResourceLocation -import java.util.stream.Stream - -/** Fabric implementation of [ACustomIngredientSerializerPlatform], adapting [IACustomIngredientSerializer] onto Fabric's `CustomIngredientSerializer` API. */ -actual object ACustomIngredientSerializerPlatform -{ - /** Registers [serializer] with Fabric's [CustomIngredientSerializer] registry via [fabric]. */ - actual fun register(serializer: IACustomIngredientSerializer) - { - CustomIngredientSerializer.register(serializer.fabric) - } - - /** Wraps this serializer as a Fabric [CustomIngredientSerializer]. */ - val IACustomIngredientSerializer.fabric: CustomIngredientSerializer> - get() = FabricCustomIngredientSerializer(this) - - /** Adapts an [IACustomIngredientSerializer] to Fabric's [CustomIngredientSerializer] interface, wrapping/unwrapping [ACustomIngredientPlatform.FabricCustomIngredient] around the shared codec/packet codec. */ - class FabricCustomIngredientSerializer( - private val custom: IACustomIngredientSerializer - ) : CustomIngredientSerializer> - { - override fun getIdentifier(): ResourceLocation - { - return custom.identifier - } - - override fun getCodec(allowEmpty: Boolean): MapCodec> - { - val codec = custom.getCodec(allowEmpty) - return object : MapCodec>() - { - override fun encode( - input: ACustomIngredientPlatform.FabricCustomIngredient, - ops: DynamicOps, - prefix: RecordBuilder - ): RecordBuilder - { - return codec.encode(input.custom, ops, prefix) - } - - override fun keys(ops: DynamicOps): Stream - { - return codec.keys(ops) - } - - override fun decode( - ops: DynamicOps, - input: MapLike - ): DataResult> - { - return codec.decode(ops, input).map { - ACustomIngredientPlatform.FabricCustomIngredient(it) - } - } - } - } - - override fun getPacketCodec(): StreamCodec> - { - return custom.packetCodec.map({ ACustomIngredientPlatform.FabricCustomIngredient(it)}, {it.custom}) - } - } -} \ No newline at end of file diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.fabric.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.fabric.kt deleted file mode 100644 index c8207a19d..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.fabric.kt +++ /dev/null @@ -1,23 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.tags - -import net.fabricmc.fabric.impl.datagen.FabricTagBuilder -import net.minecraft.data.tags.TagsProvider -import net.minecraft.tags.* - -/** Fabric implementation of [ATagBuilderPlatform]. */ -actual object ATagBuilderPlatform -{ - /** Sets the tag's `replace` flag via Fabric's `FabricTagBuilder` extension on [TagBuilder]. */ - @Suppress("UnstableApiUsage") - actual fun setTagReplace(builder: TagBuilder, replace: Boolean) - { - (builder as FabricTagBuilder).fabric_setReplace(replace) - } - - actual fun createTagBuilder(parent: TagsProvider.TagAppender, provider: ATagsProvider): IATagBuilder - { - return ATagBuilder(parent, provider) - } - - -} \ No newline at end of file diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.fabric.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.fabric.kt deleted file mode 100644 index 8426bd1db..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.fabric.kt +++ /dev/null @@ -1,105 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import net.minecraft.Util -import net.minecraft.server.Main -import net.minecraft.server.MinecraftServer -import net.minecraft.server.dedicated.DedicatedServer -import java.nio.file.Files -import java.nio.file.Path -import java.util.Properties -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException - -/** - * Fabric implementation of [ADedicatedServerPlatform]. - * - * Boots a real dedicated server ([Main.main]) on a daemon thread and hands the resulting - * [DedicatedServer] back through [ADedicatedServerPlatformInternal], which `ServerMixin` feeds - * via [ADedicatedServerPlatformInternal.captureRunningServer] once the server instance exists. - */ -actual object ADedicatedServerPlatform { - /** Baseline `server.properties` for a headless, single-player-only GameTest server; overridden by caller-supplied properties. */ - private val defaultProperties: Properties = Util.make(Properties()) { props -> - props.setProperty("online-mode", "false") - props.setProperty("sync-chunk-writes", (Util.getPlatform() == Util.OS.WINDOWS).toString()) - props.setProperty("spawn-protection", "0") - props.setProperty("max-players", "1") - } - - /** - * Writes `server.properties`/`eula.txt` into [serverDirectory], launches vanilla's dedicated - * server entrypoint on a background thread, and blocks up to [timeoutSeconds] for it to report - * back via [ADedicatedServerPlatformInternal]. Falls back to the last captured server instance - * if the wait times out but a server is already up and listening. - */ - actual fun start(serverDirectory: Path, serverProperties: Properties, timeoutSeconds: Long): Any { - Files.createDirectories(serverDirectory) - writeServerFiles(serverDirectory, serverProperties) - - val future = ADedicatedServerPlatformInternal.beginBootstrap() - - Thread({ - try { - Main.main(arrayOf("--nogui", "--universe", serverDirectory.toAbsolutePath().toString(), "--world", "world")) - } catch (t: Throwable) { - ADedicatedServerPlatformInternal.failBootstrap(t) - } - }, "Archie Dedicated GameTest Server Bootstrap").apply { - isDaemon = true - start() - } - - val server = try { - future.get(timeoutSeconds, TimeUnit.SECONDS) - } catch (e: TimeoutException) { - ADedicatedServerPlatformInternal.clearBootstrap() - val fallbackServer = ADedicatedServerPlatformInternal.latestCapturedServer() - if (fallbackServer != null && fallbackServer.isRunning && fallbackServer.serverPort > 0) { - fallbackServer - } else { - throw IllegalStateException("Timed out waiting for dedicated server bootstrap", e) - } - } - - val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds) - while (System.nanoTime() < deadline) { - if (server.isRunning && server.serverPort > 0) break - Thread.sleep(50L) - } - - return server - } - - actual fun stop(serverInstance: Any) { - (serverInstance as? DedicatedServer)?.stopServer() - } - - actual fun port(serverInstance: Any): Int { - return (serverInstance as? DedicatedServer)?.serverPort ?: 25565 - } - - actual fun isAlive(serverInstance: Any): Boolean { - val threadMethod = serverInstance.javaClass.methods.firstOrNull { - (it.name == "getRunningThread" || it.name == "getThread") && it.parameterCount == 0 - } ?: return true - - val thread = runCatching { threadMethod.invoke(serverInstance) as? Thread }.getOrNull() - return thread?.isAlive ?: true - } - - private fun writeServerFiles(serverDirectory: Path, customProperties: Properties) { - val merged = Properties() - merged.putAll(defaultProperties) - merged.putAll(customProperties) - - Files.newBufferedWriter(serverDirectory.resolve("server.properties")).use { writer -> - merged.store(writer, "Archie GameTest dedicated server properties") - } - - Files.newBufferedWriter(serverDirectory.resolve("eula.txt")).use { writer -> - writer.write("eula=true") - writer.newLine() - } - } -} - diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt deleted file mode 100644 index 0f5163f50..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt +++ /dev/null @@ -1,46 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import net.minecraft.server.MinecraftServer -import net.minecraft.server.dedicated.DedicatedServer -import java.util.concurrent.CompletableFuture -import java.util.concurrent.atomic.AtomicReference - -/** - * Bridges [ADedicatedServerPlatform.start] on the bootstrap thread with `ServerMixin`, which calls - * [captureRunningServer] from the constructed [MinecraftServer] once it exists. - */ -object ADedicatedServerPlatformInternal { - private val bootstrapFutureRef = AtomicReference?>(null) - private val latestCapturedServerRef = AtomicReference(null) - - /** Starts a new bootstrap wait, clearing any previously captured server. Throws if a bootstrap is already in progress. */ - fun beginBootstrap(): CompletableFuture { - val future = CompletableFuture() - latestCapturedServerRef.set(null) - check(bootstrapFutureRef.compareAndSet(null, future)) { "Dedicated server bootstrap already in progress" } - return future - } - - /** Fails the in-progress bootstrap future with [error]. */ - fun failBootstrap(error: Throwable) { - bootstrapFutureRef.getAndSet(null)?.completeExceptionally(error) - } - - /** Clears the in-progress bootstrap future without resolving it, used after a timeout falls back to [latestCapturedServer]. */ - fun clearBootstrap() { - bootstrapFutureRef.set(null) - } - - /** The most recently captured [DedicatedServer], if any; used as a fallback when the bootstrap future times out. */ - fun latestCapturedServer(): DedicatedServer? = latestCapturedServerRef.get() - - /** Called by `ServerMixin` when a [MinecraftServer] instance is constructed; completes the bootstrap future if [server] is a [DedicatedServer]. */ - @JvmStatic - fun captureRunningServer(server: MinecraftServer) { - if (server is DedicatedServer) { - latestCapturedServerRef.set(server) - bootstrapFutureRef.getAndSet(null)?.complete(server) - } - } -} - diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt deleted file mode 100644 index df8b60397..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt +++ /dev/null @@ -1,51 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import dev.architectury.platform.Mod -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.events.AEvents -import net.minecraft.client.Minecraft -import java.util.concurrent.atomic.AtomicBoolean - -/** - * Kicks off the client GameTest run once the client has finished loading past its title-screen - * overlay, called from `MinecraftClientMixin.onTick` on every client tick. - */ -internal object AGameTestClientHarnessInternal { - private val hasRun = AtomicBoolean(false) - - /** - * No-ops unless this is a client-side GameTest run ([AGameTestPlatform.isGameTest] and - * [AGameTestPlatform.side] `== CLIENT`) that hasn't started yet. Otherwise registers each mod's - * test classes and runs them via [AClientGameTestHarness.run] on the dedicated test thread. - * - * The client process is always terminated afterward, on both success and failure - it must - * exit with a non-zero code on failure, since [ThreadingImpl.runTestThread] catches and - * stores any thrown exception rather than propagating it, so a plain `error(...)` throw here - * would leave the client sitting at the title screen forever instead of failing the run - * (which is what CI observed: the game never closed, so the Gradle task - and the whole - * CI job - just hung until the outer timeout killed it). - */ - @JvmStatic - fun runIfNeeded() - { - if (!AGameTestPlatform.isGameTest || AGameTestPlatform.side != AGameTestSide.CLIENT) return - if (!hasRun.compareAndSet(false, true)) return - - ThreadingImpl.runTestThread { - val mods = AEvents.MODS.ifEmpty { listOf(Archie.MOD) } - mods.forEach { mod -> AEvents.REGISTER_GAME_TEST.invoker()(mod) } - - val collected: Map>> = AGameTestPlatform.testClasses.mapValues { it.value.toList() } - val summary = AClientGameTestHarness.run(collected, AGameTestPlatform.side) - if (summary.failed > 0) { - val details = summary.failedDetails.joinToString("\n") { failure -> - " - ${failure.testId}: ${failure.rootCause}" - } - Archie.LOGGER.error("Client GameTests failed: {} failing test(s)\n{}", summary.failed, details) - kotlin.system.exitProcess(1) - } - Minecraft.getInstance().stop() - } - } -} - diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.fabric.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.fabric.kt deleted file mode 100644 index 8d4dfcdbb..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.fabric.kt +++ /dev/null @@ -1,43 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import dev.architectury.platform.Mod -import dev.architectury.platform.Platform - -/** Fabric implementation of [AGameTestPlatform]. */ -@Suppress("unused") -actual object AGameTestPlatform -{ - private const val SIDE_OVERRIDE_PROP = "archie.gametest.side" - private const val GAMETEST_PROP = "archie.gametest" - - /** - * True when running under one of Archie's own GameTest run configs. - * - * Deliberately reads [GAMETEST_PROP] instead of `FabricGameTestHelper.ENABLED` - Loom's - * generated dev-launch config shares a single property bucket per environment, so a flag set - * on the `gametestClient` run can leak into the plain `client` run's bucket too (observed on - * the NeoForge side; kept consistent here). [GAMETEST_PROP] is a property Archie's own build - * sets exclusively on its `gametest`/`gametestClient` runs, so it isn't affected by that leak. - */ - actual val isGameTest: Boolean - get() = System.getProperty(GAMETEST_PROP)?.toBoolean() == true - - actual val side: AGameTestSide? - get() { - val override = System.getProperty(SIDE_OVERRIDE_PROP)?.trim()?.lowercase() - return when (override) { - "client" -> AGameTestSide.CLIENT - "server" -> AGameTestSide.SERVER - else -> null - } - } - - val testClasses: MutableMap>> - get() = AGameTestPlatformInternal.testClasses - - actual fun register(clazz: Class<*>, mod: Mod) - { - testClasses.getOrPut(mod, ::mutableSetOf).add(clazz) - } - -} diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt deleted file mode 100644 index 5a9dabfbb..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt +++ /dev/null @@ -1,62 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import dev.architectury.platform.Mod -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.events.AEvents -import net.kernelpanicsoft.archie.gametest.AGameTestPlatform.isGameTest -import net.kernelpanicsoft.archie.mixin.fabric.FabricGameTestModInitializerMixin -import net.minecraft.gametest.framework.GameTestRegistry -import net.minecraft.gametest.framework.GlobalTestReporter - -/** Backs [AGameTestPlatform] on Fabric: holds registered test classes and drives Fabric's own GameTest registry. */ -internal object AGameTestPlatformInternal -{ - - /** Test classes registered via [AGameTestPlatform.register], keyed by owning mod. */ - @JvmField - internal val testClasses: MutableMap>> = mutableMapOf() - - /** - * No-ops unless [isGameTest]. Fires [AEvents.REGISTER_GAME_TEST] for every mod selected by - * [AGameTestModFilter] (or just [Archie.MOD] if [AEvents.MODS] is empty), then registers each - * resulting test class with [GameTestRegistry] and [FabricGameTestModInitializerMixin]'s - * id/logger bookkeeping - throwing if the same class is registered under more than one mod. - * - * Falls back to [NoOpGameTest] for a mod whose registration turns up no classes at all for the - * current [AGameTestPlatform.side] (e.g. a client-only mod's server invocation) - vanilla's - * `GameTestServer` refuses to boot with zero test functions registered anywhere. - */ - @JvmStatic - @JvmName("registerGameTests") - internal fun registerGameTests() - { - if (!isGameTest) return - Archie.LOGGER.info("Registering GameTests") - GlobalTestReporter.replaceWith(VerboseTestReporter) - val mods = AGameTestModFilter.selectMods(AEvents.MODS.ifEmpty { listOf(Archie.MOD) }) - for (mod in mods) - { - AEvents.REGISTER_GAME_TEST.invoker()(mod) - val classes = testClasses.getOrPut(mod, ::mutableSetOf) - val toRegister = if (classes.isEmpty()) setOf(NoOpGameTest::class.java) else classes - for (clazz in toRegister) - { - if (FabricGameTestModInitializerMixin.getGameTestIds().containsKey(clazz)) - { - throw UnsupportedOperationException( - "Test class (${clazz.canonicalName}) has already been registered with mod (${mod.modId})" - ) - } - - FabricGameTestModInitializerMixin.getGameTestIds()[clazz] = mod.modId - GameTestRegistry.register(clazz) - - FabricGameTestModInitializerMixin.getLogger().debug( - "Registered test class {} for mod {}", - clazz.canonicalName, - mod.modId - ) - } - } - } -} \ No newline at end of file diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.fabric.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.fabric.kt deleted file mode 100644 index 27f01b376..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.fabric.kt +++ /dev/null @@ -1,21 +0,0 @@ -package net.kernelpanicsoft.archie.gui.render - -import net.fabricmc.fabric.api.client.render.fluid.v1.FluidRenderHandlerRegistry -import net.minecraft.client.renderer.texture.TextureAtlasSprite -import net.minecraft.world.level.material.Fluid - -/** Fabric implementation of [AFluidRenderPlatform], backed by `fabric-rendering-fluids-v1`. */ -actual object AFluidRenderPlatform -{ - actual fun getStillSprite(fluid: Fluid): TextureAtlasSprite? - { - val handler = FluidRenderHandlerRegistry.INSTANCE.get(fluid) ?: return null - return handler.getFluidSprites(null, null, fluid.defaultFluidState()).getOrNull(0) - } - - actual fun getTintColor(fluid: Fluid): Int - { - val handler = FluidRenderHandlerRegistry.INSTANCE.get(fluid) ?: return -1 - return handler.getFluidColor(null, null, fluid.defaultFluidState()) - } -} diff --git a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.fabric.kt b/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.fabric.kt deleted file mode 100644 index 693faf7c6..000000000 --- a/Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.fabric.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.kernelpanicsoft.archie.registries - -import dev.architectury.platform.Mod - -/** Fabric has no staged registry-event model to race, so [block] just runs immediately. */ -actual fun scheduleEarlyClientRegistration(mod: Mod, block: () -> Unit) = block() diff --git a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/ArchieMixinPlugin.java b/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/ArchieMixinPlugin.java deleted file mode 100644 index 1b2ca28f5..000000000 --- a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/ArchieMixinPlugin.java +++ /dev/null @@ -1,57 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.fabric; - -import dev.architectury.platform.Platform; -import org.objectweb.asm.tree.ClassNode; -import org.spongepowered.asm.mixin.extensibility.IMixinConfigPlugin; -import org.spongepowered.asm.mixin.extensibility.IMixinInfo; - -import java.util.List; -import java.util.Objects; -import java.util.Set; - -public class ArchieMixinPlugin implements IMixinConfigPlugin -{ - @Override - public void onLoad(String mixinPackage) - { - - } - - @Override - public String getRefMapperConfig() - { - return null; - } - - @Override - public boolean shouldApplyMixin(String targetClassName, String mixinClassName) - { - if (Objects.equals(targetClassName, "com.terraformersmc.modmenu.ModMenu")) - return Platform.isModLoaded("modmenu"); - return true; - } - - @Override - public void acceptTargets(Set myTargets, Set otherTargets) - { - - } - - @Override - public List getMixins() - { - return null; - } - - @Override - public void preApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) - { - - } - - @Override - public void postApply(String targetClassName, ClassNode targetClass, String mixinClassName, IMixinInfo mixinInfo) - { - - } -} diff --git a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java b/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java deleted file mode 100644 index 033370dc0..000000000 --- a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.fabric; - -import com.llamalad7.mixinextras.sugar.Local; -import com.llamalad7.mixinextras.sugar.ref.LocalRef; -import net.kernelpanicsoft.archie.Archie; -import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform; -import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint; -import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper; -import net.fabricmc.loader.api.entrypoint.EntrypointContainer; -import net.kernelpanicsoft.archie.data.ADataGeneratorPlatformInternal; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.util.List; - -@SuppressWarnings("UnstableApiUsage") -@Mixin(FabricDataGenHelper.class) -class FabricDataGenHelperMixin -{ - @SuppressWarnings("UnresolvedLocalCapture") - @Inject(remap = false, method = "runInternal()V", at = @At(value = "INVOKE_ASSIGN", target = "Lnet/fabricmc/loader/api/FabricLoader;getEntrypointContainers(Ljava/lang/String;Ljava/lang/Class;)Ljava/util/List;")) - private static void addEntrypoints(CallbackInfo ci, @Local(name = "dataGeneratorInitializers") LocalRef>> dataGeneratorInitializers) - { - if (ADataGeneratorPlatform.INSTANCE.isDataGen()) - { - Archie.LOGGER.info("Registering DataGen Handlers"); - ADataGeneratorPlatformInternal.addEntrypoints(dataGeneratorInitializers); - } - } -} \ No newline at end of file diff --git a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestHelperMixin.java b/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestHelperMixin.java deleted file mode 100644 index 375c75c9f..000000000 --- a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestHelperMixin.java +++ /dev/null @@ -1,16 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.fabric; - -import net.fabricmc.fabric.impl.gametest.FabricGameTestHelper; -import net.kernelpanicsoft.archie.gametest.AGameTestPlatformInternal; -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(FabricGameTestHelper.class) -public class FabricGameTestHelperMixin { - @Inject(method = "runHeadlessServer(Lnet/minecraft/world/level/storage/LevelStorageSource$LevelStorageAccess;Lnet/minecraft/server/packs/repository/PackRepository;)V", at = @At("HEAD")) - private static void runHeadlessServer(CallbackInfo ci) { - AGameTestPlatformInternal.registerGameTests(); - } -} diff --git a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java b/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java deleted file mode 100644 index f2b750a74..000000000 --- a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.fabric; - -import net.fabricmc.fabric.impl.gametest.FabricGameTestModInitializer; -import net.kernelpanicsoft.archie.Archie; -import net.kernelpanicsoft.archie.gametest.AGameTestPlatform; -import net.kernelpanicsoft.archie.gametest.VerboseTestReporter; -import net.minecraft.gametest.framework.GlobalTestReporter; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.gen.Accessor; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.util.Map; - -@SuppressWarnings("UnstableApiUsage") -@Mixin(FabricGameTestModInitializer.class) -public interface FabricGameTestModInitializerMixin -{ - - @Accessor("GAME_TEST_IDS") - static Map, String> getGameTestIds() { return null; } - - - @Accessor("LOGGER") - static org.slf4j.Logger getLogger() { return null; } -} \ No newline at end of file diff --git a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/lifecycle/MinecraftClientMixin.java b/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/lifecycle/MinecraftClientMixin.java deleted file mode 100644 index 7799dd663..000000000 --- a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/lifecycle/MinecraftClientMixin.java +++ /dev/null @@ -1,32 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.fabric.lifecycle; - -import net.kernelpanicsoft.archie.gametest.AGameTestClientHarnessInternal; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screens.Overlay; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; -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; - -import org.jetbrains.annotations.Nullable; - -@Mixin(Minecraft.class) -public class MinecraftClientMixin { - @Unique - private boolean archie$startedClientGametests = false; - - @Shadow - @Nullable - private Overlay overlay; - - @Inject(method = "tick", at = @At("HEAD")) - private void onTick(CallbackInfo ci) { - if (!archie$startedClientGametests && overlay == null) { - archie$startedClientGametests = true; - AGameTestClientHarnessInternal.runIfNeeded(); - } - } -} - diff --git a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java b/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java deleted file mode 100644 index ba85c6110..000000000 --- a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.fabric.threading; - -import net.kernelpanicsoft.archie.gametest.ThreadingImpl; -import net.minecraft.client.Minecraft; -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(Minecraft.class) -public class MinecraftClientMixin { - @Inject(method = "run", at = @At("HEAD")) - private void archie$onRunStart(CallbackInfo ci) { - ThreadingImpl.onClientRunStart(); - } - - @Inject(method = "run", at = @At("RETURN")) - private void archie$onRunStop(CallbackInfo ci) { - ThreadingImpl.onClientRunStop(); - } - - @Inject(method = "runTick(Z)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;runAllTasks()V")) - private void archie$preRunTasks(CallbackInfo ci) { - ThreadingImpl.preRunTasks(); - } - - @Inject(method = "runTick(Z)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;runAllTasks()V", shift = At.Shift.AFTER)) - private void archie$postRunTasks(CallbackInfo ci) { - ThreadingImpl.postRunTasks(); - } - - @Inject(method = "delayCrashRaw", at = @At("HEAD")) - private void archie$onDelayCrashRaw(CallbackInfo ci) { - ThreadingImpl.setGameCrashed(); - } - - @Inject(method = "emergencySaveAndCrash", at = @At("HEAD")) - private void archie$onEmergencySaveAndCrash(CallbackInfo ci) { - ThreadingImpl.setGameCrashed(); - } -} - - - diff --git a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java b/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java deleted file mode 100644 index b1c4ed829..000000000 --- a/Archie/fabric/src/main/mixin/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.fabric.threading; - -import net.kernelpanicsoft.archie.gametest.ThreadingImpl; -import net.kernelpanicsoft.archie.gametest.ADedicatedServerPlatformInternal; -import net.minecraft.server.MinecraftServer; -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; - -/** - * Injects ThreadingImpl.onServerTick() into MinecraftServer tick cycle for GameTest coordination. - * Allows ThreadingImpl to coordinate server-side task execution with client-side gametest thread. - */ -@Mixin(MinecraftServer.class) -public class ServerMixin { - @Inject(method = "runServer", at = @At("HEAD")) - private void archie$onRunServerStart(CallbackInfo ci) { - ADedicatedServerPlatformInternal.captureRunningServer((MinecraftServer) (Object) this); - ThreadingImpl.onServerRunStart(); - } - - @Inject(method = "runServer", at = @At("RETURN")) - private void archie$onRunServerStop(CallbackInfo ci) { - ThreadingImpl.onServerRunStop(); - } - - @Inject(method = "tickServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;tickChildren(Ljava/util/function/BooleanSupplier;)V", shift = At.Shift.BEFORE)) - private void archie$onServerTick(CallbackInfo ci) { - ThreadingImpl.onServerTick(); - } -} - diff --git a/Archie/fabric/src/main/resources/archie.mixins.json b/Archie/fabric/src/main/resources/archie.mixins.json deleted file mode 100644 index fef37ef83..000000000 --- a/Archie/fabric/src/main/resources/archie.mixins.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "required": true, - "package": "net.kernelpanicsoft.archie.mixin.fabric", - "plugin": "net.kernelpanicsoft.archie.mixin.fabric.ArchieMixinPlugin", - "compatibilityLevel": "JAVA_17", - "minVersion": "0.8", - "client": [ - "lifecycle.MinecraftClientMixin", - "threading.MinecraftClientMixin" - ], - "mixins": [ - "FabricDataGenHelperMixin", - "FabricGameTestHelperMixin", - "FabricGameTestModInitializerMixin", - "threading.ServerMixin" - ], - "injectors": { - "defaultRequire": 1 - } -} \ No newline at end of file diff --git a/Archie/fabric/src/main/resources/fabric.mod.json b/Archie/fabric/src/main/resources/fabric.mod.json deleted file mode 100644 index dfbedcb24..000000000 --- a/Archie/fabric/src/main/resources/fabric.mod.json +++ /dev/null @@ -1,55 +0,0 @@ -{ - "schemaVersion": 1, - "id": "${mod_id}", - "version": "${mod_version}", - "name": "${mod_display_name}", - "description": "${mod_description}", - "authors": [ - "${mod_authors}" - ], - "contributors": [ - "${mod_credits}" - ], - "contact": { - "homepage": "${mod_url}", - "sources": "${mod_source}" - }, - "custom": { - "catalogue": { - "banner": "assets/${mod_id}/banner.png" - } - }, - "license": "${mod_license}", - "icon": "assets/${mod_id}/icon.png", - "environment": "*", - "entrypoints": { - "main": [ - { - "adapter": "kotlin", - "value": "net.kernelpanicsoft.archie.ArchieFabric" - } - ], - "client": [ - { - "adapter": "kotlin", - "value": "net.kernelpanicsoft.archie.ArchieFabric" - } - ] - }, - "mixins": [ - "${mod_id}.mixins.json", - "${mod_id}-common.mixins.json" - ], - "depends": { - "minecraft": "${versions.minecraft}", - "fabricloader": ">=${versions.fabric_loader}", - "fabric-api": ">=${versions.fabric_api}", - "fabric-language-kotlin": ">=${versions.kotlin_fabric}", - "architectury": ">=${versions.architectury}" - }, - "suggests": { - "cloth-config": ">=${versions.cloth_config_range}", - "modmenu": "*", - "catalogue": "*" - } -} \ No newline at end of file diff --git a/Archie/gradle.properties b/Archie/gradle.properties deleted file mode 100644 index 1d397d138..000000000 --- a/Archie/gradle.properties +++ /dev/null @@ -1,19 +0,0 @@ -org.gradle.jvmargs=-Xmx8G -kotlin.incremental=false -org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled -org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true - -mod_id=archie -mod_group=net.kernelpanicsoft -mod_version=1.0.0 -mod_display_name=Archie -mod_description=A library mod for Kernel Panic's mods -mod_authors=Kernel Panic -mod_credits=Both the NeoForge and fabric teams for the code I ported to Architectury and Kotlin -mod_url=https://github.com/kernel-panic-codecave/Archie -mod_source=https://github.com/kernel-panic-codecave/Archie -mod_license=GPL-3.0-or-later - -client_datagen=true -server_datagen=true - diff --git a/Archie/gradle/wrapper/gradle-wrapper.jar b/Archie/gradle/wrapper/gradle-wrapper.jar deleted file mode 100644 index a4b76b9530d66f5e68d973ea569d8e19de379189..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 43583 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 yu?e%!_ptzGB|8GrCfu%p?(_ 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 GZ~q4|mN%#X diff --git a/Archie/gradle/wrapper/gradle-wrapper.properties b/Archie/gradle/wrapper/gradle-wrapper.properties deleted file mode 100644 index cea7a793a..000000000 --- a/Archie/gradle/wrapper/gradle-wrapper.properties +++ /dev/null @@ -1,7 +0,0 @@ -distributionBase=GRADLE_USER_HOME -distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip -networkTimeout=10000 -validateDistributionUrl=true -zipStoreBase=GRADLE_USER_HOME -zipStorePath=wrapper/dists diff --git a/Archie/gradlew b/Archie/gradlew deleted file mode 100755 index f3b75f3b0..000000000 --- a/Archie/gradlew +++ /dev/null @@ -1,251 +0,0 @@ -#!/bin/sh - -# -# Copyright © 2015-2021 the original authors. -# -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# https://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -# See the License for the specific language governing permissions and -# limitations under the License. -# -# SPDX-License-Identifier: Apache-2.0 -# - -############################################################################## -# -# Gradle start up script for POSIX generated by Gradle. -# -# Important for running: -# -# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is -# noncompliant, but you have some other compliant shell such as ksh or -# bash, then to run this script, type that shell name before the whole -# command line, like: -# -# ksh Gradle -# -# Busybox and similar reduced shells will NOT work, because this script -# requires all of these POSIX shell features: -# * functions; -# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», -# «${var#prefix}», «${var%suffix}», and «$( cmd )»; -# * compound commands having a testable exit status, especially «case»; -# * various built-in commands including «command», «set», and «ulimit». -# -# Important for patching: -# -# (2) This script targets any POSIX shell, so it avoids extensions provided -# by Bash, Ksh, etc; in particular arrays are avoided. -# -# The "traditional" practice of packing multiple parameters into a -# space-separated string is a well documented source of bugs and security -# problems, so this is (mostly) avoided, by progressively accumulating -# options in "$@", and eventually passing that to Java. -# -# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, -# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; -# see the in-line comments for details. -# -# There are tweaks for specific operating systems such as AIX, CygWin, -# Darwin, MinGW, and NonStop. -# -# (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt -# within the Gradle project. -# -# You can find Gradle at https://github.com/gradle/gradle/. -# -############################################################################## - -# Attempt to set APP_HOME - -# Resolve links: $0 may be a link -app_path=$0 - -# Need this for daisy-chained symlinks. -while - APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path - [ -h "$app_path" ] -do - ls=$( ls -ld "$app_path" ) - link=${ls#*' -> '} - case $link in #( - /*) app_path=$link ;; #( - *) app_path=$APP_HOME$link ;; - esac -done - -# This is normally unused -# shellcheck disable=SC2034 -APP_BASE_NAME=${0##*/} -# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit - -# Use the maximum available, or set MAX_FD != -1 to use that value. -MAX_FD=maximum - -warn () { - echo "$*" -} >&2 - -die () { - echo - echo "$*" - echo - exit 1 -} >&2 - -# OS specific support (must be 'true' or 'false'). -cygwin=false -msys=false -darwin=false -nonstop=false -case "$( uname )" in #( - CYGWIN* ) cygwin=true ;; #( - Darwin* ) darwin=true ;; #( - MSYS* | MINGW* ) msys=true ;; #( - NONSTOP* ) nonstop=true ;; -esac - -CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar - - -# Determine the Java command to use to start the JVM. -if [ -n "$JAVA_HOME" ] ; then - if [ -x "$JAVA_HOME/jre/sh/java" ] ; then - # IBM's JDK on AIX uses strange locations for the executables - JAVACMD=$JAVA_HOME/jre/sh/java - else - JAVACMD=$JAVA_HOME/bin/java - fi - if [ ! -x "$JAVACMD" ] ; then - die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -else - JAVACMD=java - if ! command -v java >/dev/null 2>&1 - then - die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. - -Please set the JAVA_HOME variable in your environment to match the -location of your Java installation." - fi -fi - -# Increase the maximum file descriptors if we can. -if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then - case $MAX_FD in #( - max*) - # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - MAX_FD=$( ulimit -H -n ) || - warn "Could not query maximum file descriptor limit" - esac - case $MAX_FD in #( - '' | soft) :;; #( - *) - # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. - # shellcheck disable=SC2039,SC3045 - ulimit -n "$MAX_FD" || - warn "Could not set maximum file descriptor limit to $MAX_FD" - esac -fi - -# Collect all arguments for the java command, stacking in reverse order: -# * args from the command line -# * the main class name -# * -classpath -# * -D...appname settings -# * --module-path (only if needed) -# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. - -# For Cygwin or MSYS, switch paths to Windows format before running java -if "$cygwin" || "$msys" ; then - APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) - CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) - - JAVACMD=$( cygpath --unix "$JAVACMD" ) - - # Now convert the arguments - kludge to limit ourselves to /bin/sh - for arg do - if - case $arg in #( - -*) false ;; # don't mess with options #( - /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath - [ -e "$t" ] ;; #( - *) false ;; - esac - then - arg=$( cygpath --path --ignore --mixed "$arg" ) - fi - # Roll the args list around exactly as many times as the number of - # args, so each arg winds up back in the position where it started, but - # possibly modified. - # - # NB: a `for` loop captures its iteration list before it begins, so - # changing the positional parameters here affects neither the number of - # iterations, nor the values presented in `arg`. - shift # remove old arg - set -- "$@" "$arg" # push replacement arg - done -fi - - -# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' - -# Collect all arguments for the java command: -# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, -# and any embedded shellness will be escaped. -# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be -# treated as '${Hostname}' itself on the command line. - -set -- \ - "-Dorg.gradle.appname=$APP_BASE_NAME" \ - -classpath "$CLASSPATH" \ - org.gradle.wrapper.GradleWrapperMain \ - "$@" - -# Stop when "xargs" is not available. -if ! command -v xargs >/dev/null 2>&1 -then - die "xargs is not available" -fi - -# Use "xargs" to parse quoted args. -# -# With -n1 it outputs one arg per line, with the quotes and backslashes removed. -# -# In Bash we could simply go: -# -# readarray ARGS < <( xargs -n1 <<<"$var" ) && -# set -- "${ARGS[@]}" "$@" -# -# but POSIX shell has neither arrays nor command substitution, so instead we -# post-process each arg (as a line of input to sed) to backslash-escape any -# character that might be a shell metacharacter, then use eval to reverse -# that process (while maintaining the separation between arguments), and wrap -# the whole thing up as a single "set" statement. -# -# This will of course break if any of these variables contains a newline or -# an unmatched quote. -# - -eval "set -- $( - printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | - xargs -n1 | - sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | - tr '\n' ' ' - )" '"$@"' - -exec "$JAVACMD" "$@" diff --git a/Archie/gradlew.bat b/Archie/gradlew.bat deleted file mode 100644 index 9b42019c7..000000000 --- a/Archie/gradlew.bat +++ /dev/null @@ -1,94 +0,0 @@ -@rem -@rem Copyright 2015 the original author or authors. -@rem -@rem Licensed under the Apache License, Version 2.0 (the "License"); -@rem you may not use this file except in compliance with the License. -@rem You may obtain a copy of the License at -@rem -@rem https://www.apache.org/licenses/LICENSE-2.0 -@rem -@rem Unless required by applicable law or agreed to in writing, software -@rem distributed under the License is distributed on an "AS IS" BASIS, -@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -@rem See the License for the specific language governing permissions and -@rem limitations under the License. -@rem -@rem SPDX-License-Identifier: Apache-2.0 -@rem - -@if "%DEBUG%"=="" @echo off -@rem ########################################################################## -@rem -@rem Gradle startup script for Windows -@rem -@rem ########################################################################## - -@rem Set local scope for the variables with windows NT shell -if "%OS%"=="Windows_NT" setlocal - -set DIRNAME=%~dp0 -if "%DIRNAME%"=="" set DIRNAME=. -@rem This is normally unused -set APP_BASE_NAME=%~n0 -set APP_HOME=%DIRNAME% - -@rem Resolve any "." and ".." in APP_HOME to make it shorter. -for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi - -@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. -set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" - -@rem Find java.exe -if defined JAVA_HOME goto findJavaFromJavaHome - -set JAVA_EXE=java.exe -%JAVA_EXE% -version >NUL 2>&1 -if %ERRORLEVEL% equ 0 goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:findJavaFromJavaHome -set JAVA_HOME=%JAVA_HOME:"=% -set JAVA_EXE=%JAVA_HOME%/bin/java.exe - -if exist "%JAVA_EXE%" goto execute - -echo. 1>&2 -echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 -echo. 1>&2 -echo Please set the JAVA_HOME variable in your environment to match the 1>&2 -echo location of your Java installation. 1>&2 - -goto fail - -:execute -@rem Setup the command line - -set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar - - -@rem Execute Gradle -"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* - -:end -@rem End local scope for the variables with windows NT shell -if %ERRORLEVEL% equ 0 goto mainEnd - -:fail -rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of -rem the _cmd.exe /c_ return code! -set EXIT_CODE=%ERRORLEVEL% -if %EXIT_CODE% equ 0 set EXIT_CODE=1 -if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% -exit /b %EXIT_CODE% - -:mainEnd -if "%OS%"=="Windows_NT" endlocal - -:omega diff --git a/Archie/neoforge/build.gradle.kts b/Archie/neoforge/build.gradle.kts deleted file mode 100644 index 2a8125933..000000000 --- a/Archie/neoforge/build.gradle.kts +++ /dev/null @@ -1,259 +0,0 @@ -import net.kernelpanicsoft.archie.plugin.bundleMod -import net.kernelpanicsoft.archie.plugin.bundleRuntimeLibrary -import net.kernelpanicsoft.archie.plugin.runtimeLibrary -import org.jetbrains.kotlin.konan.properties.loadProperties - - -plugins { - alias(libs.plugins.shadow) - alias(libs.plugins.archie) -} - -architectury { - platformSetupLoomIde() - neoForge() -} - -actualizer { - actualizes(project(":common")) -} - -val localProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("local.properties") - val sharedPropsFile = rootDir.resolve("../local.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val sharedProperties = kotlin.runCatching { - val localPropsFile = rootDir.resolve("gradle.properties") - val sharedPropsFile = rootDir.resolve("../gradle.properties") - when { - localPropsFile.exists() -> loadProperties(localPropsFile.path) - sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) - else -> null - } -}.getOrNull() - -val String.prop: String? - get() = sharedProperties?.get(this)?.toString() - -val String.local: String? - get() = localProperties?.get(this)?.toString() - -val String.env: String? - get() = System.getenv(this) - -val String.localOrEnv: String? - get() = localProperties?.get(this)?.toString() ?: System.getenv(this.uppercase()) - - -configurations { - create("common") - create("shadowCommon") - configureEach { - // Keep NeoForge Kotlin runtime provided by KotlinLangForge only. - exclude(group = "thedarkcolour", module = "kotlinforforge-neoforge") - exclude(group = "remapped.thedarkcolour", module = "kotlinforforge-neoforge-1d1bcbf2") - } - compileClasspath.get().extendsFrom(configurations["common"]) - runtimeClasspath.get().extendsFrom(configurations["common"]) - testCompileClasspath.get().extendsFrom(compileClasspath.get()) - testRuntimeClasspath.get().extendsFrom(runtimeClasspath.get()) -// getByName("developmentNeoForge").extendsFrom(configurations["common"]) -} - -loom { - log4jConfigs.from(project(":common").loom.log4jConfigs) - accessWidenerPath.set(project(":common").loom.accessWidenerPath) - - mods { - maybeCreate("main").apply { - sourceSet(sourceSets.main.get()) - } - } - - runs { - getByName("client") { - name = "Minecraft Client" - source(sourceSets.main.get()) - vmArgs("-XX:+AllowEnhancedClassRedefinition") - property("kotlinx.coroutines.debug", "off") - } - getByName("server") { - name = "Minecraft Server" - source(sourceSets.main.get()) - vmArgs("-XX:+AllowEnhancedClassRedefinition") - property("kotlinx.coroutines.debug", "off") - } - create("datagen") { - data() - name = "Minecraft Datagen" - property("archie.datagen", "true") - property("archie.datagen.client", providers.gradleProperty("client_datagen").orElse("true").get()) - property("archie.datagen.server", providers.gradleProperty("server_datagen").orElse("true").get()) - property("kotlinx.coroutines.debug", "off") - programArgs("--all", "--mod", providers.gradleProperty("mod_id").orElse("archie").get()) - programArgs("--output", file("src/main/generated").absolutePath) - } - - create("gametest") { - server() - name = "Minecraft GameTest" - property("neoforge.enableGameTest", "true") - property("neoforge.gameTestServer", "true") - property("archie.gametest", "true") - property("archie.gametest.modid", providers.gradleProperty("mod_id").orElse("archie").get()) - property("kotlinx.coroutines.debug", "off") - providers.gradleProperty("archie.junit.gametest.function").orNull?.let { property("archie.junit.gametest.function", it) } - } - - create("gametestClient") { - client() - name = "Minecraft GameTest Client" - property("neoforge.enableGameTest", "true") - property("archie.gametest.side", "client") - property("archie.gametest", "true") - property("archie.gametest.modid", providers.gradleProperty("mod_id").orElse("archie").get()) - property("kotlinx.coroutines.debug", "off") - providers.gradleProperty("archie.junit.gametest.function").orNull?.let { property("archie.junit.gametest.function", it) } - } - } - -} - -sourceSets { - main { - resources { - srcDir("src/main/generated") - } - kotlin { - srcDir("src/main/gametest") - } - java { - srcDir("src/main/mixin") - } - } -} - -dependencies { - neoForge(libs.neoforge) - modApi(libs.architectury.neoforge) - implementation(libs.kotlin.neoforge) - compileOnly(libs.kotlinx.serialization) - bundleRuntimeLibrary(libs.kotlinx.serialization) - bundleRuntimeLibrary(libs.kotlinx.serialization.json) - bundleRuntimeLibrary(libs.kotlinx.serialization.nbt) - bundleRuntimeLibrary(libs.kotlinx.serialization.toml) - bundleRuntimeLibrary(libs.kotlinx.serialization.json5) - bundleRuntimeLibrary(libs.kotlinx.serialization.cbor) - bundleRuntimeLibrary(compose.runtime) - modRuntimeOnly(libs.rei.neoforge) - modCompileOnlyApi(libs.catalogue.neoforge) - modRuntimeOnly(libs.catalogue.neoforge) - modCompileOnlyApi(libs.clothConfig.neoforge) - modRuntimeOnly(libs.clothConfig.neoforge) - bundleMod(libs.storage.neoforge) { - exclude(group = "curse.maven") - } - - implementation(libs.junit.jupiter.api) - testImplementation(libs.junit.jupiter.api) - testRuntimeOnly(libs.junit.jupiter.engine) - // dev/test-only, not shipped - see the compileOnly note in common/build.gradle.kts - runtimeLibrary(libs.kotlinx.coroutines.test) - - "common"(project(":common", "namedElements")) { isTransitive = false } - "shadowCommon"(project(":common", "transformProductionNeoForge")) { isTransitive = false } -} - -modResources { - filesMatching.add("META-INF/neoforge.mods.toml") -} - -tasks { - base.archivesName.set(base.archivesName.get() + "-neoforge") - - test { - useJUnitPlatform() - } - - processResources { - from(project(":common").sourceSets.main.get().resources) { - include("assets/${"mod_id".prop}/**") - include("data/${"mod_id".prop}/**") - include("${"mod_id".prop}-common.mixins.json") - include("${"mod_id".prop}.common.json") - include("${"mod_id".prop}.accesswidener") - } - dependsOn(processTestResources) - } - - processTestResources { - } - - classes { - finalizedBy(testClasses) - } - - shadowJar { - exclude("fabric.mod.json") - configurations = - listOf(project.configurations.getByName("shadowCommon"), project.configurations.getByName("shadow")) - archiveClassifier.set("dev-shadow") - } - - remapJar { - inputFile.set(shadowJar.get().archiveFile) - atAccessWideners.set(setOf(loom.accessWidenerPath.get().asFile.name)) - dependsOn(shadowJar) - } - - jar.get().archiveClassifier.set("dev") - - jar { - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from(project(":common").sourceSets.main.get().output) { - // That output is common's own independently-compiled (stub-linked) classes - this - // module's own sourceSets.main.output already has a correctly-actualized copy of all - // of them via actualizes(project(":common")) above. Exclude so the stub-linked copy - // can't win the duplicatesStrategy race - it did, and threw at runtime. - exclude("net/kernelpanicsoft/archie/**") - } - } - - sourcesJar { - val commonSources = project(":common").tasks.sourcesJar - dependsOn(commonSources) - duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from(commonSources.get().archiveFile.map { zipTree(it) }) - } -} - -publishing { - publications.create("mavenNeoForge") { - artifactId = base.archivesName.get() - from(components["java"]) - } - - repositories { - mavenLocal() - maven { - name = "Reposilite" - val releasesUrl = "https://maven.kernelpanicsoft.net/releases" - val snapshotsUrl = "https://maven.kernelpanicsoft.net/snapshots" - - url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsUrl else releasesUrl) - - credentials { - username = localProperties?.getProperty("reposilite.username") - ?: System.getenv("REPOSILITE_USERNAME") - password = localProperties?.getProperty("reposilite.password") - ?: System.getenv("REPOSILITE_PASSWORD") - } - } - } -} \ No newline at end of file diff --git a/Archie/neoforge/gradle.properties b/Archie/neoforge/gradle.properties deleted file mode 100644 index 2914393db..000000000 --- a/Archie/neoforge/gradle.properties +++ /dev/null @@ -1 +0,0 @@ -loom.platform=neoforge \ No newline at end of file diff --git a/Archie/neoforge/mkdocs.yml b/Archie/neoforge/mkdocs.yml deleted file mode 100644 index 1cb8be744..000000000 --- a/Archie/neoforge/mkdocs.yml +++ /dev/null @@ -1,8 +0,0 @@ -site_name: My Docs -theme: - name: material -nav: - - Home: # your existing pages… - - index.md -# !!! EMBEDDED DOKKA START, DO NOT COMMIT !!! # -# !!! EMBEDDED DOKKA END, DO NOT COMMIT !!! # diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.neoforge.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.neoforge.kt deleted file mode 100644 index de6f98342..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.neoforge.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.kernelpanicsoft.archie - -import dev.architectury.platform.Mod -import dev.architectury.platform.Platform - -/** NeoForge implementation of [APlatform]. */ -actual object APlatform -{ - actual val platform: String = "neoforge" -} \ No newline at end of file diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt deleted file mode 100644 index a84760b99..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt +++ /dev/null @@ -1,31 +0,0 @@ -package net.kernelpanicsoft.archie - -import dev.architectury.event.events.client.ClientTickEvent -import dev.nyon.klf.MOD_BUS -import net.kernelpanicsoft.archie.gametest.ThreadingImpl -import net.neoforged.fml.common.Mod -import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent -import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent -import net.neoforged.fml.event.lifecycle.FMLConstructModEvent - -/** - * NeoForge entrypoint for the mod, registered via the `@Mod` annotation. - * - * Delegates all real initialization to [Archie], wiring its lifecycle calls into the NeoForge - * mod-bus events ([FMLConstructModEvent], [FMLClientSetupEvent], [FMLCommonSetupEvent]) and - * registering the client tick pump used by [ThreadingImpl]. - */ -@Mod(Archie.MOD_ID) -object ArchieNeoForge { - init { - MOD_BUS.addListener { - Archie.init() - } - MOD_BUS.addListener { - Archie.initClient() - } - MOD_BUS.addListener { - Archie.initCommon() - } - } -} diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorNeoForge.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorNeoForge.kt deleted file mode 100644 index 62fc48480..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorNeoForge.kt +++ /dev/null @@ -1,17 +0,0 @@ -package net.kernelpanicsoft.archie.data - -import dev.architectury.platform.Mod -import net.minecraft.data.DataProvider -import net.minecraft.data.PackOutput -import net.neoforged.neoforge.data.event.GatherDataEvent - -/** NeoForge [ADataGenerator], registering providers with the [GatherDataEvent]'s underlying generator. */ -class ADataGeneratorNeoForge(private val forgeDataGenerator: GatherDataEvent, override val mod: Mod) : ADataGenerator() -{ - override fun addProvider(run: Boolean, factory: ARegistryAwareDataProviderFactory): T - { - return forgeDataGenerator.generator.addProvider(run, DataProvider.Factory { output: PackOutput -> - factory(output, forgeDataGenerator.lookupProvider) - }) - } -} \ No newline at end of file diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt deleted file mode 100644 index a447ec655..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.kernelpanicsoft.archie.data - - -/** NeoForge implementation of [ADataGeneratorPlatform]. */ -actual object ADataGeneratorPlatform -{ - /** True when launched via `neoforge:runDatagen`, which sets the `archie.datagen` system property. */ - actual val isDataGen: Boolean - get() = System.getProperty("archie.datagen").toBoolean() -} \ No newline at end of file diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt deleted file mode 100644 index d92d46939..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt +++ /dev/null @@ -1,30 +0,0 @@ -package net.kernelpanicsoft.archie.data - -import net.kernelpanicsoft.archie.events.AEvents -import net.neoforged.fml.ModList -import net.neoforged.neoforge.data.event.GatherDataEvent - -/** Backs [ADataGeneratorPlatform] on NeoForge: wires each registered mod's [GatherDataEvent] listener into [AEvents.GATHER_DATA]. */ -internal object ADataGeneratorPlatformInternal -{ - /** - * Called from `DatagenModLoaderMixin` mid-way through NeoForge's datagen bootstrap. No-ops - * outside a datagen run; otherwise, for every mod in [AEvents.MODS], subscribes to that mod's - * [GatherDataEvent] and fires [AEvents.GATHER_DATA] with an [ADataGeneratorNeoForge] wrapping it. - */ - @JvmStatic - @JvmName("addEventHandlers") - fun addEventHandlers() - { - if (!ADataGeneratorPlatform.isDataGen) return - - for (mod in AEvents.MODS) - { - ModList.get().getModContainerById(mod.modId).ifPresent { - it.eventBus?.addListener { event -> - AEvents.GATHER_DATA.invoker()(ADataGeneratorNeoForge(event, mod)) - } - } - } - } -} \ No newline at end of file diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.neoforge.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.neoforge.kt deleted file mode 100644 index 4724d2b0e..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.neoforge.kt +++ /dev/null @@ -1,151 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.conditions - -import com.mojang.serialization.* -import dev.nyon.klf.MOD_BUS -import net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider -import net.minecraft.advancements.Advancement -import net.minecraft.advancements.AdvancementHolder -import net.minecraft.core.Holder -import net.minecraft.core.HolderLookup -import net.minecraft.core.Registry -import net.minecraft.core.RegistryAccess -import net.minecraft.core.registries.BuiltInRegistries -import net.minecraft.data.recipes.RecipeOutput -import net.minecraft.data.recipes.RecipeProvider -import net.minecraft.resources.ResourceKey -import net.minecraft.resources.ResourceLocation -import net.minecraft.world.item.crafting.Recipe -import net.neoforged.neoforge.common.conditions.ICondition -import net.neoforged.neoforge.registries.DeferredRegister -import net.neoforged.neoforge.registries.NeoForgeRegistries -import java.util.concurrent.CompletableFuture -import java.util.stream.Stream -import net.kernelpanicsoft.archie.data.common.conditions.IACondition as ArchieCondition - - -/** - * NeoForge implementation of [AConditionsPlatform], adapting Archie's platform-neutral - * [ArchieCondition] onto NeoForge's `ICondition` API. - * - * Every [ArchieCondition] is wrapped as a [NeoForgeCondition] to cross into NeoForge's condition - * system, and unwrapped again via [ICondition.archie] when Archie code needs the original back. - */ -actual object AConditionsPlatform -{ - /** Registers [identifier] as a NeoForge condition serializer, backed by [codec] via the [NeoForgeConditionCodec] wrapper. */ - actual fun register(identifier: ResourceLocation, codec: MapCodec) - { - val registry = DeferredRegister.create(NeoForgeRegistries.CONDITION_SERIALIZERS, identifier.namespace) - registry.register(identifier.path) { _ -> - codec.neoforge - } - registry.register(MOD_BUS) - } - - /** Wraps [output] so every entry accepted through it also carries [condition]. */ - actual fun withCondition(output: RecipeOutput, condition: ArchieCondition): RecipeOutput - { - return NeoForgeConditionalRecipeOutput(output, condition.neoforge) - } - - /** Codec for [ArchieCondition] backed by `ICondition.CODEC`, round-tripping through [neoforge]/[archie]. */ - actual fun codec(): Codec - { - return ICondition.CODEC.xmap({ - it.archie - }, { - it.neoforge - }) - } - - /** Fabric-only concern; always `null` on NeoForge. */ - actual fun fabricRecipeProvider(child: ARecipeProvider, registries: CompletableFuture): RecipeProvider? = null - - /** Unwraps a NeoForge [ICondition] back to its originating [ArchieCondition]. Throws if it wasn't created via [neoforge]. */ - val ICondition.archie - get() = ((this as? NeoForgeCondition) ?: throw AssertionError()).condition - - /** Wraps this condition as a NeoForge [ICondition]. */ - val ArchieCondition.neoforge - get() = NeoForgeCondition(this) - - /** Wraps this codec as a NeoForge condition-serializer codec. */ - val MapCodec.neoforge - get() = NeoForgeConditionCodec(this) - - /** Adapts an [ArchieCondition] to NeoForge's [ICondition] interface, delegating [test] to it and [codec] to the registered serializer. */ - class NeoForgeCondition( - val condition: ArchieCondition - ) : ICondition - { - override fun test(iContext: ICondition.IContext): Boolean - { - return condition.test(object : ArchieCondition.IContext - { - override fun getAllTags(registry: ResourceKey>): Map>> - { - return iContext.getAllTags(registry) - } - - override fun getRegistry(registry: ResourceKey>): Registry - { - return RegistryAccess.fromRegistryOfRegistries(BuiltInRegistries.REGISTRY).registryOrThrow(registry) - } - }) - } - - override fun codec(): MapCodec - { - return NeoForgeRegistries.CONDITION_SERIALIZERS[condition.identifier]!! - } - } - - /** Adapts a [MapCodec] of [ArchieCondition] to one producing/consuming [NeoForgeCondition] wrappers. */ - class NeoForgeConditionCodec( - private val codec: MapCodec - ) : MapCodec() - { - override fun encode( - input: NeoForgeCondition, - ops: DynamicOps, - prefix: RecordBuilder - ): RecordBuilder - { - @Suppress("UNCHECKED_CAST") - return (codec as MapCodec).encode(input.condition, ops, prefix) - } - - override fun keys(ops: DynamicOps): Stream - { - return codec.keys(ops) - } - - override fun decode(ops: DynamicOps, input: MapLike): DataResult - { - return codec.decode(ops, input).map { result -> - result.neoforge - } - } - } - - /** [RecipeOutput] wrapper that always attaches [condition] to whatever [inner] accepts. */ - class NeoForgeConditionalRecipeOutput(private val inner: RecipeOutput, private val condition: ICondition) : - RecipeOutput - { - override fun advancement(): Advancement.Builder - { - return inner.advancement() - } - - /** Forwards to [inner], attaching [condition]; any [iConditions] passed by the caller are not currently applied. */ - override fun accept( - id: ResourceLocation, - recipe: Recipe<*>, - adv: AdvancementHolder?, - vararg iConditions: ICondition - ) - { - inner.accept(id, recipe, adv, this.condition) - } - } -} \ No newline at end of file diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.neoforge.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.neoforge.kt deleted file mode 100644 index bb15eb2ed..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.neoforge.kt +++ /dev/null @@ -1,37 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -import net.kernelpanicsoft.archie.data.common.crafting.ingredients.IACustomIngredient as ArchieIngredient -import net.minecraft.world.item.ItemStack -import net.minecraft.world.item.crafting.Ingredient -import net.neoforged.neoforge.common.crafting.ICustomIngredient -import net.neoforged.neoforge.common.crafting.IngredientType -import net.neoforged.neoforge.registries.NeoForgeRegistries -import java.util.stream.Stream - -/** NeoForge implementation of [ACustomIngredientPlatform], adapting [ArchieIngredient] onto NeoForge's `ICustomIngredient` API. */ -actual object ACustomIngredientPlatform -{ - /** Wraps [custom] as a NeoForge [ICustomIngredient] and converts it to a vanilla [Ingredient]. */ - actual fun vanillaOf(custom: ArchieIngredient): Ingredient - { - return custom.neoforge.toVanilla() - } - - /** Wraps this ingredient as a NeoForge [ICustomIngredient]. */ - val T.neoforge: NeoForgeCustomIngredient - get() = NeoForgeCustomIngredient(this) - - /** Adapts an [ArchieIngredient] to NeoForge's [ICustomIngredient] interface, delegating all matching logic to it; never treated as [isSimple]. */ - class NeoForgeCustomIngredient( - override val custom: T - ) : ICustomIngredient, IACustomIngredientHolder - { - override fun test(arg: ItemStack): Boolean = custom.test(arg) - - override fun getItems(): Stream = custom.matchingStacks.stream() - - override fun isSimple(): Boolean = false - - override fun getType(): IngredientType<*> = NeoForgeRegistries.INGREDIENT_TYPES[custom.serializer.identifier]!! - } -} \ No newline at end of file diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.neoforge.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.neoforge.kt deleted file mode 100644 index 1a13eda1d..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.neoforge.kt +++ /dev/null @@ -1,57 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.crafting.ingredients - -import com.mojang.serialization.* -import dev.nyon.klf.MOD_BUS -import net.kernelpanicsoft.archie.data.common.crafting.ingredients.ACustomIngredientPlatform.neoforge -import net.neoforged.neoforge.common.crafting.IngredientType -import net.neoforged.neoforge.registries.DeferredRegister -import net.neoforged.neoforge.registries.NeoForgeRegistries -import java.util.stream.Stream - -/** NeoForge implementation of [ACustomIngredientSerializerPlatform], adapting [IACustomIngredientSerializer] onto NeoForge's `IngredientType` registry. */ -actual object ACustomIngredientSerializerPlatform -{ - /** Registers [serializer] as a NeoForge [IngredientType] via [neoforge]. */ - actual fun register(serializer: IACustomIngredientSerializer) - { - val registry = DeferredRegister.create(NeoForgeRegistries.INGREDIENT_TYPES, serializer.identifier.namespace) - registry.register(serializer.identifier.path) { _ -> serializer.neoforge } - registry.register(MOD_BUS) - } - - /** Wraps this serializer as a NeoForge [IngredientType], backed by [NeoForgeCustomIngredientCodec]. */ - val IACustomIngredientSerializer.neoforge: IngredientType> - get() = IngredientType(NeoForgeCustomIngredientCodec(this)) - - /** Adapts an [IACustomIngredientSerializer]'s codec to one producing/consuming [ACustomIngredientPlatform.NeoForgeCustomIngredient] wrappers; no packet codec since NeoForge derives sync from the data codec. */ - class NeoForgeCustomIngredientCodec( - custom: IACustomIngredientSerializer - ) : MapCodec>() - { - private val codec = custom.getCodec(false) - - override fun keys(ops: DynamicOps): Stream - { - return codec.keys(ops) - } - - override fun encode( - input: ACustomIngredientPlatform.NeoForgeCustomIngredient, - ops: DynamicOps, - prefix: RecordBuilder - ): RecordBuilder - { - return codec.encode(input.custom, ops, prefix) - } - - override fun decode( - ops: DynamicOps, - input: MapLike - ): DataResult> - { - return codec.decode(ops, input).map { - it.neoforge - } - } - } -} \ No newline at end of file diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.neoforge.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.neoforge.kt deleted file mode 100644 index 23692eeee..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.neoforge.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.kernelpanicsoft.archie.data.common.tags - -import net.minecraft.data.tags.TagsProvider -import net.minecraft.tags.TagBuilder - -/** NeoForge implementation of [ATagBuilderPlatform]. */ -actual object ATagBuilderPlatform -{ - /** Sets the tag's `replace` flag directly via vanilla's [TagBuilder.replace], which NeoForge doesn't restrict. */ - actual fun setTagReplace(builder: TagBuilder, replace: Boolean) - { - builder.replace(replace) - } - - actual fun createTagBuilder(parent: TagsProvider.TagAppender, provider: ATagsProvider): IATagBuilder - { - return ATagBuilder(parent, provider) - } -} \ No newline at end of file diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.neoforge.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.neoforge.kt deleted file mode 100644 index 8d3defcc2..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.neoforge.kt +++ /dev/null @@ -1,105 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import net.minecraft.Util -import net.minecraft.server.Main -import net.minecraft.server.MinecraftServer -import net.minecraft.server.dedicated.DedicatedServer -import java.nio.file.Files -import java.nio.file.Path -import java.util.Properties -import java.util.concurrent.TimeUnit -import java.util.concurrent.TimeoutException - -/** - * NeoForge implementation of [ADedicatedServerPlatform]. - * - * Boots a real dedicated server ([Main.main]) on a daemon thread and hands the resulting - * [DedicatedServer] back through [ADedicatedServerPlatformInternal], which `ServerMixin` feeds - * via [ADedicatedServerPlatformInternal.captureRunningServer] once the server instance exists. - */ -actual object ADedicatedServerPlatform { - /** Baseline `server.properties` for a headless, single-player-only GameTest server; overridden by caller-supplied properties. */ - private val defaultProperties: Properties = Util.make(Properties()) { props -> - props.setProperty("online-mode", "false") - props.setProperty("sync-chunk-writes", (Util.getPlatform() == Util.OS.WINDOWS).toString()) - props.setProperty("spawn-protection", "0") - props.setProperty("max-players", "1") - } - - /** - * Writes `server.properties`/`eula.txt` into [serverDirectory], launches vanilla's dedicated - * server entrypoint on a background thread, and blocks up to [timeoutSeconds] for it to report - * back via [ADedicatedServerPlatformInternal]. Falls back to the last captured server instance - * if the wait times out but a server is already up and listening. - */ - actual fun start(serverDirectory: Path, serverProperties: Properties, timeoutSeconds: Long): Any { - Files.createDirectories(serverDirectory) - writeServerFiles(serverDirectory, serverProperties) - - val future = ADedicatedServerPlatformInternal.beginBootstrap() - - Thread({ - try { - Main.main(arrayOf("--nogui", "--universe", serverDirectory.toAbsolutePath().toString(), "--world", "world")) - } catch (t: Throwable) { - ADedicatedServerPlatformInternal.failBootstrap(t) - } - }, "Archie Dedicated GameTest Server Bootstrap").apply { - isDaemon = true - start() - } - - val server = try { - future.get(timeoutSeconds, TimeUnit.SECONDS) - } catch (e: TimeoutException) { - ADedicatedServerPlatformInternal.clearBootstrap() - val fallbackServer = ADedicatedServerPlatformInternal.latestCapturedServer() - if (fallbackServer != null && fallbackServer.isRunning && fallbackServer.serverPort > 0) { - fallbackServer - } else { - throw IllegalStateException("Timed out waiting for dedicated server bootstrap", e) - } - } - - val deadline = System.nanoTime() + TimeUnit.SECONDS.toNanos(timeoutSeconds) - while (System.nanoTime() < deadline) { - if (server.isRunning && server.serverPort > 0) break - Thread.sleep(50L) - } - - return server - } - - actual fun stop(serverInstance: Any) { - (serverInstance as? DedicatedServer)?.stopServer() - } - - actual fun port(serverInstance: Any): Int { - return (serverInstance as? DedicatedServer)?.serverPort ?: 25565 - } - - actual fun isAlive(serverInstance: Any): Boolean { - val threadMethod = serverInstance.javaClass.methods.firstOrNull { - (it.name == "getRunningThread" || it.name == "getThread") && it.parameterCount == 0 - } ?: return true - - val thread = runCatching { threadMethod.invoke(serverInstance) as? Thread }.getOrNull() - return thread?.isAlive ?: true - } - - private fun writeServerFiles(serverDirectory: Path, customProperties: Properties) { - val merged = Properties() - merged.putAll(defaultProperties) - merged.putAll(customProperties) - - Files.newBufferedWriter(serverDirectory.resolve("server.properties")).use { writer -> - merged.store(writer, "Archie GameTest dedicated server properties") - } - - Files.newBufferedWriter(serverDirectory.resolve("eula.txt")).use { writer -> - writer.write("eula=true") - writer.newLine() - } - } -} - diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt deleted file mode 100644 index 0f5163f50..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt +++ /dev/null @@ -1,46 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import net.minecraft.server.MinecraftServer -import net.minecraft.server.dedicated.DedicatedServer -import java.util.concurrent.CompletableFuture -import java.util.concurrent.atomic.AtomicReference - -/** - * Bridges [ADedicatedServerPlatform.start] on the bootstrap thread with `ServerMixin`, which calls - * [captureRunningServer] from the constructed [MinecraftServer] once it exists. - */ -object ADedicatedServerPlatformInternal { - private val bootstrapFutureRef = AtomicReference?>(null) - private val latestCapturedServerRef = AtomicReference(null) - - /** Starts a new bootstrap wait, clearing any previously captured server. Throws if a bootstrap is already in progress. */ - fun beginBootstrap(): CompletableFuture { - val future = CompletableFuture() - latestCapturedServerRef.set(null) - check(bootstrapFutureRef.compareAndSet(null, future)) { "Dedicated server bootstrap already in progress" } - return future - } - - /** Fails the in-progress bootstrap future with [error]. */ - fun failBootstrap(error: Throwable) { - bootstrapFutureRef.getAndSet(null)?.completeExceptionally(error) - } - - /** Clears the in-progress bootstrap future without resolving it, used after a timeout falls back to [latestCapturedServer]. */ - fun clearBootstrap() { - bootstrapFutureRef.set(null) - } - - /** The most recently captured [DedicatedServer], if any; used as a fallback when the bootstrap future times out. */ - fun latestCapturedServer(): DedicatedServer? = latestCapturedServerRef.get() - - /** Called by `ServerMixin` when a [MinecraftServer] instance is constructed; completes the bootstrap future if [server] is a [DedicatedServer]. */ - @JvmStatic - fun captureRunningServer(server: MinecraftServer) { - if (server is DedicatedServer) { - latestCapturedServerRef.set(server) - bootstrapFutureRef.getAndSet(null)?.complete(server) - } - } -} - diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt deleted file mode 100644 index 269ecc235..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt +++ /dev/null @@ -1,51 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import dev.architectury.platform.Mod -import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.events.AEvents -import net.minecraft.client.Minecraft -import java.util.concurrent.atomic.AtomicBoolean - -/** - * Kicks off the client GameTest run once the client has finished loading past its title-screen - * overlay, called from `MinecraftClientMixin.onTick` on every client tick. - */ -internal object AGameTestClientHarnessInternal { - private val hasRun = AtomicBoolean(false) - - /** - * No-ops unless this is a client-side GameTest run ([AGameTestPlatform.isGameTest] and - * [AGameTestPlatform.side] `== CLIENT`) that hasn't started yet. Otherwise registers each mod's - * test classes and runs them via [AClientGameTestHarness.run] on the dedicated test thread. - * - * The client process is always terminated afterward, on both success and failure - it must - * exit with a non-zero code on failure, since [ThreadingImpl.runTestThread] catches and - * stores any thrown exception rather than propagating it, so a plain `error(...)` throw here - * would leave the client sitting at the title screen forever instead of failing the run - * (which is what CI observed: the game never closed, so the Gradle task - and the whole - * CI job - just hung until the outer timeout killed it). - */ - @JvmStatic - fun runIfNeeded() - { - if (!AGameTestPlatform.isGameTest || AGameTestPlatform.side != AGameTestSide.CLIENT) return - if (!hasRun.compareAndSet(false, true)) return - - ThreadingImpl.runTestThread { - val mods = AEvents.MODS.ifEmpty { listOf(Archie.MOD) } - mods.forEach { mod -> AEvents.REGISTER_GAME_TEST.invoker()(mod) } - - val collected: Map>> = AGameTestPlatformInternal.testClasses.mapValues { it.value.toList() } - val summary = AClientGameTestHarness.run(collected, AGameTestPlatform.side) - if (summary.failed > 0) { - val details = summary.failedDetails.joinToString("\n") { failure -> - " - ${failure.testId}: ${failure.rootCause}" - } - Archie.LOGGER.error("Client GameTests failed: {} failing test(s)\n{}", summary.failed, details) - kotlin.system.exitProcess(1) - } - Minecraft.getInstance().stop() - } - } -} - diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.neoforge.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.neoforge.kt deleted file mode 100644 index 2716dbe42..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.neoforge.kt +++ /dev/null @@ -1,42 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import dev.architectury.platform.Mod -import dev.architectury.platform.Platform - -/** NeoForge implementation of [AGameTestPlatform]. */ -@Suppress("unused") -actual object AGameTestPlatform -{ - private const val SIDE_OVERRIDE_PROP = "archie.gametest.side" - private const val GAMETEST_PROP = "archie.gametest" - - /** - * True when running under one of Archie's own GameTest run configs. - * - * Deliberately reads [GAMETEST_PROP] instead of `GameTestHooks.isGametestEnabled()` - Loom's - * generated dev-launch config shares a single property bucket per environment, so - * `neoforge.enableGameTest` set on the `gametestClient` run leaks into the plain `client` - * run's bucket too. [GAMETEST_PROP] is a property Archie's own build sets exclusively on its - * `gametest`/`gametestClient` runs, so it isn't affected by that leak. - */ - actual val isGameTest: Boolean - get() = System.getProperty(GAMETEST_PROP)?.toBoolean() == true - - actual val side: AGameTestSide? - get() { - val override = System.getProperty(SIDE_OVERRIDE_PROP)?.trim()?.lowercase() - return when (override) { - "client" -> AGameTestSide.CLIENT - "server" -> AGameTestSide.SERVER - else -> null - } - } - - val testClasses: MutableMap>> - get() = AGameTestPlatformInternal.testClasses - - actual fun register(clazz: Class<*>, mod: Mod) - { - testClasses.getOrPut(mod, ::mutableSetOf).add(clazz) - } -} diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt deleted file mode 100644 index 0e565abda..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt +++ /dev/null @@ -1,55 +0,0 @@ -package net.kernelpanicsoft.archie.gametest - -import dev.architectury.platform.Mod -import net.kernelpanicsoft.archie.events.AEvents -import net.neoforged.fml.ModList -import net.neoforged.neoforge.event.RegisterGameTestsEvent - -/** Backs [AGameTestPlatform] on NeoForge: holds registered test classes and drives NeoForge's own `RegisterGameTestsEvent`. */ -internal object AGameTestPlatformInternal -{ - /** Test classes registered via [AGameTestPlatform.register], keyed by owning mod. */ - @JvmField - internal val testClasses: MutableMap>> = mutableMapOf() - - /** Inverse of [testClasses]: the owning mod for each registered test class. */ - @JvmStatic - @get:JvmName("getTestClassToMod") - internal val testClassToMod: Map, Mod> - get() = buildMap { - testClasses.forEach { (mod, classes) -> - classes.forEach { put(it, mod) } - } - } - - /** - * No-ops unless [AGameTestPlatform.isGameTest]. For every mod selected by [AGameTestModFilter] - * from [AEvents.MODS], subscribes to that mod's `RegisterGameTestsEvent`; when it fires, fires - * [AEvents.REGISTER_GAME_TEST] for the mod and registers each resulting test class with - * NeoForge's event. - * - * Falls back to [NoOpGameTest] for a mod whose registration turns up no classes at all for the - * current [AGameTestPlatform.side] (e.g. a client-only mod's server invocation) - vanilla's - * `GameTestServer` refuses to boot with zero test functions registered anywhere. - */ - @JvmStatic - @JvmName("addEventHandlers") - fun addEventHandlers() - { - if (!AGameTestPlatform.isGameTest) return - - for (mod in AGameTestModFilter.selectMods(AEvents.MODS)) - { - ModList.get().getModContainerById(mod.modId).ifPresent { - it.eventBus?.addListener { event -> - AEvents.REGISTER_GAME_TEST.invoker()(mod) - val classes = testClasses.getOrPut(mod, ::mutableSetOf) - for (clazz in if (classes.isEmpty()) setOf(NoOpGameTest::class.java) else classes) - { - event.register(clazz) - } - } - } - } - } -} \ No newline at end of file diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.neoforge.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.neoforge.kt deleted file mode 100644 index 0e973b4a0..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.neoforge.kt +++ /dev/null @@ -1,19 +0,0 @@ -package net.kernelpanicsoft.archie.gui.render - -import net.minecraft.client.Minecraft -import net.minecraft.client.renderer.texture.TextureAtlas -import net.minecraft.client.renderer.texture.TextureAtlasSprite -import net.minecraft.world.level.material.Fluid -import net.neoforged.neoforge.client.extensions.common.IClientFluidTypeExtensions - -/** NeoForge implementation of [AFluidRenderPlatform], backed by [IClientFluidTypeExtensions]. */ -actual object AFluidRenderPlatform -{ - actual fun getStillSprite(fluid: Fluid): TextureAtlasSprite? - { - val loc = IClientFluidTypeExtensions.of(fluid).stillTexture ?: return null - return Minecraft.getInstance().modelManager.getAtlas(TextureAtlas.LOCATION_BLOCKS).getSprite(loc) - } - - actual fun getTintColor(fluid: Fluid): Int = IClientFluidTypeExtensions.of(fluid).tintColor -} diff --git a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.neoforge.kt b/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.neoforge.kt deleted file mode 100644 index c3c77f5df..000000000 --- a/Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.neoforge.kt +++ /dev/null @@ -1,18 +0,0 @@ -package net.kernelpanicsoft.archie.registries - -import dev.architectury.platform.Mod -import dev.architectury.platform.hooks.EventBusesHooks -import net.neoforged.neoforge.client.event.RegisterMenuScreensEvent - -/** - * [RegisterMenuScreensEvent] is the earliest point NeoForge's own client registration-stage - * events fire, and specifically the one `MenuRegistry.registerScreenFactory` itself listens - * for internally - hooking the exact same event here (on [mod]'s own bus, since it's a per-mod - * event) guarantees this fires before that internal listener would otherwise miss it. - */ -actual fun scheduleEarlyClientRegistration(mod: Mod, block: () -> Unit) -{ - EventBusesHooks.whenAvailable(mod.modId) { bus -> - bus.addListener(RegisterMenuScreensEvent::class.java) { block() } - } -} diff --git a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java b/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java deleted file mode 100644 index 0b6ecc37e..000000000 --- a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.neoforge; - -import net.kernelpanicsoft.archie.Archie; -import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform; -import net.kernelpanicsoft.archie.data.ADataGeneratorPlatformInternal; -import net.neoforged.fml.ModList; -import net.neoforged.neoforge.data.event.GatherDataEvent; -import net.neoforged.neoforge.data.loading.DatagenModLoader; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; - -import java.io.File; -import java.nio.file.Path; -import java.util.Collection; -import java.util.Set; - -@Mixin(DatagenModLoader.class) -class DatagenModLoaderMixin -{ - @Inject(method = "begin(Ljava/util/Set;Ljava/nio/file/Path;Ljava/util/Collection;Ljava/util/Collection;Ljava/util/Set;ZZZZZZLjava/lang/String;Ljava/io/File;)V", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/ModLoader;runEventGenerator(Ljava/util/function/Function;)V")) - private static void addEventHandlers(Set mods, Path path, Collection inputs, Collection existingPacks, Set existingMods, boolean serverGenerators, boolean clientGenerators, boolean devToolGenerators, boolean reportsGenerator, boolean structureValidator, boolean flat, String assetIndex, File assetsDir, CallbackInfo ci) - { - if (ADataGeneratorPlatform.INSTANCE.isDataGen()) - { - Archie.LOGGER.info("Registering DataGen Handlers"); - ADataGeneratorPlatformInternal.addEventHandlers(); - } - } -} \ No newline at end of file diff --git a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java b/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java deleted file mode 100644 index 54baa19ba..000000000 --- a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java +++ /dev/null @@ -1,64 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.neoforge; - -import net.kernelpanicsoft.archie.Archie; -import net.kernelpanicsoft.archie.gametest.AGameTestPlatform; -import net.kernelpanicsoft.archie.gametest.AGameTestPlatformInternal; -import net.kernelpanicsoft.archie.gametest.VerboseTestReporter; -import net.minecraft.gametest.framework.GameTest; -import net.minecraft.gametest.framework.GlobalTestReporter; -import net.minecraft.resources.ResourceLocation; -import net.neoforged.neoforge.gametest.GameTestHooks; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; -import dev.architectury.platform.Mod; - -import java.lang.reflect.Method; - -@Mixin(GameTestHooks.class) -public abstract class GameTestHooksMixin { - - @Inject(method = "getTemplateNamespace(Ljava/lang/reflect/Method;)Ljava/lang/String;", at = @At("HEAD"), cancellable = true) - private static void getTemplateNamespaceMixin(Method method, CallbackInfoReturnable cir) - { - GameTest gameTest = method.getAnnotation(GameTest.class); - Mod mod = AGameTestPlatformInternal.getTestClassToMod().get(method.getDeclaringClass()); - - if (gameTest.template().contains(":")) - { - ResourceLocation template = ResourceLocation.parse(gameTest.template()); - cir.setReturnValue(template.getNamespace()); - return; - } - - if (mod != null) - { - cir.setReturnValue(mod.getModId()); - return; - } - - } - - @Inject(method = "prefixGameTestTemplate(Ljava/lang/reflect/Method;)Z", at = @At("HEAD"), cancellable = true) - private static void prefixGameTestTemplateMixin(Method method, CallbackInfoReturnable cir) - { - GameTest gameTest = method.getAnnotation(GameTest.class); - if (gameTest.template().contains(":")) - { - cir.setReturnValue(false); - } - } - - @Inject(method = "registerGametests()V", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/ModLoader;postEvent(Lnet/neoforged/bus/api/Event;)V")) - private static void registerGametests(CallbackInfo ci) - { - if (AGameTestPlatform.INSTANCE.isGameTest()) - { - Archie.LOGGER.info("Registering GameTests"); - GlobalTestReporter.replaceWith(VerboseTestReporter.INSTANCE); - AGameTestPlatformInternal.addEventHandlers(); - } - } -} diff --git a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/GameTestRegistryMixin.java b/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/GameTestRegistryMixin.java deleted file mode 100644 index 495dc3d37..000000000 --- a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/GameTestRegistryMixin.java +++ /dev/null @@ -1,42 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.neoforge; - -import net.minecraft.gametest.framework.*; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.world.level.block.Rotation; -import net.neoforged.neoforge.gametest.GameTestHooks; -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; - -import java.lang.reflect.Method; -import java.util.function.Consumer; - -@Mixin(GameTestRegistry.class) -public abstract class GameTestRegistryMixin { - @Shadow - private static Consumer turnMethodIntoConsumer(Method testMethod) { - return null; - } - - @Inject(method = "turnMethodIntoTestFunction(Ljava/lang/reflect/Method;)Lnet/minecraft/gametest/framework/TestFunction;", at = @At("HEAD"), cancellable = true) - private static void turnMethodIntoTestFunctionMixin(Method testMethod, CallbackInfoReturnable cir) - { - GameTest gameTest = testMethod.getAnnotation(GameTest.class); - if (gameTest.template().contains(":")) - { - ResourceLocation template = ResourceLocation.parse(gameTest.template()); - - String s = testMethod.getDeclaringClass().getSimpleName(); - String s1 = s.toLowerCase(); - boolean prefixGameTestTemplate = GameTestHooks.prefixGameTestTemplate(testMethod); - String s2 = (prefixGameTestTemplate ? s1 + "." : "") + testMethod.getName().toLowerCase(); - String s3 = GameTestHooks.getTemplateNamespace(testMethod) + ":" + (prefixGameTestTemplate ? s1 + "." : "") + template.getPath(); - String s4 = gameTest.batch(); - Rotation rotation = StructureUtils.getRotationForRotationSteps(gameTest.rotationSteps()); - - cir.setReturnValue(new TestFunction(s4, s2, s3, rotation, gameTest.timeoutTicks(), gameTest.setupTicks(), gameTest.required(), gameTest.manualOnly(), gameTest.requiredSuccesses(), gameTest.attempts(), gameTest.skyAccess(), turnMethodIntoConsumer(testMethod))); - } - } -} diff --git a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/StructureTemplateManagerMixin.java b/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/StructureTemplateManagerMixin.java deleted file mode 100644 index 4f2b59dc6..000000000 --- a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/StructureTemplateManagerMixin.java +++ /dev/null @@ -1,78 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.neoforge; - -import com.google.common.collect.ImmutableList; -import com.llamalad7.mixinextras.sugar.Local; -import com.mojang.brigadier.exceptions.CommandSyntaxException; -import com.mojang.datafixers.DataFixer; -import net.minecraft.core.HolderGetter; -import net.minecraft.nbt.CompoundTag; -import net.minecraft.nbt.NbtUtils; -import net.minecraft.resources.FileToIdConverter; -import net.minecraft.resources.ResourceLocation; -import net.minecraft.server.packs.resources.Resource; -import net.minecraft.server.packs.resources.ResourceManager; -import net.minecraft.world.level.block.Block; -import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplate; -import net.minecraft.world.level.levelgen.structure.templatesystem.StructureTemplateManager; -import net.minecraft.world.level.storage.LevelStorageSource; -import org.apache.commons.io.IOUtils; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Mutable; -import org.spongepowered.asm.mixin.Shadow; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.Inject; -import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; -import org.spongepowered.asm.mixin.injection.callback.LocalCapture; - -import java.io.IOException; -import java.io.InputStream; -import java.util.List; -import java.util.Optional; -import java.util.stream.Stream; - -@Mixin(StructureTemplateManager.class) -public abstract class StructureTemplateManagerMixin { - @Unique - private static final String GAMETEST_STRUCTURE_PATH = "gametest/structures"; - @Unique - private static final FileToIdConverter GAMETEST_STRUCTURE_FINDER = new FileToIdConverter(GAMETEST_STRUCTURE_PATH, ".snbt"); - - @Shadow - private ResourceManager resourceManager; - - @Shadow public abstract StructureTemplate readStructure(CompoundTag nbt); - - - @Shadow public List sources; - - @Unique - private Optional archie_loadSnbtFromResource(ResourceLocation id) { - ResourceLocation path = GAMETEST_STRUCTURE_FINDER.idToFile(id); - Optional resource = this.resourceManager.getResource(path); - - if (resource.isPresent()) { - try { - String snbt = IOUtils.toString(resource.get().openAsReader()); - CompoundTag nbt = NbtUtils.snbtToStructure(snbt); - return Optional.of(this.readStructure(nbt)); - } catch (IOException | CommandSyntaxException e) { - throw new RuntimeException("Failed to load GameTest structure " + id, e); - } - } - - return Optional.empty(); - } - - @Unique - private Stream archie_streamTemplatesFromResource() { - FileToIdConverter finder = GAMETEST_STRUCTURE_FINDER; - return finder.listMatchingResources(this.resourceManager).keySet().stream().map(finder::fileToId); - } - - @Inject(method = "", at = @At(value = "RETURN")) - private void addFabricTemplateProvider(ResourceManager resourceManager, LevelStorageSource.LevelStorageAccess levelStorageAccess, DataFixer fixerUpper, HolderGetter blockLookup, CallbackInfo ci, @Local ImmutableList.Builder builder) { - builder.add(new StructureTemplateManager.Source(this::archie_loadSnbtFromResource, this::archie_streamTemplatesFromResource)); - this.sources = builder.build(); - } -} \ No newline at end of file diff --git a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenDepthMixin.java b/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenDepthMixin.java deleted file mode 100644 index bf054b7fc..000000000 --- a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenDepthMixin.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.neoforge.client.gui; - -import com.mojang.blaze3d.systems.RenderSystem; -import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthProvider; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; -import org.lwjgl.opengl.GL11; -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(AbstractContainerScreen.class) -public abstract class AbstractContainerScreenDepthMixin { - @Inject(method = "render", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/systems/RenderSystem;disableDepthTest()V", shift = At.Shift.AFTER)) - private void archie$restoreDepth(GuiGraphics guiGraphics, int mouseX, int mouseY, float partialTick, CallbackInfo ci) { - if (this instanceof SlotLayerDepthProvider) { - RenderSystem.enableDepthTest(); - RenderSystem.depthMask(true); - RenderSystem.depthFunc(GL11.GL_LEQUAL); - } - } -} - diff --git a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenMixin.java b/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenMixin.java deleted file mode 100644 index d85d0e495..000000000 --- a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenMixin.java +++ /dev/null @@ -1,91 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.neoforge.client.gui; - -import net.kernelpanicsoft.archie.Archie; -import net.kernelpanicsoft.archie.gui.access.SlotHighlightClipProvider; -import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthContext; -import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthProvider; -import net.kernelpanicsoft.archie.gui.layout.IntRect; -import net.minecraft.client.gui.GuiGraphics; -import net.minecraft.client.gui.screens.Screen; -import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; -import net.minecraft.client.gui.screens.inventory.MenuAccess; -import net.minecraft.network.chat.Component; -import net.minecraft.world.inventory.AbstractContainerMenu; -import net.minecraft.world.inventory.Slot; -import net.minecraft.world.item.ItemStack; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.ModifyArgs; -import org.spongepowered.asm.mixin.injection.Redirect; -import org.spongepowered.asm.mixin.injection.invoke.arg.Args; - -@Mixin(AbstractContainerScreen.class) -public abstract class AbstractContainerScreenMixin extends Screen implements MenuAccess { - @Unique - private Float archie$slotDepthOverride; - - protected AbstractContainerScreenMixin(Component title) { - super(title); - } - - @ModifyArgs(method = "renderSlot", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;translate(FFF)V")) - private void archie$adjustSlotLayer(Args args, GuiGraphics guiGraphics, Slot slot) { - float adjustedZ = args.get(2); - archie$slotDepthOverride = null; - if (this instanceof SlotLayerDepthProvider provider) { - Float custom = provider.slotRenderLayerOffset(slot); - archie$slotDepthOverride = custom; - if (custom != null) { - adjustedZ = custom; - Archie.LOGGER.debug("Adjusting slot layer depth for {} to {}", slot, custom); - } - } - args.set(2, adjustedZ); - } - - @Redirect(method = "renderSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;renderItem(Lnet/minecraft/world/item/ItemStack;III)V")) - private void archie$wrapSlotItemRender(GuiGraphics guiGraphics, ItemStack stack, int x, int y, int seed) { - boolean pushed = false; - if (archie$slotDepthOverride != null) { - SlotLayerDepthContext.push(archie$slotDepthOverride); - pushed = true; - } - try { - guiGraphics.renderItem(stack, x, y, seed); - } finally { - if (pushed) { - SlotLayerDepthContext.pop(); - } - archie$slotDepthOverride = null; - } - } - - /** - * Vanilla's per-slot hover highlight is drawn via a static helper that only takes the - * slot's raw x/y/blitOffset - there's no per-slot instance override point to clip it the - * way {@link #archie$adjustSlotLayer} clips the item icon, so this redirects the call - * site directly instead. - */ - @Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;renderSlotHighlight(Lnet/minecraft/client/gui/GuiGraphics;III)V")) - private void archie$clipSlotHighlight(GuiGraphics guiGraphics, int x, int y, int blitOffset) { - IntRect clip = null; - if (this instanceof SlotHighlightClipProvider provider) { - clip = provider.slotHighlightClipRect(x, y); - if (clip == null) { - return; - } - } - if (clip != null) { - guiGraphics.enableScissor(clip.getMinX(), clip.getMinY(), clip.getMaxX(), clip.getMaxY()); - } - try { - AbstractContainerScreen.renderSlotHighlight(guiGraphics, x, y, blitOffset); - } finally { - if (clip != null) { - guiGraphics.disableScissor(); - } - } - } -} - diff --git a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/GuiGraphicsMixin.java b/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/GuiGraphicsMixin.java deleted file mode 100644 index 3a8474a94..000000000 --- a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/GuiGraphicsMixin.java +++ /dev/null @@ -1,36 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.neoforge.client.gui; - -import net.kernelpanicsoft.archie.Archie; -import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthContext; -import net.minecraft.client.gui.GuiGraphics; -import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; -import org.spongepowered.asm.mixin.injection.At; -import org.spongepowered.asm.mixin.injection.ModifyArgs; -import org.spongepowered.asm.mixin.injection.invoke.arg.Args; - -@Mixin(GuiGraphics.class) -public abstract class GuiGraphicsMixin { - @Unique - private static final float ITEM_TRANSLATE_Z = 150.0F; - - @ModifyArgs( - method = "renderItem(Lnet/minecraft/world/entity/LivingEntity;Lnet/minecraft/world/level/Level;Lnet/minecraft/world/item/ItemStack;IIII)V", - at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;translate(FFF)V") - ) - private void archie$flattenSlotItemDepth(Args args) { - if (!SlotLayerDepthContext.isActive()) { - return; - } - float originalZ = args.get(2); - float adjusted = originalZ - ITEM_TRANSLATE_Z; - Archie.LOGGER.debug( - "Slot depth context active: target={}, translate={} -> {}", - SlotLayerDepthContext.currentDepth(), - originalZ, - adjusted - ); - args.set(2, adjusted); - } -} - diff --git a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/lifecycle/MinecraftClientMixin.java b/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/lifecycle/MinecraftClientMixin.java deleted file mode 100644 index 126fa4382..000000000 --- a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/lifecycle/MinecraftClientMixin.java +++ /dev/null @@ -1,31 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.neoforge.lifecycle; - -import net.kernelpanicsoft.archie.gametest.AGameTestClientHarnessInternal; -import net.minecraft.client.Minecraft; -import net.minecraft.client.gui.screens.Overlay; -import org.jetbrains.annotations.Nullable; -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(Minecraft.class) -public class MinecraftClientMixin { - @Unique - private boolean archie$startedClientGametests = false; - - @Shadow - @Nullable - private Overlay overlay; - - @Inject(method = "tick", at = @At("HEAD")) - private void onTick(CallbackInfo ci) { - if (!archie$startedClientGametests && overlay == null) { - archie$startedClientGametests = true; - AGameTestClientHarnessInternal.runIfNeeded(); - } - } -} - diff --git a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java b/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java deleted file mode 100644 index 262da08ba..000000000 --- a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java +++ /dev/null @@ -1,44 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.neoforge.threading; - -import net.kernelpanicsoft.archie.gametest.ThreadingImpl; -import net.minecraft.client.Minecraft; -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(Minecraft.class) -public class MinecraftClientMixin { - @Inject(method = "run", at = @At("HEAD")) - private void archie$onRunStart(CallbackInfo ci) { - ThreadingImpl.onClientRunStart(); - } - - @Inject(method = "run", at = @At("RETURN")) - private void archie$onRunStop(CallbackInfo ci) { - ThreadingImpl.onClientRunStop(); - } - - @Inject(method = "runTick(Z)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;runAllTasks()V")) - private void archie$preRunTasks(CallbackInfo ci) { - ThreadingImpl.preRunTasks(); - } - - @Inject(method = "runTick(Z)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/Minecraft;runAllTasks()V", shift = At.Shift.AFTER)) - private void archie$postRunTasks(CallbackInfo ci) { - ThreadingImpl.postRunTasks(); - } - - @Inject(method = "delayCrashRaw", at = @At("HEAD")) - private void archie$onDelayCrashRaw(CallbackInfo ci) { - ThreadingImpl.setGameCrashed(); - } - - @Inject(method = "emergencySaveAndCrash", at = @At("HEAD")) - private void archie$onEmergencySaveAndCrash(CallbackInfo ci) { - ThreadingImpl.setGameCrashed(); - } -} - - - diff --git a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java b/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java deleted file mode 100644 index 15636d026..000000000 --- a/Archie/neoforge/src/main/mixin/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java +++ /dev/null @@ -1,33 +0,0 @@ -package net.kernelpanicsoft.archie.mixin.neoforge.threading; - -import net.kernelpanicsoft.archie.gametest.ThreadingImpl; -import net.kernelpanicsoft.archie.gametest.ADedicatedServerPlatformInternal; -import net.minecraft.server.MinecraftServer; -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; - -/** - * Injects ThreadingImpl.onServerTick() into MinecraftServer tick cycle for GameTest coordination. - * Allows ThreadingImpl to coordinate server-side task execution with client-side gametest thread. - */ -@Mixin(MinecraftServer.class) -public class ServerMixin { - @Inject(method = "runServer", at = @At("HEAD")) - private void archie$onRunServerStart(CallbackInfo ci) { - ADedicatedServerPlatformInternal.captureRunningServer((MinecraftServer) (Object) this); - ThreadingImpl.onServerRunStart(); - } - - @Inject(method = "runServer", at = @At("RETURN")) - private void archie$onRunServerStop(CallbackInfo ci) { - ThreadingImpl.onServerRunStop(); - } - - @Inject(method = "tickServer", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/MinecraftServer;tickChildren(Ljava/util/function/BooleanSupplier;)V", shift = At.Shift.BEFORE)) - private void archie$onServerTick(CallbackInfo ci) { - ThreadingImpl.onServerTick(); - } -} - diff --git a/Archie/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/Archie/neoforge/src/main/resources/META-INF/neoforge.mods.toml deleted file mode 100644 index 039e9a0f3..000000000 --- a/Archie/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ /dev/null @@ -1,58 +0,0 @@ -modLoader = "klf" -loaderVersion = "[${versions.kotlin_neoforge_range},)" -issueTrackerURL = "" -license = "${mod_license}" - -[[mods]] -modId = "${mod_id}" -version = "${mod_version}" -displayName = "${mod_display_name}" -authors = "${mod_authors}" -credits = "${mod_credits}" -description = ''' -${mod_description} -''' -logoFile = "assets/${mod_id}/banner.png" -displayURL = "${mod_url}" - -[[mixins]] -config = "archie.mixins.json" - - -[[dependencies."${mod_id}"]] -modId = "neoforge" -type = "required" -versionRange = "[${versions.neoforge_range},)" -ordering = "NONE" -side = "BOTH" - -[[dependencies."${mod_id}"]] -modId = "minecraft" -type = "required" -versionRange = "[${versions.minecraft}]" -ordering = "NONE" -side = "BOTH" - -#[[dependencies."${mod_id}"]] -#modId = "klf" -#type = "required" -#versionRange = "[${versions.kotlin_neoforge_range},)" -#ordering = "NONE" -#side = "BOTH" - -[[dependencies."${mod_id}"]] -modId = "cloth_config" -type = "optional" -versionRange = "[${versions.cloth_config_range},)" -ordering = "NONE" -side = "BOTH" - -[[dependencies."${mod_id}"]] -modId = "yet_another_config_lib_v3" -type = "optional" -versionRange = "*" -ordering = "NONE" -side = "BOTH" - -[modproperties."${mod_id}"] -catalogueImageIcon = "assets/${mod_id}/icon.png" \ No newline at end of file diff --git a/Archie/neoforge/src/main/resources/archie.mixins.json b/Archie/neoforge/src/main/resources/archie.mixins.json deleted file mode 100644 index e0f9f55c3..000000000 --- a/Archie/neoforge/src/main/resources/archie.mixins.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "required": true, - "package": "net.kernelpanicsoft.archie.mixin.neoforge", - "compatibilityLevel": "JAVA_17", - "minVersion": "0.8", - "client": [ - "lifecycle.MinecraftClientMixin", - "threading.MinecraftClientMixin" - ], - "mixins": [ - "DatagenModLoaderMixin", - "GameTestHooksMixin", - "GameTestRegistryMixin", - "StructureTemplateManagerMixin", - "threading.ServerMixin" - ], - "injectors": { - "defaultRequire": 1 - } -} \ No newline at end of file diff --git a/Archie/neoforge/src/main/resources/pack.mcmeta b/Archie/neoforge/src/main/resources/pack.mcmeta deleted file mode 100644 index 3fef55f02..000000000 --- a/Archie/neoforge/src/main/resources/pack.mcmeta +++ /dev/null @@ -1,6 +0,0 @@ -{ - "pack": { - "description": "Archie", - "pack_format": 22 - } -} diff --git a/Archie/settings.gradle.kts b/Archie/settings.gradle.kts deleted file mode 100644 index 98a3553bc..000000000 --- a/Archie/settings.gradle.kts +++ /dev/null @@ -1,36 +0,0 @@ -import org.gradle.kotlin.dsl.maven - -enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") - -pluginManagement { - repositories { - maven("https://maven.fabricmc.net/") - maven("https://maven.architectury.dev/") - maven("https://maven.minecraftforge.net/") - maven("https://maven.neoforged.net/releases/") - maven("https://maven.firstdarkdev.xyz/releases") - maven { - name = "kernelpanic releases" - url = uri("https://maven.kernelpanicsoft.net/releases") - } - maven { - name = "kernelpanic snapshots" - url = uri("https://maven.kernelpanicsoft.net/snapshots") - } - mavenLocal() - gradlePluginPortal() - } -} - -dependencyResolutionManagement { - versionCatalogs { - create("libs") { - from(files("../gradle/libs.versions.toml")) - } - } -} - -include("common", "fabric", "neoforge") - -rootProject.name = "Archie" - diff --git a/Archie/CHANGELOG.md b/CHANGELOG.md similarity index 100% rename from Archie/CHANGELOG.md rename to CHANGELOG.md diff --git a/README.md b/README.md index 1d6ea6b50..64c9cd6d4 100644 --- a/README.md +++ b/README.md @@ -4,68 +4,72 @@ Archie is a Kotlin-first Architectury library mod for Minecraft 1.21.1. It provides shared utilities used by Kernel Panic mods across Fabric and NeoForge. Full guides (config, GUI, networking, serialization, etc.) live at -[docs.kernelpanicsoft.net/Archie](https://docs.kernelpanicsoft.net/Archie/) and in [`Archie/docs/`](Archie/docs). +[docs.kernelpanicsoft.net/Archie](https://docs.kernelpanicsoft.net/Archie/) and in [`docs/`](docs). ## Repo layout -This repository root is a Gradle **composite build** (`settings.gradle.kts`) that just wires -together two independent Gradle builds: +This repository root is a single Gradle build (`settings.gradle.kts`) using Architectury Loom, +with four products, each split into `common`/`fabric`/`neoforge` subprojects nested under one +directory per product and flattened to single-level project names (e.g. `core/fabric` → +`archie-core-fabric`), matching `terrarium-earth/Common-Storage-Lib`'s layout: -- [`Archie/`](Archie): the library itself — `common/`, `fabric/`, `neoforge/` subprojects, plus - the `docs/` site sources. This is what gets published. -- [`Archie-Test/`](Archie-Test): a throwaway playground mod (`common-test`/`fabric-test`/`neoforge-test`) - used to exercise Archie during development; it substitutes in Archie's project sources instead - of a published artifact, so changes in `Archie/` are picked up live. +- [`core/`](core) (`archie-core-{common,fabric,neoforge}`): the library itself - `Archie`, + networking, config, GUI, transfer, registries, resource packs. This is what gets published. +- [`datagen/`](datagen) (`archie-datagen-{common,fabric,neoforge}`): Archie's datagen DSL. Ships as + its own separate mod (`archie_datagen`), dev-time only - never on a real player's classpath. +- [`gametest/`](gametest) (`archie-gametest-{common,fabric,neoforge}`): Archie's GameTest + framework/harness. Also its own separate mod (`archie_gametest`), dev/test-time only. +- [`test/`](test) (`archie-test-{common,fabric,neoforge}`): a throwaway playground mod + (`archie_test`) used to exercise Archie during development - depends on `core`/`datagen`/ + `gametest` via plain project references, so changes there are picked up live. -Because these are separate builds, run all Gradle commands **from inside `Archie/` (or -`Archie-Test/`)**, not the repo root — the root `build.gradle.kts` has no real tasks of its own. +Run all Gradle commands from the repo root. -Inside `Archie/`, the module layout is: +Each product's module layout is the same shape: -- `common/`: shared APIs and core implementation (`Archie`, networking, config, GUI, data helpers) +- `common/`: shared APIs and core implementation - `fabric/`: Fabric entrypoints, run configs, platform `actual` implementations - `neoforge/`: NeoForge entrypoints, run configs, platform `actual` implementations The initialization flow is: -1. Loader entrypoint (`ArchieFabric` or `ArchieNeoForge`) -2. `Archie.init()` in `common` -3. Shared systems register events/network/config and optional datagen/gametest hooks +1. Loader entrypoint (`ArchieFabric` or `ArchieNeoForge`, in `core/{fabric,neoforge}`) +2. `Archie.init()` in `core/common` +3. Shared systems register events/network/config; datagen/gametest activate via a + `ServiceLoader`-based `ArchieExtension` hook if `archie-datagen`/`archie-gametest` are present ## Common workflows -Run from inside `Archie/` (`cd Archie` first, or pass `--project-dir Archie`): - ```bash ./gradlew build -./gradlew fabric:runClient -./gradlew neoforge:runClient -./gradlew fabric:runDatagen -./gradlew neoforge:runDatagen -./gradlew fabric:runGametest -./gradlew neoforge:runGametest -./gradlew fabric:runGametestClient -./gradlew neoforge:runGametestClient +./gradlew archie-core-fabric:runClient +./gradlew archie-core-neoforge:runClient +./gradlew archie-datagen-fabric:runDatagen +./gradlew archie-datagen-neoforge:runDatagen +./gradlew archie-gametest-fabric:runGametest +./gradlew archie-gametest-neoforge:runGametest +./gradlew archie-gametest-fabric:runGametestClient +./gradlew archie-gametest-neoforge:runGametestClient ``` -`build` and `assemble` finalize with `fusejars` (merged Fabric+NeoForge artifact). - -To run the `Archie-Test` playground mod, use the same commands from inside `Archie-Test/` instead -(e.g. `./gradlew fabric-test:runClient`). +To run the `archie-test` playground mod, use the same task names against its own modules instead +(e.g. `./gradlew archie-test-fabric:runClient`). ## Conventions that matter - Add gameplay/library logic in `common` first, then platform-specific `actual` code only when needed. - Keep `expect/actual` triplets named `*.common.kt`, `*.fabric.kt`, `*.neoforge.kt`. -- Register packet classes and handlers before calling `register()` on `NetworkChannel`. +- Register packet classes and handlers before calling `register()` on `ArchieNetworkChannel`. - Keep loader manifests tokenized (`${mod_id}`, `${versions.*}`); values come from Gradle properties/version catalog. - Use `bundleRuntimeLibrary(...)` / `bundleMod(...)` for shipped runtime deps in loader modules. +- Runtime-needed-by-`Archie.kt` code (conditions, ingredients, common tags) lives in `core`, even + though the datagen DSL that also touches it lives in `datagen` - `core` never depends on + `datagen`/`gametest`. ## Key files -- `Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt` -- `Archie/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt` -- `Archie/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt` -- `Archie/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt` +- `core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt` +- `core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt` +- `core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt` +- `core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt` - `gradle/libs.versions.toml` - diff --git a/build.gradle.kts b/build.gradle.kts index ba8366316..621e05218 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,48 +1,176 @@ +import net.fabricmc.loom.api.LoomGradleExtensionAPI +import org.jetbrains.kotlin.konan.properties.loadProperties + plugins { - base + java + alias(libs.plugins.architectury) + id("net.kernelpanicsoft.actualizer") version "0.1.0" apply false + alias(libs.plugins.architectury.loom) apply false + alias(libs.plugins.kotlin.jvm) + alias(libs.plugins.kotlin.serialization) + alias(libs.plugins.kotlin.compose) + alias(libs.plugins.compose) + alias(libs.plugins.dokka.mkdocs) +} + +architectury.minecraft = libs.versions.minecraft.get() + +val sharedProperties = kotlin.runCatching { + val localPropsFile = rootDir.resolve("gradle.properties") + val sharedPropsFile = rootDir.resolve("../gradle.properties") + when { + localPropsFile.exists() -> loadProperties(localPropsFile.path) + sharedPropsFile.exists() -> loadProperties(sharedPropsFile.path) + else -> null + } +}.getOrNull() + +val String.prop: String? + get() = sharedProperties?.get(this)?.toString() + +val String.localOrEnv: String? + get() = System.getenv(this.uppercase()) + +subprojects { + apply(plugin = "dev.architectury.loom") + apply(plugin = "net.kernelpanicsoft.actualizer") + + val loom = project.extensions.getByName("loom") + + configure { + silentMojangMappingsLicense() + } + + repositories { + val githubUsername = "github_actor".localOrEnv + val githubToken = "github_token".localOrEnv + mavenCentral() + mavenLocal() + google { + content { + includeGroupByRegex("androidx\\..*") + includeGroupByRegex("com\\.android.*") + } + } + maven { + name = "kernelpanic releases" + url = uri("https://maven.kernelpanicsoft.net/releases") + } + maven { + name = "kernelpanic snapshots" + url = uri("https://maven.kernelpanicsoft.net/snapshots") + } + maven("https://maven.pkg.jetbrains.space/public/p/compose/dev") + maven("https://maven.parchmentmc.org") + maven("https://maven.fabricmc.net/") + maven("https://maven.neoforged.net/releases/") + maven("https://maven.terraformersmc.com/releases/") + maven("https://repo.nyon.dev/releases") + maven("https://maven.isxander.dev/releases") { + name = "Xander Maven" + } + maven("https://maven.resourcefulbees.com/repository/maven-public/") { + content { + includeGroup("earth.terrarium.common_storage_lib") + } + } + maven { + url = uri("https://maven.pkg.github.com/MrCrayfish/Maven") + credentials { + username = githubUsername + password = githubToken + } + } + maven { + url = uri("https://www.cursemaven.com") + content { + includeGroup("curse.maven") + } + } + } + + @Suppress("UnstableApiUsage") + dependencies { + "minecraft"(rootProject.libs.minecraft) + "mappings"(loom.layered { + officialMojangMappings() + parchment(rootProject.libs.parchment) + }) + + compileOnly("org.jetbrains:annotations:24.1.0") + } } allprojects { - repositories { - mavenCentral() - mavenLocal() - } + apply(plugin = "java") + apply(plugin = "org.jetbrains.kotlin.jvm") + apply(plugin = "org.jetbrains.kotlin.plugin.serialization") + apply(plugin = "org.jetbrains.kotlin.plugin.compose") + apply(plugin = "org.jetbrains.compose") + apply(plugin = "dev.opensavvy.dokka-mkdocs") + apply(plugin = "architectury-plugin") + + version = "mod_version".prop ?: "0.0.1-SNAPSHOT" + group = "mod_group".prop ?: "net.kernelpanicsoft" + base.archivesName = "archie-core" + + tasks.withType().configureEach { + options.encoding = "UTF-8" + options.release.set(21) + } + + kotlin { + compilerOptions { + freeCompilerArgs.add("-Xexpect-actual-classes") + } + } + + architectury { + compileOnly() + } + + dokka { + dokkaGeneratorIsolation = ClassLoaderIsolation() + } + + java.withSourcesJar() +} + +dependencies { + dokka(project(":archie-core-common")) { isTransitive = false } + dokka(project(":archie-core-fabric")) { isTransitive = false } + dokka(project(":archie-core-neoforge")) { isTransitive = false } + dokka(project(":archie-datagen-common")) { isTransitive = false } + dokka(project(":archie-datagen-fabric")) { isTransitive = false } + dokka(project(":archie-datagen-neoforge")) { isTransitive = false } + dokka(project(":archie-gametest-common")) { isTransitive = false } + dokka(project(":archie-gametest-fabric")) { isTransitive = false } + dokka(project(":archie-gametest-neoforge")) { isTransitive = false } } tasks { - check { - dependsOn(gradle.includedBuild("Archie").task(":check")) - dependsOn(gradle.includedBuild("Archie-Test").task(":check")) - } - - register("syncRunConfigurations") { - group = "ide" - description = "Regenerates each included build's run configurations, then copies them up to the composite root's .idea folder." - - dependsOn(gradle.includedBuild("Archie").task(":fabric:ideaSyncTask")) - dependsOn(gradle.includedBuild("Archie").task(":neoforge:ideaSyncTask")) - dependsOn(gradle.includedBuild("Archie-Test").task(":fabric-test:ideaSyncTask")) - dependsOn(gradle.includedBuild("Archie-Test").task(":neoforge-test:ideaSyncTask")) - - doLast { - val targetDir = file(".idea/runConfigurations").apply { mkdirs() } - targetDir.listFiles { f -> f.extension == "xml" }?.forEach { it.delete() } - - val nameAttr = Regex("""name="([^"]*)" type="Application"""") - listOf("Archie", "Archie-Test").forEach { includedBuildName -> - val sourceDir = file("$includedBuildName/.idea/runConfigurations") - if (!sourceDir.isDirectory) return@forEach - sourceDir.listFiles { f -> f.extension == "xml" }?.forEach { source -> - val rewritten = source.readText() - .replace("\$PROJECT_DIR\$/", "\$PROJECT_DIR\$/$includedBuildName/") - .replace("name=\"Minecraft ", "name=\"$includedBuildName ") - - val displayName = nameAttr.find(rewritten)?.groupValues?.get(1) ?: source.nameWithoutExtension - val sanitized = displayName.map { if (it.isLetterOrDigit()) it else '_' }.joinToString("") - - file("$targetDir/$sanitized.xml").writeText(rewritten) - } - } - } - } + register("publishDocs") { + dependsOn(getByName("embedDokkaIntoMkDocs")) + group = "publishing" + val tag = rootProject.version.toString().substringBeforeLast(".") + workingDir = rootDir + commandLine("mike", "deploy", "--push", "--update-aliases", tag, "latest") + } + // modpublisher's changelog reads CHANGELOG.md straight off disk when a publish task runs - it + // doesn't know about git tags or PRs. .github/workflows/release-notes.yaml (reactive, post-tag) + // can't help here: by the time it would generate this release's entry, the publish task attached + // to the tag has already read (and shipped) whatever was on disk before. This task closes that + // gap by generating CHANGELOG.md synchronously - see .github/scripts/generate_release_notes.py's + // module docstring for the two call shapes. + register("generateChangelog") { + group = "publishing" + workingDir = rootDir + commandLine( + "python3", ".github/scripts/generate_release_notes.py", + "--repo", "mod_source".prop!!.removePrefix("https://github.com/"), + "--new-tag", "v${project.version}", + "--range-end", "HEAD", + "--changelog-path", "CHANGELOG.md", + ) + } } diff --git a/Archie-Core/core/common/build.gradle.kts b/core/common/build.gradle.kts similarity index 100% rename from Archie-Core/core/common/build.gradle.kts rename to core/common/build.gradle.kts diff --git a/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java b/core/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java similarity index 100% rename from Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java rename to core/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java diff --git a/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenDepthMixin.java b/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenDepthMixin.java similarity index 100% rename from Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenDepthMixin.java rename to core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenDepthMixin.java diff --git a/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java b/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java similarity index 100% rename from Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java rename to core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java diff --git a/Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/GuiGraphicsMixin.java b/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/GuiGraphicsMixin.java similarity index 100% rename from Archie-Core/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/GuiGraphicsMixin.java rename to core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/GuiGraphicsMixin.java diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.common.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.common.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.common.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.common.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/block/entity/NBTBlockEntity.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/block/entity/NBTBlockEntity.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/block/entity/NBTBlockEntity.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/block/entity/NBTBlockEntity.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CategorySpec.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CategorySpec.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CategorySpec.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CategorySpec.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigContainer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigContainer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigContainer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigContainer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigSpec.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigSpec.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigSpec.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientConfigSpec.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientDataSpec.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientDataSpec.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientDataSpec.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ClientDataSpec.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CommonKeyCode.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CommonKeyCode.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CommonKeyCode.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/CommonKeyCode.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigContainer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigContainer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigContainer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigContainer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigSpec.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigSpec.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigSpec.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/ConfigSpec.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/DataSpec.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/DataSpec.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/DataSpec.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/DataSpec.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/FieldType.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/FieldType.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/FieldType.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/FieldType.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/IConfigSerializer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/IConfigSerializer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/IConfigSerializer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/IConfigSerializer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorListBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorListBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorListBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorListBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorMapBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorMapBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorMapBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ColorMapBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ConfigFieldBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ConfigFieldBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ConfigFieldBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ConfigFieldBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DoubleMapBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DoubleMapBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DoubleMapBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DoubleMapBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DropdownFieldBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DropdownFieldBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DropdownFieldBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/DropdownFieldBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/FloatMapBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/FloatMapBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/FloatMapBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/FloatMapBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/IntegerMapBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/IntegerMapBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/IntegerMapBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/IntegerMapBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeListBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeListBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeListBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeListBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeMapBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeMapBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeMapBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/KeycodeMapBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ListFieldBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ListFieldBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ListFieldBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/ListFieldBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/LongMapBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/LongMapBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/LongMapBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/LongMapBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/MapFieldBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/MapFieldBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/MapFieldBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/MapFieldBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryFieldBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryFieldBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryFieldBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryFieldBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryListBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryListBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryListBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryListBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryMapBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryMapBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryMapBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/RegistryMapBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecFieldBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecFieldBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecFieldBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecFieldBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecListBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecListBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecListBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecListBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecMapBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecMapBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecMapBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/SpecMapBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/StringMapBuilder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/StringMapBuilder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/StringMapBuilder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/StringMapBuilder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/extensions.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/extensions.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/extensions.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/builder/extensions.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/entry/ConfigSpecEntry.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/entry/ConfigSpecEntry.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/entry/ConfigSpecEntry.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/entry/ConfigSpecEntry.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/extensions.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/extensions.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/extensions.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/extensions.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/Json5ConfigSerializer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/Json5ConfigSerializer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/Json5ConfigSerializer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/Json5ConfigSerializer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/JsonConfigSerializer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/JsonConfigSerializer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/JsonConfigSerializer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/JsonConfigSerializer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/NullConfigSerializer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/NullConfigSerializer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/NullConfigSerializer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/NullConfigSerializer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/TomlConfigSerializer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/TomlConfigSerializer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/TomlConfigSerializer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/config/serializer/TomlConfigSerializer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AAndCondition.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AAndCondition.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AAndCondition.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AAndCondition.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ABuiltinConditions.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ABuiltinConditions.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ABuiltinConditions.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ABuiltinConditions.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.common.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AEqualsCondition.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AEqualsCondition.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AEqualsCondition.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AEqualsCondition.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AFalseCondition.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AFalseCondition.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AFalseCondition.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AFalseCondition.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AGroupCondition.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AGroupCondition.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AGroupCondition.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AGroupCondition.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AModLoadedCondition.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AModLoadedCondition.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AModLoadedCondition.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AModLoadedCondition.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ANotCondition.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ANotCondition.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ANotCondition.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ANotCondition.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AOrCondition.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AOrCondition.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AOrCondition.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AOrCondition.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/APlatformCondition.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/APlatformCondition.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/APlatformCondition.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/APlatformCondition.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ARegistryCondition.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ARegistryCondition.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ARegistryCondition.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ARegistryCondition.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ATrueCondition.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ATrueCondition.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ATrueCondition.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ATrueCondition.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AXorCondition.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AXorCondition.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AXorCondition.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AXorCondition.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAllIngredient.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAllIngredient.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAllIngredient.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAllIngredient.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAnyIngredient.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAnyIngredient.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAnyIngredient.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AAnyIngredient.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ABuiltinIngredients.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ABuiltinIngredients.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ABuiltinIngredients.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ABuiltinIngredients.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACombinedIngredient.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACombinedIngredient.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACombinedIngredient.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACombinedIngredient.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AComponentsIngredient.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AComponentsIngredient.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AComponentsIngredient.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/AComponentsIngredient.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomDataIngredient.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomDataIngredient.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomDataIngredient.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomDataIngredient.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.common.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.common.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.common.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.common.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.common.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.common.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.common.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.common.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredient.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredient.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredient.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredient.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientHolder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientHolder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientHolder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientHolder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientSerializer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientSerializer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientSerializer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/IACustomIngredientSerializer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/AUIScopeManager.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/AUIScopeManager.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/AUIScopeManager.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/AUIScopeManager.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeBlockContainerMenu.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeBlockContainerMenu.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeBlockContainerMenu.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeBlockContainerMenu.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerMenuBase.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerMenuBase.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerMenuBase.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerMenuBase.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerScreen.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerScreen.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerScreen.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeContainerScreen.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/ComposeScreen.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/Slot.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/Slot.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/Slot.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/Slot.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotHighlightClipProvider.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotHighlightClipProvider.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotHighlightClipProvider.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotHighlightClipProvider.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthProvider.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthProvider.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthProvider.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthProvider.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/animation/Animation.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/animation/Animation.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/animation/Animation.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/animation/Animation.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateComposables.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateComposables.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateComposables.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateComposables.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateContainer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateContainer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateContainer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateContainer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateManager.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateManager.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateManager.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStateManager.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacket.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacket.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacket.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacket.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityStatePacketRegistry.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityUpdatePacket.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityUpdatePacket.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityUpdatePacket.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/BlockEntityUpdatePacket.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/ComposeBlockEntityState.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/ComposeBlockEntityState.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/ComposeBlockEntityState.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/blockentity/ComposeBlockEntityState.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Divider.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Divider.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Divider.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Divider.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/EnergyBar.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/FluidTank.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Icon.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Icon.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Icon.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Icon.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/ProgressBar.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Spacer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Spacer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Spacer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Spacer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Text.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Text.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Text.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Text.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Texture.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Texture.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Texture.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/basic/Texture.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Collapsible.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Collapsible.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Collapsible.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Collapsible.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/ContainerPanel.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/ContainerPanel.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/ContainerPanel.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/ContainerPanel.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Panel.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Panel.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Panel.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Panel.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/RootContainer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/RootContainer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/RootContainer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/RootContainer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Scrollable.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Scrollable.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Scrollable.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Scrollable.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Surface.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Surface.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Surface.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/Surface.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/TabContainer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/TabContainer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/TabContainer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/containers/TabContainer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Button.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Button.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Button.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Button.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Checkbox.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Checkbox.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Checkbox.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Checkbox.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Clickable.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Clickable.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Clickable.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Clickable.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/ColorPicker.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/ColorPicker.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/ColorPicker.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/ColorPicker.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Radio.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Radio.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Radio.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Radio.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Slider.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Slider.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Slider.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Slider.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Switch.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Switch.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Switch.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/Switch.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextField.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextField.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextField.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextField.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldCore.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldCore.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldCore.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldCore.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldValue.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldValue.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldValue.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/input/textfield/TextFieldValue.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/ConfirmDialog.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/ConfirmDialog.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/ConfirmDialog.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/ConfirmDialog.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/DialogPrimitives.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/DialogPrimitives.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/DialogPrimitives.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/modal/DialogPrimitives.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/TextureStates.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/TextureStates.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/TextureStates.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/TextureStates.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/WidgetState.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/WidgetState.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/WidgetState.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/composables/theme/WidgetState.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemContainerMenu.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemContainerMenu.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemContainerMenu.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemContainerMenu.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemState.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemState.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemState.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ComposeItemState.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemContainerAccess.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemContainerAccess.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemContainerAccess.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemContainerAccess.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateComposables.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateComposables.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateComposables.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateComposables.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateManager.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateManager.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateManager.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStateManager.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacket.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacket.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacket.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacket.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacketRegistry.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacketRegistry.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacketRegistry.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemStatePacketRegistry.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemUpdatePacket.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemUpdatePacket.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemUpdatePacket.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/ItemUpdatePacket.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/SyncedItemHolder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/SyncedItemHolder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/SyncedItemHolder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/item/SyncedItemHolder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/Layer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/Layer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/Layer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/Layer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/LayerStackManager.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/LayerStackManager.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/LayerStackManager.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layer/LayerStackManager.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Alignment.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Alignment.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Alignment.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Alignment.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Arrangement.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Arrangement.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Arrangement.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Arrangement.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Box.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Box.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Box.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Box.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Column.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Column.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Column.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Column.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Helpers.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Helpers.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Helpers.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Helpers.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntCoordinates.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntCoordinates.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntCoordinates.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntCoordinates.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntRect.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntRect.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntRect.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/IntRect.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Layout.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Layout.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Layout.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Layout.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutDirection.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutDirection.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutDirection.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutDirection.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutNode.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutNode.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutNode.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/LayoutNode.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/MeasurePolicy.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/MeasurePolicy.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/MeasurePolicy.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/MeasurePolicy.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Row.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Row.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Row.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Row.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/RowColumnMeasurePolicy.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/RowColumnMeasurePolicy.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/RowColumnMeasurePolicy.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/RowColumnMeasurePolicy.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Size.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Size.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Size.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/layout/Size.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Constraints.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Constraints.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Constraints.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Constraints.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DebugModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DebugModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DebugModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DebugModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DrawModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DrawModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DrawModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/DrawModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/LayoutChangingModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/LayoutChangingModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/LayoutChangingModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/LayoutChangingModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Modifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Modifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Modifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/Modifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnGloballyPositionedModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnGloballyPositionedModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnGloballyPositionedModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnGloballyPositionedModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnSizeChangedModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnSizeChangedModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnSizeChangedModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/OnSizeChangedModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/SizeModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/SizeModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/SizeModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/SizeModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BackgroundModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BackgroundModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BackgroundModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BackgroundModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BorderModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BorderModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BorderModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/BorderModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TextureModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TextureModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TextureModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TextureModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TooltipModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TooltipModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TooltipModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/appearance/TooltipModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/InputEvent.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/InputEvent.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/InputEvent.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/InputEvent.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnCharTypedModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnCharTypedModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnCharTypedModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnCharTypedModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnKeyEventModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnKeyEventModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnKeyEventModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnKeyEventModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnPointerEventModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnPointerEventModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnPointerEventModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/input/OnPointerEventModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/MarginModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/MarginModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/MarginModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/MarginModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/OffsetModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/OffsetModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/OffsetModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/OffsetModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/PaddingModifier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/PaddingModifier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/PaddingModifier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/PaddingModifier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/ZIndex.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/ZIndex.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/ZIndex.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/modifiers/position/ZIndex.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/LayoutNodeApplier.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/LayoutNodeApplier.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/LayoutNodeApplier.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/LayoutNodeApplier.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/UINode.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/UINode.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/UINode.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/nodes/UINode.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.common.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/ComposableTheme.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/ComposableTheme.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/ComposableTheme.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/ComposableTheme.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/Theme.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/Theme.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/Theme.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/theme/Theme.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/HsvColor.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/HsvColor.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/HsvColor.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/HsvColor.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/KColor.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/KColor.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/KColor.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/KColor.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/GuiGraphics.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/GuiGraphics.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/GuiGraphics.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/GuiGraphics.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/Screen.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/Screen.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/Screen.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/Screen.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/VertexConsumer.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/VertexConsumer.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/VertexConsumer.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gui/util/extension/VertexConsumer.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/ArchieNetworkChannel.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/IPacketContext.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/IPacketContext.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/IPacketContext.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/IPacketContext.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/networking/NetworkChannel.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.common.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.common.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.common.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.common.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ACreativeTabRegistry.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ACreativeTabRegistry.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ACreativeTabRegistry.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ACreativeTabRegistry.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ADeferredRegistryHolder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ADeferredRegistryHolder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ADeferredRegistryHolder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/ADeferredRegistryHolder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/BlockRegistryHelper.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/BlockRegistryHelper.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/BlockRegistryHelper.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/BlockRegistryHelper.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/CreativeTabRegistryHelper.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/CreativeTabRegistryHelper.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/CreativeTabRegistryHelper.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/CreativeTabRegistryHelper.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistrarHelper.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistrarHelper.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistrarHelper.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistrarHelper.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistryHelper.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistryHelper.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistryHelper.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/RegistryHelper.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/extensions.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/extensions.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/extensions.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/registries/extensions.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/resourcepacks/SerializationReloadListener.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/resourcepacks/SerializationReloadListener.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/resourcepacks/SerializationReloadListener.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/resourcepacks/SerializationReloadListener.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ArchieDataAttachmentImpl.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ArchieDataAttachmentImpl.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ArchieDataAttachmentImpl.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ArchieDataAttachmentImpl.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/AttachmentRegistry.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/AttachmentRegistry.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/AttachmentRegistry.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/AttachmentRegistry.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/DataAttachment.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/DataAttachment.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/DataAttachment.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/DataAttachment.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/FluidStackNBTHolderImpl.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ItemStackNBTHolderImpl.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/KOps.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/KOps.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/KOps.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/KOps.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBT.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBT.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBT.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBT.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolder.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/NBTHolderImpl.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableList.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableList.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableList.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableList.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableMap.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableMap.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableMap.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/ObservableMap.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/SerializationManager.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/SerializationManager.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/SerializationManager.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/SerializationManager.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Sync.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Sync.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Sync.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Sync.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Utils.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Utils.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Utils.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/Utils.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/BuiltinSerializers.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/BuiltinSerializers.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/BuiltinSerializers.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/BuiltinSerializers.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/MinecraftSerializers.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/MinecraftSerializers.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/MinecraftSerializers.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/serialization/serializers/MinecraftSerializers.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieCapabilityExposure.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieEnergyStorage.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidSlot.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieFluidStorage.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemMenuSlot.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemMenuSlot.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemMenuSlot.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemMenuSlot.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemSlot.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemSlot.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemSlot.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemSlot.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemStorage.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemStorage.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemStorage.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/ArchieItemStorage.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/VanillaMenuSlot.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/VanillaMenuSlot.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/VanillaMenuSlot.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/transfer/VanillaMenuSlot.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Array.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Array.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Array.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Array.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Component.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Component.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Component.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Component.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Env.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Env.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Env.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Env.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/MutableEntry.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/MutableEntry.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/MutableEntry.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/MutableEntry.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Properties.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Properties.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Properties.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Properties.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Reflect.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Reflect.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Reflect.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Reflect.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/ResourceLocation.kt diff --git a/Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Tile.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Tile.kt similarity index 100% rename from Archie-Core/core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Tile.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/util/Tile.kt diff --git a/Archie-Core/core/common/src/main/resources/archie-common.mixins.json b/core/common/src/main/resources/archie-common.mixins.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/archie-common.mixins.json rename to core/common/src/main/resources/archie-common.mixins.json diff --git a/Archie-Core/core/common/src/main/resources/archie.accesswidener b/core/common/src/main/resources/archie.accesswidener similarity index 100% rename from Archie-Core/core/common/src/main/resources/archie.accesswidener rename to core/common/src/main/resources/archie.accesswidener diff --git a/Archie-Core/core/common/src/main/resources/archie.common.json b/core/common/src/main/resources/archie.common.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/archie.common.json rename to core/common/src/main/resources/archie.common.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java.theme.json b/core/common/src/main/resources/assets/archie/archie_themes/java.theme.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java.theme.json rename to core/common/src/main/resources/assets/archie/archie_themes/java.theme.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/button.json b/core/common/src/main/resources/assets/archie/archie_themes/java/button.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/button.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/button.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/checkbox.json b/core/common/src/main/resources/assets/archie/archie_themes/java/checkbox.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/checkbox.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/checkbox.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/dark/surface.json b/core/common/src/main/resources/assets/archie/archie_themes/java/dark/surface.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/dark/surface.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/dark/surface.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json b/core/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/energy_bar.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json b/core/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/fluid_tank.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json b/core/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/progress_bar.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/radio.json b/core/common/src/main/resources/assets/archie/archie_themes/java/radio.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/radio.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/radio.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slider.json b/core/common/src/main/resources/assets/archie/archie_themes/java/slider.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slider.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/slider.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slider_handle.json b/core/common/src/main/resources/assets/archie/archie_themes/java/slider_handle.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slider_handle.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/slider_handle.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slot.json b/core/common/src/main/resources/assets/archie/archie_themes/java/slot.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/slot.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/slot.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/small_checkbox.json b/core/common/src/main/resources/assets/archie/archie_themes/java/small_checkbox.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/small_checkbox.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/small_checkbox.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/surface.json b/core/common/src/main/resources/assets/archie/archie_themes/java/surface.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/surface.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/surface.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/switch_thumb.json b/core/common/src/main/resources/assets/archie/archie_themes/java/switch_thumb.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/switch_thumb.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/switch_thumb.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/switch_track.json b/core/common/src/main/resources/assets/archie/archie_themes/java/switch_track.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/switch_track.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/switch_track.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/tab_game.json b/core/common/src/main/resources/assets/archie/archie_themes/java/tab_game.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/tab_game.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/tab_game.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/tab_menu.json b/core/common/src/main/resources/assets/archie/archie_themes/java/tab_menu.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/tab_menu.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/tab_menu.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/text_field.json b/core/common/src/main/resources/assets/archie/archie_themes/java/text_field.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/archie_themes/java/text_field.json rename to core/common/src/main/resources/assets/archie/archie_themes/java/text_field.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/atlases/java.json b/core/common/src/main/resources/assets/archie/atlases/java.json similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/atlases/java.json rename to core/common/src/main/resources/assets/archie/atlases/java.json diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/banner.png b/core/common/src/main/resources/assets/archie/banner.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/banner.png rename to core/common/src/main/resources/assets/archie/banner.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/icon.png b/core/common/src/main/resources/assets/archie/icon.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/icon.png rename to core/common/src/main/resources/assets/archie/icon.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_disabled.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/button_highlighted.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked_and_hovered.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked_and_hovered.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked_and_hovered.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_clicked_and_hovered.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_hovered.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_hovered.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_hovered.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/checkbox_hovered.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/energy_bar.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/fluid_tank.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/progress_bar.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked_and_hovered.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked_and_hovered.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked_and_hovered.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_clicked_and_hovered.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_disabled.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_disabled.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_disabled.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_disabled.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_hovered.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_hovered.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_hovered.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/radio_hovered.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_handle_highlighted.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slider_highlighted.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slot.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slot.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slot.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/slot.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox_clicked.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox_clicked.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox_clicked.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/small_checkbox_clicked.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_dark.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/surface_inset_dark.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb_disabled.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb_disabled.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb_disabled.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_thumb_disabled.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked_and_hovered.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked_and_hovered.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked_and_hovered.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_clicked_and_hovered.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_disabled.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_disabled.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_disabled.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_disabled.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_hovered.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_hovered.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_hovered.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/switch_track_hovered.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_clicked_and_hovered.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_disabled.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_hovered.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_game_selected_highlighted.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_clicked_and_hovered.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_disabled.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_hovered.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/tab_menu_selected_highlighted.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png diff --git a/Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png.mcmeta b/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png.mcmeta similarity index 100% rename from Archie-Core/core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png.mcmeta rename to core/common/src/main/resources/assets/archie/textures/gui/sprites/java/text_field_highlighted.png.mcmeta diff --git a/Archie-Core/core/common/src/main/resources/data/archie/structure/gametest/empty.nbt b/core/common/src/main/resources/data/archie/structure/gametest/empty.nbt similarity index 100% rename from Archie-Core/core/common/src/main/resources/data/archie/structure/gametest/empty.nbt rename to core/common/src/main/resources/data/archie/structure/gametest/empty.nbt diff --git a/Archie-Core/core/fabric/build.gradle.kts b/core/fabric/build.gradle.kts similarity index 100% rename from Archie-Core/core/fabric/build.gradle.kts rename to core/fabric/build.gradle.kts diff --git a/Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/ArchieMixinPlugin.java b/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/ArchieMixinPlugin.java similarity index 100% rename from Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/ArchieMixinPlugin.java rename to core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/ArchieMixinPlugin.java diff --git a/Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java b/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java similarity index 100% rename from Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java rename to core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java diff --git a/Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java b/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java similarity index 100% rename from Archie-Core/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java rename to core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.fabric.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.fabric.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.fabric.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.fabric.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.fabric.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.fabric.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.fabric.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.fabric.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.fabric.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.fabric.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.fabric.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.fabric.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt diff --git a/Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt similarity index 100% rename from Archie-Core/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt diff --git a/Archie-Core/core/fabric/src/main/resources/archie.mixins.json b/core/fabric/src/main/resources/archie.mixins.json similarity index 100% rename from Archie-Core/core/fabric/src/main/resources/archie.mixins.json rename to core/fabric/src/main/resources/archie.mixins.json diff --git a/Archie-Core/core/fabric/src/main/resources/fabric.mod.json b/core/fabric/src/main/resources/fabric.mod.json similarity index 100% rename from Archie-Core/core/fabric/src/main/resources/fabric.mod.json rename to core/fabric/src/main/resources/fabric.mod.json diff --git a/Archie-Core/core/neoforge/build.gradle.kts b/core/neoforge/build.gradle.kts similarity index 100% rename from Archie-Core/core/neoforge/build.gradle.kts rename to core/neoforge/build.gradle.kts diff --git a/Archie-Core/core/neoforge/gradle.properties b/core/neoforge/gradle.properties similarity index 100% rename from Archie-Core/core/neoforge/gradle.properties rename to core/neoforge/gradle.properties diff --git a/Archie-Core/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java b/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java similarity index 100% rename from Archie-Core/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java rename to core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java diff --git a/Archie-Core/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java b/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java similarity index 100% rename from Archie-Core/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java rename to core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/APlatform.kt diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.neoforge.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.neoforge.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.neoforge.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionsPlatform.neoforge.kt diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.neoforge.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.neoforge.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.neoforge.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientPlatform.neoforge.kt diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.neoforge.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.neoforge.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.neoforge.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ingredients/ACustomIngredientSerializerPlatform.neoforge.kt diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gui/render/AFluidRenderPlatform.kt diff --git a/Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt similarity index 100% rename from Archie-Core/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/registries/AClientRegistrationPlatform.kt diff --git a/Archie-Core/core/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/core/neoforge/src/main/resources/META-INF/neoforge.mods.toml similarity index 100% rename from Archie-Core/core/neoforge/src/main/resources/META-INF/neoforge.mods.toml rename to core/neoforge/src/main/resources/META-INF/neoforge.mods.toml diff --git a/Archie-Core/core/neoforge/src/main/resources/archie.mixins.json b/core/neoforge/src/main/resources/archie.mixins.json similarity index 100% rename from Archie-Core/core/neoforge/src/main/resources/archie.mixins.json rename to core/neoforge/src/main/resources/archie.mixins.json diff --git a/Archie-Core/datagen/common/build.gradle.kts b/datagen/common/build.gradle.kts similarity index 100% rename from Archie-Core/datagen/common/build.gradle.kts rename to datagen/common/build.gradle.kts diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGenerator.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/IADataProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/IADataProvider.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/IADataProvider.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/IADataProvider.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/ALanguageProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/ALanguageProvider.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/ALanguageProvider.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/ALanguageProvider.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelBuilder.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelBuilder.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelProvider.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelProvider.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockModelProvider.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockStateProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockStateProvider.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockStateProvider.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ABlockStateProvider.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AConfiguredModel.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AConfiguredModel.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AConfiguredModel.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AConfiguredModel.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ACustomLoaderBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ACustomLoaderBuilder.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ACustomLoaderBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/ACustomLoaderBuilder.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelBuilder.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelBuilder.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelProvider.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelProvider.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AItemModelProvider.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelBuilder.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelFile.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelFile.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelFile.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelFile.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelProvider.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelProvider.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AModelProvider.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AMultiPartBlockStateBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AMultiPartBlockStateBuilder.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AMultiPartBlockStateBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AMultiPartBlockStateBuilder.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AVariantBlockStateBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AVariantBlockStateBuilder.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AVariantBlockStateBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/AVariantBlockStateBuilder.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/IAGeneratedBlockState.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/IAGeneratedBlockState.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/IAGeneratedBlockState.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/client/model/IAGeneratedBlockState.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.common.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.common.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.common.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.common.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieCookingRecipeBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieCookingRecipeBuilder.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieCookingRecipeBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieCookingRecipeBuilder.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapedRecipeBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapedRecipeBuilder.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapedRecipeBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapedRecipeBuilder.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapelessRecipeBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapelessRecipeBuilder.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapelessRecipeBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/ArchieShapelessRecipeBuilder.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilder.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilder.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.common.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.common.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.common.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.common.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagsProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagsProvider.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagsProvider.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagsProvider.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/IATagBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/IATagBuilder.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/IATagBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/IATagBuilder.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/DatagenArchieExtension.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/DatagenArchieExtension.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/DatagenArchieExtension.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/DatagenArchieExtension.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/util/TransformationHelper.kt diff --git a/Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ADatagenEvents.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ADatagenEvents.kt similarity index 100% rename from Archie-Core/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ADatagenEvents.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ADatagenEvents.kt diff --git a/Archie-Core/datagen/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension b/datagen/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension similarity index 100% rename from Archie-Core/datagen/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension rename to datagen/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension diff --git a/Archie-Core/datagen/fabric/build.gradle.kts b/datagen/fabric/build.gradle.kts similarity index 100% rename from Archie-Core/datagen/fabric/build.gradle.kts rename to datagen/fabric/build.gradle.kts diff --git a/Archie-Core/datagen/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java b/datagen/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java similarity index 100% rename from Archie-Core/datagen/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java rename to datagen/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java diff --git a/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorFabric.kt b/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorFabric.kt similarity index 100% rename from Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorFabric.kt rename to datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorFabric.kt diff --git a/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt b/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt similarity index 100% rename from Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt rename to datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt diff --git a/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.fabric.kt b/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.fabric.kt similarity index 100% rename from Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.fabric.kt rename to datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.fabric.kt diff --git a/Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt b/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt similarity index 100% rename from Archie-Core/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt rename to datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt diff --git a/Archie-Core/datagen/fabric/src/main/resources/archie_datagen.mixins.json b/datagen/fabric/src/main/resources/archie_datagen.mixins.json similarity index 100% rename from Archie-Core/datagen/fabric/src/main/resources/archie_datagen.mixins.json rename to datagen/fabric/src/main/resources/archie_datagen.mixins.json diff --git a/Archie-Core/datagen/fabric/src/main/resources/fabric.mod.json b/datagen/fabric/src/main/resources/fabric.mod.json similarity index 100% rename from Archie-Core/datagen/fabric/src/main/resources/fabric.mod.json rename to datagen/fabric/src/main/resources/fabric.mod.json diff --git a/Archie-Core/datagen/neoforge/build.gradle.kts b/datagen/neoforge/build.gradle.kts similarity index 100% rename from Archie-Core/datagen/neoforge/build.gradle.kts rename to datagen/neoforge/build.gradle.kts diff --git a/Archie-Core/datagen/neoforge/gradle.properties b/datagen/neoforge/gradle.properties similarity index 100% rename from Archie-Core/datagen/neoforge/gradle.properties rename to datagen/neoforge/gradle.properties diff --git a/Archie-Core/datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java b/datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java similarity index 100% rename from Archie-Core/datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java rename to datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java diff --git a/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorNeoForge.kt b/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorNeoForge.kt similarity index 100% rename from Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorNeoForge.kt rename to datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorNeoForge.kt diff --git a/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt b/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt similarity index 100% rename from Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt rename to datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt diff --git a/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.neoforge.kt b/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.neoforge.kt similarity index 100% rename from Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.neoforge.kt rename to datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.neoforge.kt diff --git a/Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt b/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt similarity index 100% rename from Archie-Core/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt rename to datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ATagBuilderPlatform.kt diff --git a/Archie-Core/datagen/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/datagen/neoforge/src/main/resources/META-INF/neoforge.mods.toml similarity index 100% rename from Archie-Core/datagen/neoforge/src/main/resources/META-INF/neoforge.mods.toml rename to datagen/neoforge/src/main/resources/META-INF/neoforge.mods.toml diff --git a/Archie-Core/datagen/neoforge/src/main/resources/archie_datagen.mixins.json b/datagen/neoforge/src/main/resources/archie_datagen.mixins.json similarity index 100% rename from Archie-Core/datagen/neoforge/src/main/resources/archie_datagen.mixins.json rename to datagen/neoforge/src/main/resources/archie_datagen.mixins.json diff --git a/Archie/docs/assets/icon.svg b/docs/assets/icon.svg similarity index 100% rename from Archie/docs/assets/icon.svg rename to docs/assets/icon.svg diff --git a/Archie/docs/config.md b/docs/config.md similarity index 100% rename from Archie/docs/config.md rename to docs/config.md diff --git a/Archie/docs/datagen.md b/docs/datagen.md similarity index 98% rename from Archie/docs/datagen.md rename to docs/datagen.md index 00c11e6f6..7a4c4931f 100644 --- a/Archie/docs/datagen.md +++ b/docs/datagen.md @@ -10,12 +10,12 @@ datagen API directly. ## Running datagen -Datagen runs as a separate Gradle run configuration per loader, from inside `Archie/` (or -`Archie-Test/` for the playground mod): +Datagen runs as a separate Gradle run configuration per loader, from the repo root (or +`archie-test-{fabric,neoforge}` for the playground mod): ```bash -./gradlew fabric:runDatagen -./gradlew neoforge:runDatagen +./gradlew archie-datagen-fabric:runDatagen +./gradlew archie-datagen-neoforge:runDatagen ``` This sets the `archie.datagen` system property, which `ADataGeneratorPlatform.isDataGen` reads to diff --git a/Archie/docs/events.md b/docs/events.md similarity index 100% rename from Archie/docs/events.md rename to docs/events.md diff --git a/Archie/docs/gametest.md b/docs/gametest.md similarity index 98% rename from Archie/docs/gametest.md rename to docs/gametest.md index 78b6c1711..073007245 100644 --- a/Archie/docs/gametest.md +++ b/docs/gametest.md @@ -368,13 +368,13 @@ entirely: ## Running the tests -From inside `Archie/` (or `Archie-Test/`, for the playground mod's own suite): +From the repo root (or `archie-test-{fabric,neoforge}` for the playground mod's own suite): ```bash -./gradlew fabric:runGametest -./gradlew neoforge:runGametest -./gradlew fabric:runGametestClient -./gradlew neoforge:runGametestClient +./gradlew archie-gametest-fabric:runGametest +./gradlew archie-gametest-neoforge:runGametest +./gradlew archie-gametest-fabric:runGametestClient +./gradlew archie-gametest-neoforge:runGametestClient ``` `runGametest` runs server-side `@GameTest`s; `runGametestClient` runs `@ClientGameTest`s via diff --git a/Archie/docs/gui.md b/docs/gui.md similarity index 100% rename from Archie/docs/gui.md rename to docs/gui.md diff --git a/Archie/docs/index.md b/docs/index.md similarity index 97% rename from Archie/docs/index.md rename to docs/index.md index cb3707fdb..0f1be3a4d 100644 --- a/Archie/docs/index.md +++ b/docs/index.md @@ -64,7 +64,7 @@ object MyMod { ## Where to add new code -- Put shared logic in `Archie/common/src/main/kotlin/...` first. +- Put shared logic in `core/common/src/main/kotlin/...` first. - Add loader differences with `expect/actual` triplets: `*.common.kt`, `*.fabric.kt`, `*.neoforge.kt`. - Keep Fabric and NeoForge entrypoints thin (`ArchieFabric`, `ArchieNeoForge`) and delegate to `Archie.init*()`. - Register packet handlers before calling `register()` on your `NetworkChannel`. diff --git a/Archie/docs/networking.md b/docs/networking.md similarity index 100% rename from Archie/docs/networking.md rename to docs/networking.md diff --git a/Archie/docs/news/index.md b/docs/news/index.md similarity index 100% rename from Archie/docs/news/index.md rename to docs/news/index.md diff --git a/Archie/docs/overrides/main.html b/docs/overrides/main.html similarity index 100% rename from Archie/docs/overrides/main.html rename to docs/overrides/main.html diff --git a/Archie/docs/registries.md b/docs/registries.md similarity index 100% rename from Archie/docs/registries.md rename to docs/registries.md diff --git a/Archie/docs/resource-packs.md b/docs/resource-packs.md similarity index 100% rename from Archie/docs/resource-packs.md rename to docs/resource-packs.md diff --git a/Archie/docs/serialization.md b/docs/serialization.md similarity index 100% rename from Archie/docs/serialization.md rename to docs/serialization.md diff --git a/Archie/docs/transfer.md b/docs/transfer.md similarity index 100% rename from Archie/docs/transfer.md rename to docs/transfer.md diff --git a/Archie-Core/gametest/common/build.gradle.kts b/gametest/common/build.gradle.kts similarity index 100% rename from Archie-Core/gametest/common/build.gradle.kts rename to gametest/common/build.gradle.kts diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AGametestEvents.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AGametestEvents.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AGametestEvents.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AGametestEvents.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ComposeScreenTestContext.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ComposeScreenTestContext.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ComposeScreenTestContext.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ComposeScreenTestContext.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/GameTestAssertions.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparer.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparer.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparer.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparer.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparisonAlgorithm.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparisonAlgorithm.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparisonAlgorithm.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotComparisonAlgorithm.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotManager.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotManager.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotManager.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ScreenshotManager.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/VerboseTestReporter.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/VerboseTestReporter.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/VerboseTestReporter.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/VerboseTestReporter.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/GametestArchieExtension.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/GametestArchieExtension.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/GametestArchieExtension.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/GametestArchieExtension.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ArchieItemHandlerTests.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityNBTHolderTests.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/BlockEntityStateManagerTests.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ComposeRenderingTests.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ComposeRenderingTests.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ComposeRenderingTests.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ComposeRenderingTests.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/InputComponentsGameTest.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/InputComponentsGameTest.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/InputComponentsGameTest.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/InputComponentsGameTest.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/LayoutComponentsGameTest.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/LayoutComponentsGameTest.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/LayoutComponentsGameTest.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/LayoutComponentsGameTest.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ModalComponentsGameTest.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ModalComponentsGameTest.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ModalComponentsGameTest.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/tests/ModalComponentsGameTest.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt diff --git a/Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt similarity index 100% rename from Archie-Core/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt diff --git a/Archie-Core/gametest/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension b/gametest/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension similarity index 100% rename from Archie-Core/gametest/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension rename to gametest/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension diff --git a/Archie-Core/gametest/fabric/build.gradle.kts b/gametest/fabric/build.gradle.kts similarity index 100% rename from Archie-Core/gametest/fabric/build.gradle.kts rename to gametest/fabric/build.gradle.kts diff --git a/Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestHelperMixin.java b/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestHelperMixin.java similarity index 100% rename from Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestHelperMixin.java rename to gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestHelperMixin.java diff --git a/Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java b/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java similarity index 100% rename from Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java rename to gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java diff --git a/Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/lifecycle/MinecraftClientMixin.java b/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/lifecycle/MinecraftClientMixin.java similarity index 100% rename from Archie-Core/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/lifecycle/MinecraftClientMixin.java rename to gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/lifecycle/MinecraftClientMixin.java diff --git a/Archie-Core/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt b/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt similarity index 100% rename from Archie-Core/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt rename to gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt diff --git a/Archie-Core/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt b/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt similarity index 100% rename from Archie-Core/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt rename to gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt diff --git a/Archie-Core/gametest/fabric/src/main/resources/archie_gametest.mixins.json b/gametest/fabric/src/main/resources/archie_gametest.mixins.json similarity index 100% rename from Archie-Core/gametest/fabric/src/main/resources/archie_gametest.mixins.json rename to gametest/fabric/src/main/resources/archie_gametest.mixins.json diff --git a/Archie-Core/gametest/fabric/src/main/resources/fabric.mod.json b/gametest/fabric/src/main/resources/fabric.mod.json similarity index 100% rename from Archie-Core/gametest/fabric/src/main/resources/fabric.mod.json rename to gametest/fabric/src/main/resources/fabric.mod.json diff --git a/Archie-Core/gametest/neoforge/build.gradle.kts b/gametest/neoforge/build.gradle.kts similarity index 100% rename from Archie-Core/gametest/neoforge/build.gradle.kts rename to gametest/neoforge/build.gradle.kts diff --git a/Archie-Core/gametest/neoforge/gradle.properties b/gametest/neoforge/gradle.properties similarity index 100% rename from Archie-Core/gametest/neoforge/gradle.properties rename to gametest/neoforge/gradle.properties diff --git a/Archie-Core/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java b/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java similarity index 100% rename from Archie-Core/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java rename to gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java diff --git a/Archie-Core/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/lifecycle/MinecraftClientMixin.java b/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/lifecycle/MinecraftClientMixin.java similarity index 100% rename from Archie-Core/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/lifecycle/MinecraftClientMixin.java rename to gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/lifecycle/MinecraftClientMixin.java diff --git a/Archie-Core/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt b/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt similarity index 100% rename from Archie-Core/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt rename to gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt diff --git a/Archie-Core/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt b/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt similarity index 100% rename from Archie-Core/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt rename to gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt diff --git a/Archie-Core/gametest/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/gametest/neoforge/src/main/resources/META-INF/neoforge.mods.toml similarity index 100% rename from Archie-Core/gametest/neoforge/src/main/resources/META-INF/neoforge.mods.toml rename to gametest/neoforge/src/main/resources/META-INF/neoforge.mods.toml diff --git a/Archie-Core/gametest/neoforge/src/main/resources/archie_gametest.mixins.json b/gametest/neoforge/src/main/resources/archie_gametest.mixins.json similarity index 100% rename from Archie-Core/gametest/neoforge/src/main/resources/archie_gametest.mixins.json rename to gametest/neoforge/src/main/resources/archie_gametest.mixins.json diff --git a/gradle.properties b/gradle.properties index 1d397d138..7682d2f95 100644 --- a/gradle.properties +++ b/gradle.properties @@ -16,4 +16,3 @@ mod_license=GPL-3.0-or-later client_datagen=true server_datagen=true - diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar index d64cd4917707c1f8861d8cb53dd15194d4248596..a4b76b9530d66f5e68d973ea569d8e19de379189 100644 GIT binary patch delta 34592 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 delta 34555 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 diff --git a/gradlew b/gradlew index 1aa94a426..f3b75f3b0 100755 --- a/gradlew +++ b/gradlew @@ -15,6 +15,8 @@ # See the License for the specific language governing permissions and # limitations under the License. # +# SPDX-License-Identifier: Apache-2.0 +# ############################################################################## # @@ -55,7 +57,7 @@ # Darwin, MinGW, and NonStop. # # (3) This script is generated from the Groovy template -# https://github.com/gradle/gradle/blob/HEAD/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt # within the Gradle project. # # You can find Gradle at https://github.com/gradle/gradle/. @@ -84,7 +86,7 @@ done # shellcheck disable=SC2034 APP_BASE_NAME=${0##*/} # Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) -APP_HOME=$( cd "${APP_HOME:-./}" > /dev/null && pwd -P ) || exit +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit # Use the maximum available, or set MAX_FD != -1 to use that value. MAX_FD=maximum diff --git a/gradlew.bat b/gradlew.bat index 7101f8e46..9b42019c7 100644 --- a/gradlew.bat +++ b/gradlew.bat @@ -13,6 +13,8 @@ @rem See the License for the specific language governing permissions and @rem limitations under the License. @rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem @if "%DEBUG%"=="" @echo off @rem ########################################################################## diff --git a/Archie/mkdocs.yml b/mkdocs.yml similarity index 98% rename from Archie/mkdocs.yml rename to mkdocs.yml index dd82aa5d1..325a87f0e 100644 --- a/Archie/mkdocs.yml +++ b/mkdocs.yml @@ -4,7 +4,7 @@ site_description: > site_author: Kernel Panic site_url: https://docs.kernelpanicsoft.net/Archie/ repo_url: https://github.com/kernel-panic-codecave/Archie/ -edit_uri: edit/1.21.x/Archie/docs/ +edit_uri: edit/1.21.x/docs/ use_directory_urls: false theme: name: material diff --git a/settings.gradle.kts b/settings.gradle.kts index 15e04a1dc..ddc2b0788 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -1,4 +1,7 @@ enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +rootProject.name = "Archie" + pluginManagement { repositories { maven("https://maven.fabricmc.net/") @@ -7,15 +10,50 @@ pluginManagement { maven("https://maven.neoforged.net/releases/") maven("https://maven.firstdarkdev.xyz/releases") maven { - name = "kernelpanic" + name = "kernelpanic releases" url = uri("https://maven.kernelpanicsoft.net/releases") } + maven { + name = "kernelpanic snapshots" + url = uri("https://maven.kernelpanicsoft.net/snapshots") + } + mavenLocal() gradlePluginPortal() } -// includeBuild("plugins") } -rootProject.name = "Archie-Repo" +// libs.versions.toml sits at the conventional gradle/libs.versions.toml location now (it used to +// live one level up, outside archie-core's own project dir, hence the old explicit +// dependencyResolutionManagement { versionCatalogs { create("libs") { from(...) } } } block - Gradle +// auto-registers it from here, so that block is gone; adding it back double-registers "libs". + +// Matches terrarium-earth/Common-Storage-Lib's settings.gradle.kts layout: one nested +// / directory per platform, flattened into a single-level Gradle project name +// (e.g. core/fabric -> archie-core-fabric). archie-core is the library; archie-datagen and +// archie-gametest are its dev-time-only sibling modules; archie-test is the dev-playground mod +// that exercises all three. +includeCorePlatform("common") +includeCorePlatform("fabric") +includeCorePlatform("neoforge") + +includeModule("datagen", "common") +includeModule("datagen", "fabric") +includeModule("datagen", "neoforge") -includeBuild("Archie") -includeBuild("Archie-Test") +includeModule("gametest", "common") +includeModule("gametest", "fabric") +includeModule("gametest", "neoforge") + +includeModule("test", "common") +includeModule("test", "fabric") +includeModule("test", "neoforge") + +fun includeModule(name: String, platform: String) { + include("$name/$platform") + project(":$name/$platform").name = "archie-$name-$platform" +} + +fun includeCorePlatform(platform: String) { + include("core/$platform") + project(":core/$platform").name = "archie-core-$platform" +} diff --git a/test/common/build.gradle.kts b/test/common/build.gradle.kts new file mode 100644 index 000000000..f8c49b805 --- /dev/null +++ b/test/common/build.gradle.kts @@ -0,0 +1,48 @@ +architectury { + common("fabric", "neoforge") +} + +actualizer { + stubUnfulfilledExpects() +} + +loom { + log4jConfigs.from(rootDir.resolve("log4j-dev.xml")) + accessWidenerPath = file("src/main/resources/archie_test.accesswidener") + enableTransitiveAccessWideners = true +} + +dependencies { + modApi(project(":archie-core-common")) + modApi(project(":archie-datagen-common")) + modApi(project(":archie-gametest-common")) + + testImplementation(libs.junit.jupiter.api) + testImplementation(kotlin("reflect")) + testRuntimeOnly(libs.junit.jupiter.engine) + // We depend on fabric loader here to use the fabric @Environment annotations and get the mixin dependencies + // Do NOT use other classes from fabric loader + modImplementation(libs.fabric.loader) + + modApi(libs.architectury.common) + modApi(libs.rei.common) + modApi(libs.storage.common) + modApi(libs.storage.resources.common) +} + +tasks { + base.archivesName.set(base.archivesName.get() + "-test-common") + + test { + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + useJUnitPlatform() + systemProperty("archie.junit.gametest", "true") + systemProperty( + "archie.junit.gametest.matrix", + System.getProperty("archie.junit.gametest.matrix") ?: "fabric:server,fabric:client,neoforge:server,neoforge:client", + ) + systemProperty("archie.junit.gametest.timeoutMinutes", "20") + systemProperty("archie.junit.gametest.root", rootProject.rootDir.absolutePath) + } +} diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt similarity index 91% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt index 5d691e294..5e2150ce8 100644 --- a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt +++ b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt @@ -5,7 +5,8 @@ import dev.architectury.platform.Mod import dev.architectury.platform.Platform import net.kernelpanicsoft.archie.Archie import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform -import net.kernelpanicsoft.archie.events.AEvents +import net.kernelpanicsoft.archie.events.ADatagenEvents +import net.kernelpanicsoft.archie.events.AGametestEvents import net.kernelpanicsoft.archie.gametest.AGameTestPlatform import net.kernelpanicsoft.archie.test.gametest.ArchieTestGameTest import net.kernelpanicsoft.archie.test.gametest.DataAttachmentTestFixtures @@ -31,7 +32,8 @@ object ArchieTest @JvmStatic fun init() { - AEvents += MOD + ADatagenEvents += MOD + AGametestEvents += MOD if (ADataGeneratorPlatform.isDataGen) ArchieTestDatagen.init() if (AGameTestPlatform.isGameTest) diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/BlockRegistry.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/BlockRegistry.kt similarity index 100% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/BlockRegistry.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/BlockRegistry.kt diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/GuiRegistry.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/GuiRegistry.kt similarity index 100% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/GuiRegistry.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/GuiRegistry.kt diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ItemRegistry.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ItemRegistry.kt similarity index 100% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ItemRegistry.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ItemRegistry.kt diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestBlock.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestBlock.kt similarity index 100% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestBlock.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestBlock.kt diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItem.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItem.kt similarity index 100% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItem.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItem.kt diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemContainerScreen.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemContainerScreen.kt similarity index 100% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemContainerScreen.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemContainerScreen.kt diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemMenu.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemMenu.kt similarity index 100% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemMenu.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemMenu.kt diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemScreen.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemScreen.kt similarity index 100% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemScreen.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestItemScreen.kt diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestKind.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestKind.kt similarity index 100% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestKind.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestKind.kt diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestMenu.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestMenu.kt similarity index 100% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestMenu.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestMenu.kt diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestScreen.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestScreen.kt similarity index 100% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestScreen.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestScreen.kt diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestTile.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestTile.kt similarity index 100% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestTile.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TestTile.kt diff --git a/Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TileRegistry.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TileRegistry.kt similarity index 100% rename from Archie-Test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TileRegistry.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/TileRegistry.kt diff --git a/Archie-Test/common/src/main/datagen/net/kernelpanicsoft/archie/test/data/ArchieTestDatagen.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/data/ArchieTestDatagen.kt similarity index 100% rename from Archie-Test/common/src/main/datagen/net/kernelpanicsoft/archie/test/data/ArchieTestDatagen.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/data/ArchieTestDatagen.kt diff --git a/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt similarity index 69% rename from Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt index ab87661af..04744fe0d 100644 --- a/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt +++ b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt @@ -1,10 +1,10 @@ package net.kernelpanicsoft.archie.test.gametest -import net.kernelpanicsoft.archie.events.AEvents +import net.kernelpanicsoft.archie.events.AGametestEvents import net.kernelpanicsoft.archie.gametest.AGameTestEventObject import net.kernelpanicsoft.archie.test.ArchieTest -internal fun AEvents.ArchieGameTestBuilder.archieTestGameTests() +internal fun AGametestEvents.ArchieGameTestBuilder.archieTestGameTests() { client { register() @@ -21,5 +21,5 @@ internal fun AEvents.ArchieGameTestBuilder.archieTestGameTests() internal object ArchieTestGameTest : AGameTestEventObject(ArchieTest.MOD) { - override fun AEvents.ArchieGameTestBuilder.handler() = archieTestGameTests() + override fun AGametestEvents.ArchieGameTestBuilder.handler() = archieTestGameTests() } diff --git a/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTestFixtures.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTestFixtures.kt similarity index 100% rename from Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTestFixtures.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTestFixtures.kt diff --git a/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTests.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTests.kt similarity index 100% rename from Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTests.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/CapabilityLookupTests.kt diff --git a/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/ComposeItemContainerMenuClientTests.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/ComposeItemContainerMenuClientTests.kt similarity index 100% rename from Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/ComposeItemContainerMenuClientTests.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/ComposeItemContainerMenuClientTests.kt diff --git a/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/ComposeItemContainerMenuTests.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/ComposeItemContainerMenuTests.kt similarity index 100% rename from Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/ComposeItemContainerMenuTests.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/ComposeItemContainerMenuTests.kt diff --git a/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/DataAttachmentTestFixtures.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/DataAttachmentTestFixtures.kt similarity index 100% rename from Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/DataAttachmentTestFixtures.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/DataAttachmentTestFixtures.kt diff --git a/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/DataAttachmentTests.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/DataAttachmentTests.kt similarity index 100% rename from Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/DataAttachmentTests.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/DataAttachmentTests.kt diff --git a/Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/TestScreenGameTest.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/TestScreenGameTest.kt similarity index 100% rename from Archie-Test/common/src/main/gametest/net/kernelpanicsoft/archie/test/gametest/TestScreenGameTest.kt rename to test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/TestScreenGameTest.kt diff --git a/Archie-Test/common/src/main/resources/archie_test.accesswidener b/test/common/src/main/resources/archie_test.accesswidener similarity index 100% rename from Archie-Test/common/src/main/resources/archie_test.accesswidener rename to test/common/src/main/resources/archie_test.accesswidener diff --git a/Archie-Test/common/src/main/resources/archie_test.common.json b/test/common/src/main/resources/archie_test.common.json similarity index 100% rename from Archie-Test/common/src/main/resources/archie_test.common.json rename to test/common/src/main/resources/archie_test.common.json diff --git a/Archie-Test/common/src/main/resources/assets/archie_test/banner.png b/test/common/src/main/resources/assets/archie_test/banner.png similarity index 100% rename from Archie-Test/common/src/main/resources/assets/archie_test/banner.png rename to test/common/src/main/resources/assets/archie_test/banner.png diff --git a/Archie-Test/common/src/main/resources/assets/archie_test/icon.png b/test/common/src/main/resources/assets/archie_test/icon.png similarity index 100% rename from Archie-Test/common/src/main/resources/assets/archie_test/icon.png rename to test/common/src/main/resources/assets/archie_test/icon.png diff --git a/Archie-Test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt b/test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt similarity index 100% rename from Archie-Test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt rename to test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt diff --git a/Archie-Test/common/src/test/resources/junit-platform.properties b/test/common/src/test/resources/junit-platform.properties similarity index 100% rename from Archie-Test/common/src/test/resources/junit-platform.properties rename to test/common/src/test/resources/junit-platform.properties diff --git a/test/fabric/build.gradle.kts b/test/fabric/build.gradle.kts new file mode 100644 index 000000000..76114bbd9 --- /dev/null +++ b/test/fabric/build.gradle.kts @@ -0,0 +1,154 @@ +import net.kernelpanicsoft.archie.plugin.bundleMod + +plugins { + alias(libs.plugins.shadow) + alias(libs.plugins.archie) +} + +architectury { + platformSetupLoomIde() + fabric() +} + +actualizer { + actualizes(project(":archie-test-common")) +} + +configurations { + create("common") + create("shadowCommon") + compileClasspath.get().extendsFrom(configurations["common"]) + runtimeClasspath.get().extendsFrom(configurations["common"]) + testCompileClasspath.get().extendsFrom(compileClasspath.get()) + testRuntimeClasspath.get().extendsFrom(runtimeClasspath.get()) +} + +loom { + log4jConfigs.from(project(":archie-test-common").loom.log4jConfigs) + accessWidenerPath.set(project(":archie-test-common").loom.accessWidenerPath) + + mods { + maybeCreate("main").apply { + sourceSet(sourceSets.main.get()) + } + } + + runs { + getByName("client") { + name = "Minecraft Client" + source(sourceSets.main.get()) + vmArg("-XX:+AllowEnhancedClassRedefinition") + } + getByName("server") { + name = "Minecraft Server" + source(sourceSets.main.get()) + vmArgs("-XX:+AllowEnhancedClassRedefinition") + } + // This adds a new gradle task that runs the datagen API: "gradlew runDatagen" + create("datagen") { + client() + name = "Minecraft Datagen" + property("archie.datagen", "true") + property("archie.datagen.client", providers.gradleProperty("client_datagen").orElse("true").get()) + property("archie.datagen.server", providers.gradleProperty("server_datagen").orElse("true").get()) + property("fabric-api.datagen") + property("fabric-api.datagen.modid", "archie_test") + property("fabric-api.datagen.output-dir", file("src/main/generated").absolutePath) + + runDir = "build/datagen" + } + create("gametest") { + server() + name = "Minecraft GameTest" + property("fabric-api.gametest") + property("archie.gametest", "true") + property("archie.gametest.side", "server") + property("archie.gametest.modid", "archie_test") + } + create("gametestClient") { + client() + name = "Minecraft GameTest Client" + property("fabric-api.gametest") + property("archie.gametest", "true") + property("archie.gametest.side", "client") + property("archie.gametest.modid", "archie_test") + } + } +} + +fabricApi.configureDataGeneration { + createRunConfiguration = false + outputDirectory.set(file("src/main/generated")) +} + +dependencies { + modImplementation(libs.fabric.loader) + modApi(libs.fabric.api) + modApi(libs.architectury.fabric) + modImplementation(libs.kotlin.fabric) + modLocalRuntime(libs.rei.fabric) + modLocalRuntime(libs.catalogue.fabric) + modLocalRuntime(libs.menulogue.fabric) + modLocalRuntime(libs.clothConfig.fabric) + bundleMod(libs.storage.fabric) + + implementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.api) + testRuntimeOnly(libs.junit.jupiter.engine) + + "common"(project(":archie-test-common", "namedElements")) { isTransitive = false } + "shadowCommon"(project(":archie-test-common", "transformProductionFabric")) { isTransitive = false } + modApi(project(":archie-core-fabric")) + modApi(project(":archie-datagen-fabric")) + modApi(project(":archie-gametest-fabric")) +} + +modResources { + filesMatching.add("fabric.mod.json") +} + +tasks { + base.archivesName.set(base.archivesName.get() + "-test-fabric") + + test { + useJUnitPlatform() + } + + processResources { + from(project(":archie-test-common").sourceSets.main.get().resources) { + include("assets/archie_test/**") + include("data/archie_test/**") + include("archie_test.common.json") + include("archie_test.accesswidener") + } + dependsOn(processTestResources) + } + + processTestResources { + } + + classes { + finalizedBy(testClasses) + } + + shadowJar { + configurations = + listOf(project.configurations.getByName("shadowCommon"), project.configurations.getByName("shadow")) + archiveClassifier.set("dev-shadow") + } + + remapJar { + injectAccessWidener.set(true) + inputFile.set(shadowJar.get().archiveFile) + dependsOn(shadowJar) + } + + jar.get().archiveClassifier.set("dev") + + sourcesJar { + val commonSources = project(":archie-test-common").tasks.sourcesJar + dependsOn(commonSources) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(commonSources.get().archiveFile.map { zipTree(it) }) + } +} diff --git a/Archie-Test/fabric/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestFabric.kt b/test/fabric/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestFabric.kt similarity index 100% rename from Archie-Test/fabric/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestFabric.kt rename to test/fabric/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestFabric.kt diff --git a/Archie-Test/fabric/src/main/resources/fabric.mod.json b/test/fabric/src/main/resources/fabric.mod.json similarity index 78% rename from Archie-Test/fabric/src/main/resources/fabric.mod.json rename to test/fabric/src/main/resources/fabric.mod.json index 1096ed1f2..e33df7f81 100644 --- a/Archie-Test/fabric/src/main/resources/fabric.mod.json +++ b/test/fabric/src/main/resources/fabric.mod.json @@ -1,8 +1,8 @@ { "schemaVersion": 1, - "id": "${mod_id}", + "id": "${mod_id}_test", "version": "${mod_version}", - "name": "${mod_display_name}", + "name": "${mod_display_name} Test", "description": "${mod_description}", "authors": [ "${mod_authors}" @@ -16,11 +16,11 @@ }, "custom": { "catalogue": { - "banner": "assets/${mod_id}/banner.png" + "banner": "assets/${mod_id}_test/banner.png" } }, "license": "${mod_license}", - "icon": "assets/${mod_id}/icon.png", + "icon": "assets/${mod_id}_test/icon.png", "environment": "*", "entrypoints": { "main": [ @@ -42,6 +42,7 @@ "fabric-api": ">=${versions.fabric_api}", "fabric-language-kotlin": ">=${versions.kotlin_fabric}", "architectury": ">=${versions.architectury}", - "cloth-config": ">=${versions.cloth_config_range}" + "cloth-config": ">=${versions.cloth_config_range}", + "archie": ">=${mod_version}" } } \ No newline at end of file diff --git a/test/neoforge/build.gradle.kts b/test/neoforge/build.gradle.kts new file mode 100644 index 000000000..eaaa3658e --- /dev/null +++ b/test/neoforge/build.gradle.kts @@ -0,0 +1,171 @@ +import net.kernelpanicsoft.archie.plugin.bundleMod + +plugins { + alias(libs.plugins.shadow) + alias(libs.plugins.archie) +} + +architectury { + platformSetupLoomIde() + neoForge() +} + +actualizer { + actualizes(project(":archie-test-common")) +} + +configurations { + create("common") + create("shadowCommon") + configureEach { + // Keep NeoForge Kotlin runtime provided by KotlinLangForge only. + exclude(group = "thedarkcolour", module = "kotlinforforge-neoforge") + exclude(group = "remapped.thedarkcolour", module = "kotlinforforge-neoforge-1d1bcbf2") + } + compileClasspath.get().extendsFrom(configurations["common"]) + runtimeClasspath.get().extendsFrom(configurations["common"]) + testCompileClasspath.get().extendsFrom(compileClasspath.get()) + testRuntimeClasspath.get().extendsFrom(runtimeClasspath.get()) +} + +loom { + log4jConfigs.from(project(":archie-test-common").loom.log4jConfigs) + accessWidenerPath.set(project(":archie-test-common").loom.accessWidenerPath) + + mods { + maybeCreate("main").apply { + sourceSet(sourceSets.main.get()) + } + } + + runs { + getByName("client") { + name = "Minecraft Client" + source(sourceSets.main.get()) + vmArgs("-XX:+AllowEnhancedClassRedefinition") + property("kotlinx.coroutines.debug", "off") + } + getByName("server") { + name = "Minecraft Server" + source(sourceSets.main.get()) + property("kotlinx.coroutines.debug", "off") + vmArgs("-XX:+AllowEnhancedClassRedefinition") + } + create("datagen") { + data() + name = "Minecraft Datagen" + property("archie.datagen", "true") + property("archie.datagen.client", providers.gradleProperty("client_datagen").orElse("true").get()) + property("archie.datagen.server", providers.gradleProperty("server_datagen").orElse("true").get()) + property("kotlinx.coroutines.debug", "off") + programArgs("--all", "--mod", "archie_test") + programArgs("--output", file("src/main/generated").absolutePath) + } + + create("gametest") { + server() + name = "Minecraft GameTest" + property("neoforge.enableGameTest", "true") + property("neoforge.gameTestServer", "true") + property("archie.gametest", "true") + property("archie.gametest.modid", "archie_test") + property("kotlinx.coroutines.debug", "off") + providers.gradleProperty("archie.junit.gametest.function").orNull?.let { property("archie.junit.gametest.function", it) } + } + + create("gametestClient") { + client() + name = "Minecraft GameTest Client" + property("neoforge.enableGameTest", "true") + property("archie.gametest.side", "client") + property("archie.gametest", "true") + property("archie.gametest.modid", "archie_test") + property("kotlinx.coroutines.debug", "off") + providers.gradleProperty("archie.junit.gametest.function").orNull?.let { property("archie.junit.gametest.function", it) } + } + } +} + +sourceSets { + main { + resources { + srcDir("src/main/generated") + } + } +} + +dependencies { + "neoForge"(libs.neoforge) + modApi(libs.architectury.neoforge) + implementation(libs.kotlin.neoforge) + modRuntimeOnly(libs.rei.neoforge) + modRuntimeOnly(libs.catalogue.neoforge) + modRuntimeOnly(libs.clothConfig.neoforge) + bundleMod(libs.storage.neoforge) { exclude(group = "curse.maven") } + + implementation(libs.junit.jupiter.api) + testImplementation(libs.junit.jupiter.api) + testRuntimeOnly(libs.junit.jupiter.engine) + + "common"(project(":archie-test-common", "namedElements")) { isTransitive = false } + "shadowCommon"(project(":archie-test-common", "transformProductionNeoForge")) { isTransitive = false } + modApi(project(":archie-core-neoforge")) + modApi(project(":archie-datagen-neoforge")) + modApi(project(":archie-gametest-neoforge")) +} + +modResources { + filesMatching.add("META-INF/neoforge.mods.toml") +} + +tasks { + base.archivesName.set(base.archivesName.get() + "-test-neoforge") + + test { + useJUnitPlatform() + } + + processResources { + from(project(":archie-test-common").sourceSets.main.get().resources) { + include("assets/archie_test/**") + include("data/archie_test/**") + include("archie_test.common.json") + include("archie_test.accesswidener") + } + dependsOn(processTestResources) + } + + processTestResources { + } + + classes { + finalizedBy(testClasses) + } + + shadowJar { + exclude("fabric.mod.json") + configurations = + listOf(project.configurations.getByName("shadowCommon"), project.configurations.getByName("shadow")) + archiveClassifier.set("dev-shadow") + } + + remapJar { + inputFile.set(shadowJar.get().archiveFile) + atAccessWideners.set(setOf(loom.accessWidenerPath.get().asFile.name)) + dependsOn(shadowJar) + } + + jar.get().archiveClassifier.set("dev") + + jar { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(project(":archie-test-common").sourceSets.main.get().output) + } + + sourcesJar { + val commonSources = project(":archie-test-common").tasks.sourcesJar + dependsOn(commonSources) + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(commonSources.get().archiveFile.map { zipTree(it) }) + } +} diff --git a/Archie-Test/neoforge/gradle.properties b/test/neoforge/gradle.properties similarity index 95% rename from Archie-Test/neoforge/gradle.properties rename to test/neoforge/gradle.properties index 3ce8f5694..7da18ea6f 100644 --- a/Archie-Test/neoforge/gradle.properties +++ b/test/neoforge/gradle.properties @@ -1,2 +1 @@ loom.platform=neoforge - diff --git a/Archie-Test/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestNeoForge.kt b/test/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestNeoForge.kt similarity index 100% rename from Archie-Test/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestNeoForge.kt rename to test/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestNeoForge.kt diff --git a/Archie-Test/neoforge/src/main/resources/META-INF/neoforge.mods.toml b/test/neoforge/src/main/resources/META-INF/neoforge.mods.toml similarity index 61% rename from Archie-Test/neoforge/src/main/resources/META-INF/neoforge.mods.toml rename to test/neoforge/src/main/resources/META-INF/neoforge.mods.toml index e05e1ec07..9fecb812e 100644 --- a/Archie-Test/neoforge/src/main/resources/META-INF/neoforge.mods.toml +++ b/test/neoforge/src/main/resources/META-INF/neoforge.mods.toml @@ -4,44 +4,51 @@ issueTrackerURL = "" license = "${mod_license}" [[mods]] -modId = "${mod_id}" +modId = "${mod_id}_test" version = "${mod_version}" -displayName = "${mod_display_name}" +displayName = "${mod_display_name} Test" authors = "${mod_authors}" credits = "${mod_credits}" description = ''' ${mod_description} ''' -logoFile = "assets/${mod_id}/banner.png" +logoFile = "assets/${mod_id}_test/banner.png" displayURL = "${mod_url}" -[[dependencies."${mod_id}"]] +[[dependencies."${mod_id}_test"]] modId = "neoforge" type = "required" versionRange = "[${versions.neoforge_range},)" ordering = "NONE" side = "BOTH" -[[dependencies."${mod_id}"]] +[[dependencies."${mod_id}_test"]] modId = "minecraft" type = "required" versionRange = "[${versions.minecraft}]" ordering = "NONE" side = "BOTH" -#[[dependencies."${mod_id}"]] +#[[dependencies."${mod_id}_test"]] #modId = "klf" #type = "required" #versionRange = "[${versions.kotlin_neoforge_range},)" #ordering = "NONE" #side = "BOTH" -[[dependencies."${mod_id}"]] +[[dependencies."${mod_id}_test"]] modId = "cloth_config" type = "required" versionRange = "[${versions.cloth_config_range},)" ordering = "NONE" side = "BOTH" -[modproperties."${mod_id}"] -catalogueImageIcon = "assets/${mod_id}/icon.png" \ No newline at end of file +[[dependencies."${mod_id}_test"]] +modId = "archie" +type = "required" +versionRange = "[${mod_version},)" +ordering = "AFTER" +side = "BOTH" + +[modproperties."${mod_id}_test"] +catalogueImageIcon = "assets/${mod_id}_test/icon.png" \ No newline at end of file diff --git a/Archie-Test/neoforge/src/main/resources/pack.mcmeta b/test/neoforge/src/main/resources/pack.mcmeta similarity index 100% rename from Archie-Test/neoforge/src/main/resources/pack.mcmeta rename to test/neoforge/src/main/resources/pack.mcmeta From ae061d39870bdf7e34cab01bd9698de8419265c2 Mon Sep 17 00:00:00 2001 From: KernelPanic Date: Mon, 10 Aug 2026 22:12:31 -0400 Subject: [PATCH 4/9] Fold in modfusioner/modpublisher and per-module Maven publishing Ports the remaining pieces of the old Archie/build.gradle.kts's release pipeline that weren't part of the repo-collapse commit's scope: - modfusioner: merges only archie-core-fabric's and archie-core-neoforge's remapJar outputs into one artifact (fusioner { fabric { projectName = "archie-core-fabric" }; neoforge { projectName = "archie-core-neoforge" } }). archie-datagen/-gametest/-test are never fused - each ships as its own separate mod. build/assemble are finalizedBy(fusejars). Verified: fusejars alone produces build/artifacts/archie-core-merged-*.jar with both fabric.mod.json and neoforge.mods.toml, nothing else pulled in. - modpublisher: publisher{} block ported verbatim (CurseForge/Modrinth/ GitHub IDs, deps, artifact = tasks.fusejars.get()); publishCurseforge/ publishModrinth/publishGitHub/publishMod each dependsOn(generateChangelog). - Per-module Maven publishing to kernelpanicsoft.net's Reposilite: one MavenPublication per archie-core/-datagen/-gametest module (archie-test excluded - dev playground, never published), wired via extensions.configure("publishing") { ... } inside subprojects{} - the bare publishing { } DSL accessor isn't type-safe here since maven-publish is applied imperatively in the same script, not via a plugins{} block (confirmed by mirroring old Archie's own root build.gradle.kts, which hit the identical issue once already). Verified via publishToMavenLocal: exactly the 9 expected artifacts, zero archie-test-* ones. - Dropped the older, superseded root-level "mavenJava" publishing setup that existed alongside the newer per-module one in old Archie/ build.gradle.kts (different repo, different credentials convention) - the per-module one was the more recent, more specific mechanism. Verified: full ./gradlew build -x test still succeeds at the root across all 12 modules with dokka/mkdocs/fusioner/publisher/maven-publish wired in together. Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 25 +++++++--- build.gradle.kts | 114 ++++++++++++++++++++++++++++++++++++++++++++++ gradle.properties | 2 +- 3 files changed, 134 insertions(+), 7 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 19fff5645..fb4fcc01b 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,9 +41,22 @@ All commands below are run from the repo root. - Docs pipeline: `embedDokkaIntoMkDocs` then `publishDocs` (calls `mike deploy ...`); root `mkdocs.yml` contains `# !!! EMBEDDED DOKKA ... DO NOT COMMIT !!!` markers. CI (`.github/workflows/docs.yaml`) runs `./gradlew publishDocs` from the repo root. -- **Not currently wired** (a known gap from the single-repo migration, not yet ported): - `modfusioner` (`fusejars`, merged Fabric+NeoForge artifact) and `modpublisher` - (CurseForge/Modrinth/GitHub publishing) - see "Dependency and integration touchpoints" below. +- `./gradlew build`/`assemble` are `finalizedBy(fusejars)`, which merges only `archie-core-fabric`'s + and `archie-core-neoforge`'s `remapJar` outputs (`fusioner { fabric { projectName = + "archie-core-fabric" }; neoforge { projectName = "archie-core-neoforge" } }` in root + `build.gradle.kts`) into one artifact under `build/artifacts/` - `datagen`/`gametest`/`test` each + ship as their own separate mod and are never fused. `./gradlew publishCurseforge`/ + `publishModrinth`/`publishGitHub`/`publishMod` publish that merged jar (`publisher{}` block, same + file). +- Every `archie-core`/`archie-datagen`/`archie-gametest` module (not `archie-test` - dev playground, + never published) gets its own `MavenPublication` to kernelpanicsoft.net's Reposilite + (`./gradlew publishToMavenLocal`/`publish`), wired via `extensions.configure + ("publishing") { ... }` inside root `build.gradle.kts`'s `subprojects{}` (not the bare `publishing + { }` DSL accessor - that's only type-safe when the plugin is applied via a `plugins{}` block, and + `maven-publish` here is applied imperatively). Release vs. snapshot repo URL is chosen by whether + `version` ends in `SNAPSHOT`; credentials come from `local.properties` (`reposilite.username`/ + `reposilite.password`, gitignored, developer machines) or `REPOSILITE_USERNAME`/ + `REPOSILITE_PASSWORD` env vars (CI). ## Project-specific conventions - Keep resource/manifests tokenized using Gradle properties (`${mod_id}`, `${versions.*}`) in `fabric.mod.json` and `neoforge.mods.toml`. `datagen`/`gametest`/`test` each ship as their own mod, so their own modId is `${mod_id}_datagen`/`${mod_id}_gametest`/`${mod_id}_test`, not the bare `${mod_id}`. @@ -65,9 +78,9 @@ All commands below are run from the repo root. - `generateChangelog` (root `build.gradle.kts`) regenerates `CHANGELOG.md` synchronously from `.github/scripts/generate_release_notes.py` - kept separate from the reactive, tag-triggered `release-notes.yaml` workflow for the same reasons as before the migration (see the task's own - comment in `build.gradle.kts`). Packaging/publishing itself (`modfusioner`/`modpublisher`, the - tasks that used to `dependsOn` `generateChangelog`) isn't wired into the new build yet - port - from git history if reviving it. + comment in `build.gradle.kts`). `publishCurseforge`/`publishModrinth`/`publishGitHub`/`publishMod` + each `dependsOn(generateChangelog)` so `modpublisher`'s changelog (read straight off disk) is + always fresh when a publish task runs. - Mixins are split by scope: loader mixins in `core/fabric/src/main/resources/archie.mixins.json` and `core/neoforge/src/main/resources/archie.mixins.json`, common mixin config in `core/common/src/main/resources/archie-common.mixins.json`. `datagen`/`gametest` each have their diff --git a/build.gradle.kts b/build.gradle.kts index 621e05218..cf9722cdb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,4 +1,6 @@ import net.fabricmc.loom.api.LoomGradleExtensionAPI +import org.gradle.api.publish.PublishingExtension +import org.gradle.api.publish.maven.MavenPublication import org.jetbrains.kotlin.konan.properties.loadProperties plugins { @@ -11,6 +13,8 @@ plugins { alias(libs.plugins.kotlin.compose) alias(libs.plugins.compose) alias(libs.plugins.dokka.mkdocs) + alias(libs.plugins.modfusioner) + alias(libs.plugins.modpublisher) } architectury.minecraft = libs.versions.minecraft.get() @@ -25,6 +29,13 @@ val sharedProperties = kotlin.runCatching { } }.getOrNull() +// Not committed - local.properties (repo root) holds reposilite.username/reposilite.password for +// developer machines; CI supplies REPOSILITE_USERNAME/REPOSILITE_PASSWORD env vars instead. +val localProperties = kotlin.runCatching { + val localPropsFile = rootDir.resolve("local.properties") + if (localPropsFile.exists()) loadProperties(localPropsFile.path) else null +}.getOrNull() + val String.prop: String? get() = sharedProperties?.get(this)?.toString() @@ -99,6 +110,45 @@ subprojects { compileOnly("org.jetbrains:annotations:24.1.0") } + + // One MavenPublication per module, published to kernelpanicsoft.net's Reposilite - archie-core/ + // -datagen/-gametest are real consumable libraries; archie-test is a dev playground, never + // published (matches fusioner/dokka's own product/test split above). + if (!project.name.startsWith("archie-test-")) { + // allprojects{} (below) is what normally applies these, but it's declared after this + // subprojects{} block and hasn't run for this project yet - apply is idempotent, so + // re-applying here just guarantees ordering for the components["java"]/publishing{} access + // immediately below. + apply(plugin = "java") + apply(plugin = "maven-publish") + + extensions.configure("publishing") { + publications { + create("maven") { + artifactId = base.archivesName.get() + from(components["java"]) + } + } + + repositories { + mavenLocal() + maven { + name = "Reposilite" + val releasesUrl = "https://maven.kernelpanicsoft.net/releases" + val snapshotsUrl = "https://maven.kernelpanicsoft.net/snapshots" + + url = uri(if (version.toString().endsWith("SNAPSHOT")) snapshotsUrl else releasesUrl) + + credentials { + username = localProperties?.getProperty("reposilite.username") + ?: System.getenv("REPOSILITE_USERNAME") + password = localProperties?.getProperty("reposilite.password") + ?: System.getenv("REPOSILITE_PASSWORD") + } + } + } + } + } } allprojects { @@ -109,6 +159,7 @@ allprojects { apply(plugin = "org.jetbrains.compose") apply(plugin = "dev.opensavvy.dokka-mkdocs") apply(plugin = "architectury-plugin") + apply(plugin = "maven-publish") version = "mod_version".prop ?: "0.0.1-SNAPSHOT" group = "mod_group".prop ?: "net.kernelpanicsoft" @@ -136,6 +187,57 @@ allprojects { java.withSourcesJar() } +// Merges only archie-core's fabric+neoforge jars into one artifact - datagen/gametest/test each +// ship as their own separate mod and are never fused/published. +fusioner { + packageGroup = project.group.toString() + mergedJarName = "${project.base.archivesName.get()}-merged-${libs.versions.minecraft.get()}" + jarVersion = project.version.toString() + outputDirectory = "build/artifacts" + + fabric { + projectName = "archie-core-fabric" + inputTaskName = "remapJar" + } + + neoforge { + projectName = "archie-core-neoforge" + inputTaskName = "remapJar" + } +} + +publisher { + apiKeys { + curseforge("curseforge_api_key".localOrEnv) + modrinth("modrinth_api_key".localOrEnv) + } + + debug = true + + curseID = "1029738" + modrinthID = "archie" + githubRepo = "https://github.com/kernel-panic-codecave/Archie" + + projectVersion = "${libs.versions.minecraft.get()}-${project.version}" + displayName = "Archie-Merged-${projectVersion.get()}" + gameVersions = listOf("1.21.1") + loaders = listOf("neoforge", "fabric") + curseEnvironment = "both" + versionType = "alpha" + artifact = tasks.fusejars.get() + javaVersions = listOf(JavaVersion.VERSION_21) + + changelog = file("CHANGELOG.md") + + curseDepends { + required = listOf("fabric-api", "fabric-language-kotlin", "kotlinlangforge", "architectury-api", "cloth-config") + } + + modrinthDepends { + required = listOf("fabric-api", "fabric-language-kotlin", "kotlin-lang-forge", "architectury-api", "cloth-config") + } +} + dependencies { dokka(project(":archie-core-common")) { isTransitive = false } dokka(project(":archie-core-fabric")) { isTransitive = false } @@ -149,6 +251,15 @@ dependencies { } tasks { + build { + finalizedBy(fusejars) + } + assemble { + finalizedBy(fusejars) + } + named("publish") { + dependsOn(publishMod) + } register("publishDocs") { dependsOn(getByName("embedDokkaIntoMkDocs")) group = "publishing" @@ -173,4 +284,7 @@ tasks { "--changelog-path", "CHANGELOG.md", ) } + listOf("publishCurseforge", "publishModrinth", "publishGitHub", "publishMod").forEach { + named(it) { dependsOn(getByName("generateChangelog")) } + } } diff --git a/gradle.properties b/gradle.properties index 7682d2f95..eb2bd8617 100644 --- a/gradle.properties +++ b/gradle.properties @@ -4,7 +4,7 @@ org.jetbrains.dokka.experimental.gradle.pluginMode=V2Enabled org.jetbrains.dokka.experimental.gradle.pluginMode.noWarn=true mod_id=archie -mod_group=net.kernelpanicsoft +mod_group=net.kernelpanicsoft.archie mod_version=1.0.0 mod_display_name=Archie mod_description=A library mod for Kernel Panic's mods From 16a225a81c97be0b33797f39920ca44ab2a726d5 Mon Sep 17 00:00:00 2001 From: KernelPanic Date: Mon, 10 Aug 2026 23:11:54 -0400 Subject: [PATCH 5/9] fix: never use mod* project deps between archie-core/-datagen/-gametest/-test CI's check job (kernel-panic-codecave/Archie#15) failed on a genuinely from-scratch checkout with: A problem occurred configuring project ':archie-datagen-common'. > Failed to setup Minecraft, java.io.UncheckedIOException: Failed to read metadata from core/common/build/libs/archie-core-common-1.0.0.jar Root cause: every cross-product dependency in datagen/gametest/test's build.gradle.kts files used modApi(project(":archie-core-*")) (and similarly for datagen/gametest from test). mod* configurations mark a dependency as needing Loom's intermediary<->named remapping - for a project(...) reference, that makes Loom eagerly read the target project's own output jar during *configuration*, before any task has executed. On a clean checkout that jar can't possibly exist yet, and no task ordering within one Gradle invocation can fix a configuration-time file read (confirmed: even `./gradlew help` alone fails this way). None of these cross-product dependencies need remapping at all - every product's fabric target is already namespace-symmetric with every other product's fabric target (same for neoforge/common), matching this session's original common-to-common design intent. Fix: replaced all 11 modApi(project(":archie-x-y")) call sites with plain api(project(":archie-x-y", "namedElements")) - the explicit "namedElements" target matters (a bare api(project(":x")) reintroduces the separate, already-solved transformProductionFabric/NeoForge "Type ... not present" issue that modApi was originally reached for). Deliberately did NOT add isTransitive = false to these calls (copied by habit from an unrelated custom-configuration pattern elsewhere in this build) - it strips the target project's own api-declared dependencies (compose.runtime, kotlinx-serialization) from flowing through, breaking compilation. Also reverts the "build archie-core first" CI workaround steps added while chasing a wrong initial diagnosis (a coincidental, non-reproducible "dirty cache" theory) - unnecessary now that the real dependency-shape bug is fixed. Verified: full ./gradlew build -x test, publishToMavenLocal, and fusejars all succeed from a completely clean checkout state (*/build/ + .gradle + .kotlin all removed beforehand) across all 12 modules. Co-Authored-By: Claude Sonnet 5 --- AGENTS.md | 17 ++++++++++++ datagen/common/build.gradle.kts | 8 +++--- datagen/fabric/build.gradle.kts | 2 +- datagen/neoforge/build.gradle.kts | 2 +- gametest/common/build.gradle.kts | 2 +- gametest/fabric/build.gradle.kts | 2 +- gametest/neoforge/build.gradle.kts | 2 +- settings.gradle.kts | 43 +++++++++--------------------- test/common/build.gradle.kts | 6 ++--- test/fabric/build.gradle.kts | 6 ++--- test/neoforge/build.gradle.kts | 6 ++--- 11 files changed, 49 insertions(+), 47 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index fb4fcc01b..68ed9a210 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -34,6 +34,23 @@ ## Build and run workflows All commands below are run from the repo root. - Build everything: `./gradlew build`. +- **Never use `mod*` configurations (`modApi`/`modImplementation`/`modCompileOnly`/etc.) on a + `project(...)` reference between archie-core/-datagen/-gametest/-test** - `mod*` marks a + dependency as needing Loom's intermediary<->named *remapping*, and for a project reference that + makes Loom eagerly read that project's own output jar during *configuration* (before any task + has run), which reliably breaks the very first build on a clean checkout with "Failed to setup + Minecraft ... NoSuchFileException: .../build/libs/archie-x-*.jar". None of these cross-product + dependencies need remapping at all - every product's fabric target is already namespace-symmetric + with every other product's fabric target (same for neoforge, same for common). Use plain + `api(project(":archie-x-y", "namedElements"))` instead (the explicit `"namedElements"` target + matters - it's what makes the Architectury Transformer's own classpath resolve the target + project's classes correctly; a bare `api(project(":archie-x-y"))` with no target reintroduces the + `transformProductionFabric`/`transformProductionNeoForge` "Type ... not present" failure this was + chosen to avoid). Do **not** add `{ isTransitive = false }` to these `api(...)` calls either - that + strips the target project's own `api`-declared dependencies (e.g. `archie-core-common`'s + `compose.runtime`/`kotlinx-serialization`) from flowing through to whichever module declared the + dependency, breaking compilation with "Unresolved reference" on types that module never + redeclares itself. - Loader-specific dev runs: `./gradlew archie-core-fabric:runClient`, `./gradlew archie-core-neoforge:runClient`. - Datagen runs are explicit tasks: `./gradlew archie-datagen-fabric:runDatagen` / `./gradlew archie-datagen-neoforge:runDatagen`. - GameTest runs: `./gradlew archie-gametest-fabric:runGametest` / `./gradlew archie-gametest-neoforge:runGametest` (server-side suite), diff --git a/datagen/common/build.gradle.kts b/datagen/common/build.gradle.kts index 086363cd1..0d4d95360 100644 --- a/datagen/common/build.gradle.kts +++ b/datagen/common/build.gradle.kts @@ -11,9 +11,11 @@ loom { } dependencies { - // modApi, not plain api - the architectury transformer needs archie-core-common as a tracked - // mod dependency to resolve its own classes (e.g. gui types referenced by datagen providers). - modApi(project(":archie-core-common")) + // Plain api, explicit "namedElements" target - not modApi. mod* on a project(...) reference + // makes Loom eagerly read that project's output jar during *configuration*, which can't + // possibly exist yet on a from-scratch build (mod* is for real remapping needs; this and + // archie-core-common are already namespace-symmetric, nothing to remap). + api(project(":archie-core-common", "namedElements")) modApi(libs.architectury.common) compileOnly(kotlin("reflect")) diff --git a/datagen/fabric/build.gradle.kts b/datagen/fabric/build.gradle.kts index fbda67cf8..96cb5b106 100644 --- a/datagen/fabric/build.gradle.kts +++ b/datagen/fabric/build.gradle.kts @@ -71,7 +71,7 @@ dependencies { testRuntimeOnly(libs.junit.jupiter.engine) "common"(project(":archie-datagen-common", "namedElements")) { isTransitive = false } - modApi(project(":archie-core-fabric")) + api(project(":archie-core-fabric", "namedElements")) } modResources { diff --git a/datagen/neoforge/build.gradle.kts b/datagen/neoforge/build.gradle.kts index e0f97e66c..480ac217c 100644 --- a/datagen/neoforge/build.gradle.kts +++ b/datagen/neoforge/build.gradle.kts @@ -68,7 +68,7 @@ dependencies { testRuntimeOnly(libs.junit.jupiter.engine) "common"(project(":archie-datagen-common", "namedElements")) { isTransitive = false } - modApi(project(":archie-core-neoforge")) + api(project(":archie-core-neoforge", "namedElements")) } modResources { diff --git a/gametest/common/build.gradle.kts b/gametest/common/build.gradle.kts index ccfc6be89..81335356e 100644 --- a/gametest/common/build.gradle.kts +++ b/gametest/common/build.gradle.kts @@ -11,7 +11,7 @@ loom { } dependencies { - modApi(project(":archie-core-common")) + api(project(":archie-core-common", "namedElements")) modApi(libs.architectury.common) modApi(libs.storage.common) modApi(libs.storage.resources.common) diff --git a/gametest/fabric/build.gradle.kts b/gametest/fabric/build.gradle.kts index bc6284cd4..2a5dbda8c 100644 --- a/gametest/fabric/build.gradle.kts +++ b/gametest/fabric/build.gradle.kts @@ -69,7 +69,7 @@ dependencies { testRuntimeOnly(libs.junit.jupiter.engine) "common"(project(":archie-gametest-common", "namedElements")) { isTransitive = false } - modApi(project(":archie-core-fabric")) + api(project(":archie-core-fabric", "namedElements")) } modResources { diff --git a/gametest/neoforge/build.gradle.kts b/gametest/neoforge/build.gradle.kts index 6f5d33904..980954975 100644 --- a/gametest/neoforge/build.gradle.kts +++ b/gametest/neoforge/build.gradle.kts @@ -76,7 +76,7 @@ dependencies { testRuntimeOnly(libs.junit.jupiter.engine) "common"(project(":archie-gametest-common", "namedElements")) { isTransitive = false } - modApi(project(":archie-core-neoforge")) + api(project(":archie-core-neoforge", "namedElements")) } modResources { diff --git a/settings.gradle.kts b/settings.gradle.kts index ddc2b0788..4cd9a8e67 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -22,38 +22,21 @@ pluginManagement { } } -// libs.versions.toml sits at the conventional gradle/libs.versions.toml location now (it used to -// live one level up, outside archie-core's own project dir, hence the old explicit -// dependencyResolutionManagement { versionCatalogs { create("libs") { from(...) } } } block - Gradle -// auto-registers it from here, so that block is gone; adding it back double-registers "libs". - -// Matches terrarium-earth/Common-Storage-Lib's settings.gradle.kts layout: one nested -// / directory per platform, flattened into a single-level Gradle project name -// (e.g. core/fabric -> archie-core-fabric). archie-core is the library; archie-datagen and -// archie-gametest are its dev-time-only sibling modules; archie-test is the dev-playground mod -// that exercises all three. -includeCorePlatform("common") -includeCorePlatform("fabric") -includeCorePlatform("neoforge") - -includeModule("datagen", "common") -includeModule("datagen", "fabric") -includeModule("datagen", "neoforge") - -includeModule("gametest", "common") -includeModule("gametest", "fabric") -includeModule("gametest", "neoforge") - -includeModule("test", "common") -includeModule("test", "fabric") -includeModule("test", "neoforge") - -fun includeModule(name: String, platform: String) { +includeModule("core") + +includeModule("datagen") + +includeModule("gametest") + +includeModule("test") + +fun includeModulePlatform(name: String, platform: String) { include("$name/$platform") project(":$name/$platform").name = "archie-$name-$platform" } -fun includeCorePlatform(platform: String) { - include("core/$platform") - project(":core/$platform").name = "archie-core-$platform" +fun includeModule(name: String) { + includeModulePlatform(name, "common") + includeModulePlatform(name, "fabric") + includeModulePlatform(name, "neoforge") } diff --git a/test/common/build.gradle.kts b/test/common/build.gradle.kts index f8c49b805..2efc4d2e5 100644 --- a/test/common/build.gradle.kts +++ b/test/common/build.gradle.kts @@ -13,9 +13,9 @@ loom { } dependencies { - modApi(project(":archie-core-common")) - modApi(project(":archie-datagen-common")) - modApi(project(":archie-gametest-common")) + api(project(":archie-core-common", "namedElements")) + api(project(":archie-datagen-common", "namedElements")) + api(project(":archie-gametest-common", "namedElements")) testImplementation(libs.junit.jupiter.api) testImplementation(kotlin("reflect")) diff --git a/test/fabric/build.gradle.kts b/test/fabric/build.gradle.kts index 76114bbd9..11ac8f767 100644 --- a/test/fabric/build.gradle.kts +++ b/test/fabric/build.gradle.kts @@ -98,9 +98,9 @@ dependencies { "common"(project(":archie-test-common", "namedElements")) { isTransitive = false } "shadowCommon"(project(":archie-test-common", "transformProductionFabric")) { isTransitive = false } - modApi(project(":archie-core-fabric")) - modApi(project(":archie-datagen-fabric")) - modApi(project(":archie-gametest-fabric")) + api(project(":archie-core-fabric", "namedElements")) + api(project(":archie-datagen-fabric", "namedElements")) + api(project(":archie-gametest-fabric", "namedElements")) } modResources { diff --git a/test/neoforge/build.gradle.kts b/test/neoforge/build.gradle.kts index eaaa3658e..e8f6213de 100644 --- a/test/neoforge/build.gradle.kts +++ b/test/neoforge/build.gradle.kts @@ -109,9 +109,9 @@ dependencies { "common"(project(":archie-test-common", "namedElements")) { isTransitive = false } "shadowCommon"(project(":archie-test-common", "transformProductionNeoForge")) { isTransitive = false } - modApi(project(":archie-core-neoforge")) - modApi(project(":archie-datagen-neoforge")) - modApi(project(":archie-gametest-neoforge")) + api(project(":archie-core-neoforge", "namedElements")) + api(project(":archie-datagen-neoforge", "namedElements")) + api(project(":archie-gametest-neoforge", "namedElements")) } modResources { From 0abaa55ac6c59ca908c416c4a47b48efd8c1a9a9 Mon Sep 17 00:00:00 2001 From: KernelPanic Date: Mon, 10 Aug 2026 23:32:59 -0400 Subject: [PATCH 6/9] fix: port archie's own unit tests + GameTests.kt, wire missing useJUnitPlatform() Old Archie/common/src/test/ had 13 real JUnit5 unit test classes plus its own GameTests.kt (a @TestFactory dispatching real GameTest runs, same shape as archie-test's own) - none of it ever got ported when archie-core was first stood up on Loom. This was silent locally because -x test skips the test task but not compileTestKotlin, and even compileTestKotlin alone doesn't catch a missing useJUnitPlatform() (compiles fine, discovers 0 tests at execution time) - only actually running the test task and checking for real JUnit XML output surfaced it. - Ported the 13 plain unit tests straight to core/common/src/test/ (none reference gametest - just core's own gui/networking/util packages). - Moved GameTests.kt to gametest/common/src/test/ instead - it needs archie-gametest-common's own `internal fun archieGameTests()`, which a different module's test sourceSet can't see. Fixed its stale AEvents reference (-> AGametestEvents) along the way, the same class of leftover reference this session's earlier datagen/gametest work fixed repeatedly - this exact file just hadn't been grepped yet. - core/common/build.gradle.kts had no `test { useJUnitPlatform() }` at all - a pre-existing gap, not new this session. Added it. gametest/common/build.gradle.kts got the full test{} block (the archie.junit.gametest.matrix systemProperty wiring GameTests.kt needs) ported from old Archie/common/build.gradle.kts verbatim. - junit-platform.properties (parallel-execution tuning GameTests.kt's @Execution(CONCURRENT) needs) copied to both core/common/src/test/ resources/ and gametest/common/src/test/resources/. Verified: :archie-core-common:test actually executes and passes all 13 test classes (real JUnit XML tests="13 files" failures="0" errors="0" counts, not just a green task), and a full ./gradlew build -x test compileTestKotlin still succeeds from a completely clean checkout state. Co-Authored-By: Claude Sonnet 5 --- core/common/build.gradle.kts | 4 + .../archie/testing/AnimationEasingTests.kt | 27 ++++ .../archie/testing/ArrayUtilsTests.kt | 56 ++++++++ .../archie/testing/CommonTests.kt | 18 +++ .../archie/testing/GuiClientHarnessTests.kt | 62 +++++++++ .../archie/testing/InputPrimitiveTests.kt | 40 ++++++ .../archie/testing/LayoutCoreTests.kt | 68 ++++++++++ .../archie/testing/MutableEntryTests.kt | 64 +++++++++ .../testing/NetworkChannelValidationTests.kt | 40 ++++++ .../archie/testing/PaddingMarginTests.kt | 125 ++++++++++++++++++ .../archie/testing/ReflectionUtilsTests.kt | 66 +++++++++ .../archie/testing/ResourceLocationTests.kt | 78 +++++++++++ .../archie/testing/ScrollableLayoutTests.kt | 24 ++++ .../testing/ScrollableStateSmokeTests.kt | 32 +++++ .../test/resources/junit-platform.properties | 17 +++ gametest/common/build.gradle.kts | 20 +++ .../archie/testing/GameTests.kt | 16 +++ .../test/resources/junit-platform.properties | 17 +++ .../archie/test/testing/GameTests.kt | 4 +- 19 files changed, 776 insertions(+), 2 deletions(-) create mode 100644 core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/AnimationEasingTests.kt create mode 100644 core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ArrayUtilsTests.kt create mode 100644 core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/CommonTests.kt create mode 100644 core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GuiClientHarnessTests.kt create mode 100644 core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/InputPrimitiveTests.kt create mode 100644 core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/LayoutCoreTests.kt create mode 100644 core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/MutableEntryTests.kt create mode 100644 core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/NetworkChannelValidationTests.kt create mode 100644 core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/PaddingMarginTests.kt create mode 100644 core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ReflectionUtilsTests.kt create mode 100644 core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ResourceLocationTests.kt create mode 100644 core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableLayoutTests.kt create mode 100644 core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableStateSmokeTests.kt create mode 100644 core/common/src/test/resources/junit-platform.properties create mode 100644 gametest/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt create mode 100644 gametest/common/src/test/resources/junit-platform.properties diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index c453eeb18..78a3cc9d8 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -98,4 +98,8 @@ tasks { sourcesJar { exclude("**/*Stub.kt") } + + test { + useJUnitPlatform() + } } diff --git a/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/AnimationEasingTests.kt b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/AnimationEasingTests.kt new file mode 100644 index 000000000..e67ee4e56 --- /dev/null +++ b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/AnimationEasingTests.kt @@ -0,0 +1,27 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.gui.animation.Easings +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test +import kotlin.math.abs + +class AnimationEasingTests { + @Test + fun testOutCubicAnchors() { + assertEquals(0f, Easings.OutCubic.transform(0f)) + assertEquals(1f, Easings.OutCubic.transform(1f)) + } + + @Test + fun testOutBackOvershoots() { + val sample = Easings.OutBack.transform(0.9f) + assertTrue(sample > 1f) { "OutBack should overshoot near the end, got $sample" } + } + + @Test + fun testLinearMidpoint() { + val sample = Easings.Linear.transform(0.5f) + assertTrue(abs(sample - 0.5f) < 0.0001f) { "Expected 0.5, got $sample" } + } +} \ No newline at end of file diff --git a/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ArrayUtilsTests.kt b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ArrayUtilsTests.kt new file mode 100644 index 000000000..63e999c57 --- /dev/null +++ b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ArrayUtilsTests.kt @@ -0,0 +1,56 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.util.buildArray +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class ArrayUtilsTests { + @Test + fun testBuildArrayCreatesCorrectArray() { + val array = buildArray { + add(1) + add(2) + add(3) + } + assertEquals(3, array.size) + assertEquals(1, array[0]) + assertEquals(2, array[1]) + assertEquals(3, array[2]) + } + + @Test + fun testBuildArrayWithCapacity() { + val array = buildArray(5) { + add("a") + add("b") + } + assertEquals(2, array.size) + assertEquals("a", array[0]) + assertEquals("b", array[1]) + } + + @Test + fun testBuildArrayWithEmptyList() { + val array = buildArray { + // Empty + } + assertEquals(0, array.size) + } + + @Test + fun testBuildArrayPreservesOrder() { + val array = buildArray { + add(5) + add(4) + add(3) + add(2) + add(1) + } + assertEquals(5, array[0]) + assertEquals(4, array[1]) + assertEquals(3, array[2]) + assertEquals(2, array[3]) + assertEquals(1, array[4]) + } +} + diff --git a/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/CommonTests.kt b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/CommonTests.kt new file mode 100644 index 000000000..5d037ddda --- /dev/null +++ b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/CommonTests.kt @@ -0,0 +1,18 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.Archie +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class CommonTests { + @Test + fun testModIdConstant() { + assertEquals("archie", Archie.MOD_ID) + } + + @Test + fun testModIdNotBlank() { + assertTrue(Archie.MOD_ID.isNotBlank()) + } +} diff --git a/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GuiClientHarnessTests.kt b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GuiClientHarnessTests.kt new file mode 100644 index 000000000..ce8e2b3a7 --- /dev/null +++ b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GuiClientHarnessTests.kt @@ -0,0 +1,62 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.gui.composables.containers.resolveScrollableContentAxis +import net.kernelpanicsoft.archie.gui.composables.containers.resolveScrollableViewportAxis +import net.kernelpanicsoft.archie.gui.composables.input.normalizeSliderValue +import net.kernelpanicsoft.archie.gui.composables.input.resolveSliderThumbX +import net.kernelpanicsoft.archie.gui.composables.input.resolveSwitchThumbOffset +import net.kernelpanicsoft.archie.gui.composables.input.snapSliderValue +import net.kernelpanicsoft.archie.gui.layout.IntCoordinates +import net.kernelpanicsoft.archie.gui.layout.IntRect +import net.kernelpanicsoft.archie.gui.layout.Size +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertNotNull +import org.junit.jupiter.api.Test + +class GuiClientHarnessTests { + @Test + fun testSliderNormalization() { + assertEquals(0f, normalizeSliderValue(-0.1f)) + assertEquals(0.5f, normalizeSliderValue(0.5f)) + assertEquals(1f, normalizeSliderValue(1.5f)) + } + + @Test + fun testSliderStepSnapping() { + assertEquals(0f, snapSliderValue(0.1f, steps = 4)) + assertEquals(0.5f, snapSliderValue(0.49f, steps = 4)) + assertEquals(1f, snapSliderValue(0.99f, steps = 4)) + } + + @Test + fun testScrollableAxisResolution() { + assertEquals(140, resolveScrollableViewportAxis(childSize = 24, min = 0, max = 140)) + assertEquals(24, resolveScrollableViewportAxis(childSize = 24, min = 0, max = Int.MAX_VALUE)) + assertEquals(24, resolveScrollableContentAxis(childSize = 24, min = 0, max = 140)) + } + + @Test + fun testClipRectIntersection() { + val a = IntRect.fromPositionAndSize(IntCoordinates(10, 10), Size(30, 20)) + val b = IntRect.fromPositionAndSize(IntCoordinates(25, 20), Size(20, 20)) + val intersection = a.intersect(b) + + assertNotNull(intersection) + assertEquals(IntRect(25, 20, 40, 30), intersection) + } + + @Test + fun testSwitchThumbOffsetNarrowTrack() { + assertEquals(2, resolveSwitchThumbOffset(thumbOffset = 30, trackWidth = 0)) + assertEquals(2, resolveSwitchThumbOffset(thumbOffset = 30, trackWidth = 12)) + assertEquals(18, resolveSwitchThumbOffset(thumbOffset = 18, trackWidth = 34)) + } + + @Test + fun testSliderThumbXNarrowWidth() { + assertEquals(15, resolveSliderThumbX(rawThumbX = 40, sliderX = 15, sliderWidth = 0)) + assertEquals(15, resolveSliderThumbX(rawThumbX = -5, sliderX = 15, sliderWidth = 6)) + assertEquals(22, resolveSliderThumbX(rawThumbX = 22, sliderX = 15, sliderWidth = 15)) + } +} + diff --git a/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/InputPrimitiveTests.kt b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/InputPrimitiveTests.kt new file mode 100644 index 000000000..b9a9c194e --- /dev/null +++ b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/InputPrimitiveTests.kt @@ -0,0 +1,40 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.gui.composables.input.normalizeSliderValue +import net.kernelpanicsoft.archie.gui.composables.input.resolveSliderThumbX +import net.kernelpanicsoft.archie.gui.composables.input.resolveSwitchThumbOffset +import net.kernelpanicsoft.archie.gui.composables.input.snapSliderValue +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class InputPrimitiveTests { + @Test + fun testSliderNormalizeClamps() { + assertEquals(0f, normalizeSliderValue(-1f)) + assertEquals(0.25f, normalizeSliderValue(0.25f)) + assertEquals(1f, normalizeSliderValue(2f)) + } + + @Test + fun testSliderSnapRespectsSteps() { + assertEquals(0.5f, snapSliderValue(0.49f, steps = 4)) + assertEquals(0.75f, snapSliderValue(0.74f, steps = 4)) + assertEquals(1f, snapSliderValue(1.4f, steps = 4)) + } + + @Test + fun testSwitchThumbOffsetHandlesNarrowTrack() { + assertEquals(2, resolveSwitchThumbOffset(thumbOffset = 10, trackWidth = 0)) + assertEquals(2, resolveSwitchThumbOffset(thumbOffset = 10, trackWidth = 10)) + assertEquals(2, resolveSwitchThumbOffset(thumbOffset = -5, trackWidth = 18)) + assertEquals(4, resolveSwitchThumbOffset(thumbOffset = 4, trackWidth = 20)) + } + + @Test + fun testSliderThumbXHandlesNarrowWidth() { + assertEquals(15, resolveSliderThumbX(rawThumbX = 20, sliderX = 15, sliderWidth = 0)) + assertEquals(15, resolveSliderThumbX(rawThumbX = -10, sliderX = 15, sliderWidth = 2)) + assertEquals(19, resolveSliderThumbX(rawThumbX = 19, sliderX = 15, sliderWidth = 12)) + } +} + diff --git a/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/LayoutCoreTests.kt b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/LayoutCoreTests.kt new file mode 100644 index 000000000..38a95b5b0 --- /dev/null +++ b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/LayoutCoreTests.kt @@ -0,0 +1,68 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.gui.layout.Alignment +import net.kernelpanicsoft.archie.gui.layout.IntCoordinates +import net.kernelpanicsoft.archie.gui.layout.IntSize +import net.kernelpanicsoft.archie.gui.layout.LayoutDirection +import net.kernelpanicsoft.archie.gui.layout.offset as layoutOffset +import net.kernelpanicsoft.archie.gui.layout.pos +import net.kernelpanicsoft.archie.gui.layout.size +import net.kernelpanicsoft.archie.gui.modifiers.Constraints +import net.kernelpanicsoft.archie.gui.modifiers.offset +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class LayoutCoreTests { + @Test + fun testIntCoordinatesMath() { + val a = IntCoordinates(20, 6) + val b = IntCoordinates(5, 4) + assertEquals(IntCoordinates(25, 10), a + b) + assertEquals(IntCoordinates(15, 2), a - b) + } + + @Test + fun testAliasesConstructExpectedCoordinates() { + assertEquals(IntCoordinates(2, 3), pos(2, 3)) + assertEquals(IntCoordinates(7, 9), layoutOffset(7, 9)) + assertEquals(IntSize(10, 11), size(10, 11)) + } + + @Test + fun testConstraintsCopyNormalizesBounds() { + val c = Constraints(minWidth = 90, maxWidth = 10, minHeight = 50, maxHeight = 12) + val normalized = c.copy() + assertEquals(10, normalized.minWidth) + assertEquals(90, normalized.maxWidth) + assertEquals(12, normalized.minHeight) + assertEquals(50, normalized.maxHeight) + } + + @Test + fun testConstraintsOffsetKeepsMaxUnbounded() { + val c = Constraints(maxWidth = Int.MAX_VALUE, maxHeight = Int.MAX_VALUE) + val shifted = c.offset(horizontal = -20, vertical = -30) + assertEquals(Int.MAX_VALUE, shifted.maxWidth) + assertEquals(Int.MAX_VALUE, shifted.maxHeight) + } + + @Test + fun testConstraintsOffsetCoercesAtZero() { + val c = Constraints(minWidth = 4, maxWidth = 8, minHeight = 3, maxHeight = 9) + val shifted = c.offset(horizontal = -20, vertical = -20) + assertEquals(0, shifted.minWidth) + assertEquals(0, shifted.maxWidth) + assertEquals(0, shifted.minHeight) + assertEquals(0, shifted.maxHeight) + } + + @Test + fun testAlignmentStartEndByDirection() { + val child = IntSize(20, 20) + val space = IntSize(100, 100) + assertEquals(IntCoordinates(0, 0), Alignment.TopStart.align(child, space, LayoutDirection.Ltr)) + assertEquals(IntCoordinates(80, 0), Alignment.TopStart.align(child, space, LayoutDirection.Rtl)) + assertEquals(IntCoordinates(40, 40), Alignment.Center.align(child, space, LayoutDirection.Ltr)) + } +} + diff --git a/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/MutableEntryTests.kt b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/MutableEntryTests.kt new file mode 100644 index 000000000..47f3b70c6 --- /dev/null +++ b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/MutableEntryTests.kt @@ -0,0 +1,64 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.util.MutableEntry +import net.kernelpanicsoft.archie.util.toMutableEntry +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class MutableEntryTests { + @Test + fun testMutableEntryCreation() { + val entry = MutableEntry("key", "value") + assertEquals("key", entry.key) + assertEquals("value", entry.value) + } + + @Test + fun testMutableEntryKeyMutation() { + val entry = MutableEntry("original", 42) + entry.key = "modified" + assertEquals("modified", entry.key) + assertEquals(42, entry.value) + } + + @Test + fun testMutableEntryValueMutation() { + val entry = MutableEntry("key", 10) + entry.value = 20 + assertEquals("key", entry.key) + assertEquals(20, entry.value) + } + + @Test + fun testPairToMutableEntry() { + val pair = Pair("pairKey", "pairValue") + val entry = pair.toMutableEntry() + assertEquals("pairKey", entry.key) + assertEquals("pairValue", entry.value) + } + + @Test + fun testMapEntryToMutableEntry() { + val map = mapOf("mapKey" to 100) + val mapEntry = map.entries.first() + val mutableEntry = mapEntry.toMutableEntry() + assertEquals("mapKey", mutableEntry.key) + assertEquals(100, mutableEntry.value) + } + + @Test + fun testMutableEntryEquality() { + val entry1 = MutableEntry("key", "value") + val entry2 = MutableEntry("key", "value") + assertEquals(entry1, entry2) + } + + @Test + fun testMutableEntryToString() { + val entry = MutableEntry("test", 123) + val str = entry.toString() + assertTrue("test" in str && "123" in str) { "toString should contain key and value" } + } +} + diff --git a/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/NetworkChannelValidationTests.kt b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/NetworkChannelValidationTests.kt new file mode 100644 index 000000000..1b255fd9e --- /dev/null +++ b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/NetworkChannelValidationTests.kt @@ -0,0 +1,40 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.networking.NetworkChannel +import net.kernelpanicsoft.archie.util.rem +import net.minecraft.resources.ResourceLocation +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +class NetworkChannelValidationTests { + private class NotDataClass(val value: Int) + private data class DataWithoutSerializer(val value: Int) + + @kotlinx.serialization.Serializable + private data class ValidPacket(val value: Int) + + @Test + fun testRejectsNonDataClass() { + val channel = NetworkChannel("archie" % "gametest_non_data") + assertThrows(IllegalArgumentException::class.java) { + channel.serverbound { _, _ -> } + } + } + + @Test + fun testRejectsDataWithoutSerializer() { + val channel = NetworkChannel("archie" % "gametest_no_serializer") + assertThrows(IllegalArgumentException::class.java) { + channel.clientbound { _, _ -> } + } + } + + @Test + fun testRejectsDuplicateRegistrations() { + val channel = NetworkChannel("archie" % "gametest_duplicate") + channel.clientbound { _, _ -> } + assertThrows(IllegalArgumentException::class.java) { + channel.clientbound { _, _ -> } + } + } +} diff --git a/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/PaddingMarginTests.kt b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/PaddingMarginTests.kt new file mode 100644 index 000000000..e4ec0645c --- /dev/null +++ b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/PaddingMarginTests.kt @@ -0,0 +1,125 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.gui.modifiers.Constraints +import net.kernelpanicsoft.archie.gui.modifiers.position.MarginModifier +import net.kernelpanicsoft.archie.gui.modifiers.position.MarginValues +import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingModifier +import net.kernelpanicsoft.archie.gui.modifiers.position.PaddingValues +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class PaddingMarginTests { + @Test + fun testPaddingModifierReducesConstraints() { + val padding = PaddingValues(left = 10, right = 10, top = 5, bottom = 5) + val modifier = PaddingModifier(padding) + + val constraints = Constraints( + minWidth = 100, + maxWidth = 200, + minHeight = 50, + maxHeight = 100, + ) + + val modified = modifier.modifyInnerConstraints(constraints) + + assertEquals(180, modified.maxWidth) + assertEquals(90, modified.maxHeight) + } + + @Test + fun testMarginModifierHorizontal() { + val margin = MarginValues(left = 5, right = 5, top = 3, bottom = 3) + val modifier = MarginModifier(margin) + + assertEquals(10, modifier.horizontal) + assertEquals(6, modifier.vertical) + } + + @Test + fun testPaddingValuesGetOffset() { + val padding = PaddingValues(left = 8, right = 12, top = 4, bottom = 6) + val offset = padding.getOffset() + + assertEquals(8, offset.x) + assertEquals(4, offset.y) + } + + @Test + fun testPaddingModifierNeverNegative() { + val largePadding = PaddingValues(left = 100, right = 100, top = 100, bottom = 100) + val modifier = PaddingModifier(largePadding) + + val constraints = Constraints( + minWidth = 0, + maxWidth = 50, + minHeight = 0, + maxHeight = 50, + ) + + val modified = modifier.modifyInnerConstraints(constraints) + + assertTrue(modified.maxWidth >= 0) { "Max width should never be negative" } + assertTrue(modified.maxHeight >= 0) { "Max height should never be negative" } + } + + @Test + fun testAsymmetricPadding() { + val padding = PaddingValues( + left = 5, + right = 15, + top = 10, + bottom = 20, + ) + val modifier = PaddingModifier(padding) + + assertEquals(20, modifier.horizontal) + assertEquals(30, modifier.vertical) + } + + @Test + fun testPaddingMerge() { + val padding1 = PaddingValues(left = 5, top = 5, right = 0, bottom = 0) + val padding2 = PaddingValues(left = 0, top = 0, right = 5, bottom = 5) + + val merged = padding1 + padding2 + + assertEquals(5, merged.left) + assertEquals(5, merged.right) + assertEquals(5, merged.top) + assertEquals(5, merged.bottom) + } + + @Test + fun testMarginMerge() { + val margin1 = MarginValues(left = 2, top = 2, right = 0, bottom = 0) + val margin2 = MarginValues(left = 0, top = 0, right = 3, bottom = 3) + + val merged = margin1 + margin2 + + assertEquals(2, merged.left) + assertEquals(3, merged.right) + assertEquals(2, merged.top) + assertEquals(3, merged.bottom) + } + + @Test + fun testPaddingReducesMinConstraints() { + val padding = PaddingValues(left = 10, right = 10, top = 10, bottom = 10) + val modifier = PaddingModifier(padding) + + val constraints = Constraints( + minWidth = 50, + maxWidth = 200, + minHeight = 50, + maxHeight = 200, + ) + + val modified = modifier.modifyInnerConstraints(constraints) + + assertEquals(30, modified.minWidth) + assertEquals(30, modified.minHeight) + } +} + diff --git a/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ReflectionUtilsTests.kt b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ReflectionUtilsTests.kt new file mode 100644 index 000000000..ea856bf4b --- /dev/null +++ b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ReflectionUtilsTests.kt @@ -0,0 +1,66 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.util.getReflection +import net.kernelpanicsoft.archie.util.setReflection +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertThrows +import org.junit.jupiter.api.Test + +class ReflectionUtilsTests { + private class TestClass { + @Suppress("unused") + private var privateField: String = "initial" + + @Suppress("unused") + private var numberField: Int = 42 + + fun getPrivateField(): String = privateField + } + + @Test + fun testGetReflection() { + val obj = TestClass() + val value: String = obj.getReflection("privateField") + assertEquals("initial", value) + } + + @Test + fun testSetReflection() { + val obj = TestClass() + obj.setReflection("privateField", "modified") + val retrieved: String = obj.getReflection("privateField") + assertEquals("modified", retrieved) + } + + @Test + fun testReflectionWithDifferentType() { + val obj = TestClass() + val value: Int = obj.getReflection("numberField") + assertEquals(42, value) + } + + @Test + fun testSetReflectionWithDifferentType() { + val obj = TestClass() + obj.setReflection("numberField", 99) + val retrieved: Int = obj.getReflection("numberField") + assertEquals(99, retrieved) + } + + @Test + fun testReflectionModifiesObjectState() { + val obj = TestClass() + assertEquals("initial", obj.getPrivateField()) + obj.setReflection("privateField", "changed") + assertEquals("changed", obj.getPrivateField()) + } + + @Test + fun testReflectionNonExistentFieldThrows() { + val obj = TestClass() + assertThrows(NoSuchFieldException::class.java) { + obj.getReflection("nonexistent") + } + } +} + diff --git a/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ResourceLocationTests.kt b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ResourceLocationTests.kt new file mode 100644 index 000000000..f69a1ea37 --- /dev/null +++ b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ResourceLocationTests.kt @@ -0,0 +1,78 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.util.div +import net.kernelpanicsoft.archie.util.rem +import net.minecraft.resources.ResourceLocation +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class ResourceLocationTests { + @Test + fun testNamespacePathOperator() { + val id = "archie_test" % "path/value" + assertEquals("archie_test", id.namespace) + assertEquals("path/value", id.path) + } + + @Test + fun testArchieNamespaceOperatorEquivalent() { + val id = "archie" % "main" + assertEquals("archie", id.namespace) + assertEquals("main", id.path) + } + + @Test + fun testResourceLocationDivisionOperators() { + val base = ResourceLocation.fromNamespaceAndPath("archie", "root") + val byString = base / "child" + val byResource = base / ResourceLocation.fromNamespaceAndPath("other", "leaf") + val withPrefix = "prefix" / base + + assertEquals("root/child", byString.path) + assertEquals("root/leaf", byResource.path) + assertEquals("prefix/root", withPrefix.path) + } + + @Test + fun testDivisionOperatorPreservesNamespace() { + val base = ResourceLocation.fromNamespaceAndPath("archie", "root") + val result = base / "child" + assertEquals("archie", result.namespace) + } + + @Test + fun testDivisionOperatorWithNestedPaths() { + val base = ResourceLocation.fromNamespaceAndPath("archie", "a") + val result = base / "b" / "c" / "d" + assertEquals("archie", result.namespace) + assertEquals("a/b/c/d", result.path) + } + + @Test + fun testRemOperatorWithEmptyPath() { + val id = "namespace" % "" + assertEquals("namespace", id.namespace) + assertEquals("", id.path) + } + + @Test + fun testRemOperatorWithComplexPaths() { + val id = "my_mod" % "blocks/custom_block" + assertEquals("my_mod", id.namespace) + assertEquals("blocks/custom_block", id.path) + } + + @Test + fun testResourceLocationCombinations() { + val id1 = "archie" % "test" + val id2 = id1 / "sub" + val id3 = "prefix" / id2 + + assertEquals("archie", id1.namespace) + assertEquals("test", id1.path) + assertEquals("archie", id2.namespace) + assertEquals("test/sub", id2.path) + assertEquals("archie", id3.namespace) + assertEquals("prefix/test/sub", id3.path) + } +} diff --git a/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableLayoutTests.kt b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableLayoutTests.kt new file mode 100644 index 000000000..3c84cffc9 --- /dev/null +++ b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableLayoutTests.kt @@ -0,0 +1,24 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.gui.composables.containers.resolveScrollableContentAxis +import net.kernelpanicsoft.archie.gui.composables.containers.resolveScrollableViewportAxis +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test + +class ScrollableLayoutTests { + @Test + fun testHorizontalScrollableWrapsHeight() { + val resolved = resolveScrollableContentAxis(childSize = 24, min = 0, max = 198) + assertEquals(24, resolved) + } + + @Test + fun testScrollableViewportAxisFillsBounds() { + val finite = resolveScrollableViewportAxis(childSize = 32, min = 0, max = 150) + assertEquals(150, finite) + + val unbounded = resolveScrollableViewportAxis(childSize = 32, min = 0, max = Int.MAX_VALUE) + assertEquals(32, unbounded) + } +} + diff --git a/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableStateSmokeTests.kt b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableStateSmokeTests.kt new file mode 100644 index 000000000..a9d7c2534 --- /dev/null +++ b/core/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/ScrollableStateSmokeTests.kt @@ -0,0 +1,32 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.gui.composables.containers.ScrollableState +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class ScrollableStateSmokeTests { + @Test + fun testScrollByClampsToRange() { + val state = ScrollableState().apply { maxScroll = 100 } + + state.scrollBy(250.0) + assertEquals(100.0, state.scrollOffset) + + state.scrollBy(-500.0) + assertEquals(0.0, state.scrollOffset) + } + + @Test + fun testScrollByUpdatesInteractionTimestamp() { + val state = ScrollableState().apply { maxScroll = 100 } + val before = state.lastInteractTime + + state.scrollBy(1.0) + + assertTrue(state.lastInteractTime >= before) { + "Expected interaction timestamp to increase after scrollBy" + } + } +} + diff --git a/core/common/src/test/resources/junit-platform.properties b/core/common/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..222e4cb1a --- /dev/null +++ b/core/common/src/test/resources/junit-platform.properties @@ -0,0 +1,17 @@ +# Parallel execution is enabled but defaults to same_thread, so every existing test class stays +# sequential unless it explicitly opts in via @Execution(CONCURRENT) (see GameTests). +junit.jupiter.execution.parallel.enabled=true +junit.jupiter.execution.parallel.mode.default=same_thread +junit.jupiter.execution.parallel.mode.classes.default=same_thread + +# Worker threads here spend most of their time blocked waiting on external Gradle/Minecraft +# processes rather than doing CPU work, so size the pool by a fixed count instead of core count +# (which on small CI runners could be lower than the GameTest matrix size). Each invocation test +# and each individual method test blocks its own thread until its specific result appears in the +# invocation's log (which can be minutes into a 20-minute run, since the game processes its own +# tests sequentially) - too small a pool here means most tests just queue for a thread that won't +# free up soon, never even starting even though their result may already be available. Sized well +# past worst-case demand (invocation count + every matching test method across the whole matrix) +# since these threads are cheap to over-provision. +junit.jupiter.execution.parallel.config.strategy=fixed +junit.jupiter.execution.parallel.config.fixed.parallelism=128 diff --git a/gametest/common/build.gradle.kts b/gametest/common/build.gradle.kts index 81335356e..2f77ed2a9 100644 --- a/gametest/common/build.gradle.kts +++ b/gametest/common/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.api.tasks.testing.logging.TestExceptionFormat + architectury { common("fabric", "neoforge") } @@ -37,4 +39,22 @@ tasks { sourcesJar { exclude("**/*Stub.kt") } + + test { + testClassesDirs = sourceSets.test.get().output.classesDirs + classpath = sourceSets.test.get().runtimeClasspath + useJUnitPlatform() + systemProperty("archie.junit.gametest", "true") + // Overridable via -Darchie.junit.gametest.matrix=... (a plain JVM system property, not a + // Gradle project property, so a CI job matrix reaches every product's :test at once). + systemProperty( + "archie.junit.gametest.matrix", + System.getProperty("archie.junit.gametest.matrix") ?: "fabric:server,fabric:client,neoforge:server,neoforge:client", + ) + systemProperty("archie.junit.gametest.timeoutMinutes", "20") + systemProperty("archie.junit.gametest.root", rootProject.rootDir.absolutePath) + testLogging { + exceptionFormat = TestExceptionFormat.FULL + } + } } diff --git a/gametest/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt b/gametest/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt new file mode 100644 index 000000000..d29995882 --- /dev/null +++ b/gametest/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt @@ -0,0 +1,16 @@ +package net.kernelpanicsoft.archie.testing + +import net.kernelpanicsoft.archie.events.AGametestEvents +import net.kernelpanicsoft.archie.gametest.internal.archieGameTests +import net.kernelpanicsoft.archie.gametest.junit.GameTestRunner +import org.junit.jupiter.api.DynamicContainer +import org.junit.jupiter.api.TestFactory +import org.junit.jupiter.api.parallel.Execution +import org.junit.jupiter.api.parallel.ExecutionMode + +@Execution(ExecutionMode.CONCURRENT) +class GameTests +{ + @TestFactory + fun tests(): Collection = GameTestRunner.tests("archie", AGametestEvents.ArchieGameTestBuilder::archieGameTests) +} \ No newline at end of file diff --git a/gametest/common/src/test/resources/junit-platform.properties b/gametest/common/src/test/resources/junit-platform.properties new file mode 100644 index 000000000..222e4cb1a --- /dev/null +++ b/gametest/common/src/test/resources/junit-platform.properties @@ -0,0 +1,17 @@ +# Parallel execution is enabled but defaults to same_thread, so every existing test class stays +# sequential unless it explicitly opts in via @Execution(CONCURRENT) (see GameTests). +junit.jupiter.execution.parallel.enabled=true +junit.jupiter.execution.parallel.mode.default=same_thread +junit.jupiter.execution.parallel.mode.classes.default=same_thread + +# Worker threads here spend most of their time blocked waiting on external Gradle/Minecraft +# processes rather than doing CPU work, so size the pool by a fixed count instead of core count +# (which on small CI runners could be lower than the GameTest matrix size). Each invocation test +# and each individual method test blocks its own thread until its specific result appears in the +# invocation's log (which can be minutes into a 20-minute run, since the game processes its own +# tests sequentially) - too small a pool here means most tests just queue for a thread that won't +# free up soon, never even starting even though their result may already be available. Sized well +# past worst-case demand (invocation count + every matching test method across the whole matrix) +# since these threads are cheap to over-provision. +junit.jupiter.execution.parallel.config.strategy=fixed +junit.jupiter.execution.parallel.config.fixed.parallelism=128 diff --git a/test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt b/test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt index a5402494a..60a2e712a 100644 --- a/test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt +++ b/test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt @@ -1,6 +1,6 @@ package net.kernelpanicsoft.archie.test.testing -import net.kernelpanicsoft.archie.events.AEvents +import net.kernelpanicsoft.archie.events.AGametestEvents import net.kernelpanicsoft.archie.gametest.junit.GameTestRunner import net.kernelpanicsoft.archie.test.gametest.archieTestGameTests import org.junit.jupiter.api.DynamicContainer @@ -12,5 +12,5 @@ import org.junit.jupiter.api.parallel.ExecutionMode class GameTests { @TestFactory - fun tests(): Collection = GameTestRunner.tests("archie_test", AEvents.ArchieGameTestBuilder::archieTestGameTests) + fun tests(): Collection = GameTestRunner.tests("archie_test", AGametestEvents.ArchieGameTestBuilder::archieTestGameTests) } \ No newline at end of file From d50b75f4ecdbad0043d40125bba2e5bd7e2ace6f Mon Sep 17 00:00:00 2001 From: KernelPanic Date: Tue, 11 Aug 2026 01:11:13 -0400 Subject: [PATCH 7/9] fix: JUnit GameTest bridge, missing dev cloth-config runtime, wrong mod filter CI's check job got further this time (past config, past compilation, into actually running real GameTests) and surfaced three more real bugs, found by literally running the GameTest suite locally instead of reasoning about it: 1. GameTestGradleInvocation.taskPath hardcoded the old composite-build task paths :fabric:runGametest/:neoforge:runGametest - stale from before the repo collapse, when archie-gametest and archie-test each had their own separate workspace root with its own bare :fabric project. Now both call through the same shared JUnit bridge but need different target projects (archie-gametest-fabric vs archie-test-fabric). Added a projectPrefix parameter threaded through GameTestRunner.tests(modID, projectPrefix, tests) -> GameTestGradleInvocation.parseMatrix -> taskPath (now :$projectPrefix-$loader:runGametest), updated both GameTests.kt call sites accordingly. 2. archie-gametest/-datagen's fabric/neoforge dev run configs (runGametest/runDatagen) crashed with NoClassDefFoundError: me/shedaniel/math/Color - archie-core's BuiltinSerializers.kt touches cloth-config's Color class unconditionally at class-init time even though cloth-config is compileOnly in the shipped jar. Old Archie/fabric and Archie/neoforge's own dev runs papered over this with modLocalRuntime/modRuntimeOnly(libs.clothConfig.*) - ported correctly to core and test already, but missed on datagen/gametest's own fabric+neoforge modules, whose dev runs also load archie-core as a mod dependency. Added the same dependency to all four. 3. archie-gametest-fabric/-neoforge's own gametest run config filtered by mod id "archie_gametest", but ArchieGameTest registers its tests under Archie.MOD ("archie", the library's own id) - matching old Archie/fabric's own run config, which always used mod_id ("archie"), never anything gametest-specific. Filtering by archie_gametest found zero tests. My own mistake writing this run config earlier this session, not an upstream porting error. Fixed to filter by "archie". Verified end to end, not just build-succeeded: :archie-gametest-common:test and :archie-test-common:test, each with -Darchie.junit.gametest.matrix=fabric:server, genuinely launch a real Minecraft server subprocess, run real @GameTest methods, and report failures="0" errors="0" in their JUnit XML output. Full ./gradlew build -x test compileTestKotlin still succeeds from a completely clean checkout. Co-Authored-By: Claude Sonnet 5 --- datagen/fabric/build.gradle.kts | 2 + datagen/neoforge/build.gradle.kts | 2 + .../gametest/junit/GameTestGradleExecutor.kt | 52 +++++++++---------- .../junit/GameTestGradleInvocation.kt | 30 ++++++----- .../archie/gametest/junit/GameTestRunner.kt | 7 +-- .../archie/testing/GameTests.kt | 2 +- gametest/fabric/build.gradle.kts | 9 +++- gametest/neoforge/build.gradle.kts | 6 ++- .../archie/test/testing/GameTests.kt | 2 +- 9 files changed, 62 insertions(+), 50 deletions(-) diff --git a/datagen/fabric/build.gradle.kts b/datagen/fabric/build.gradle.kts index 96cb5b106..f7ab6d928 100644 --- a/datagen/fabric/build.gradle.kts +++ b/datagen/fabric/build.gradle.kts @@ -65,6 +65,8 @@ dependencies { modApi(libs.fabric.api) modImplementation(libs.kotlin.fabric) compileOnly(libs.kotlinx.serialization) + // See the matching comment in gametest/fabric/build.gradle.kts. + modLocalRuntime(libs.clothConfig.fabric) implementation(libs.junit.jupiter.api) testImplementation(libs.junit.jupiter.api) diff --git a/datagen/neoforge/build.gradle.kts b/datagen/neoforge/build.gradle.kts index 480ac217c..83f616e2f 100644 --- a/datagen/neoforge/build.gradle.kts +++ b/datagen/neoforge/build.gradle.kts @@ -62,6 +62,8 @@ dependencies { "neoForge"(libs.neoforge) implementation(libs.kotlin.neoforge) compileOnly(libs.kotlinx.serialization) + // See the matching comment in gametest/fabric/build.gradle.kts. + modRuntimeOnly(libs.clothConfig.neoforge) implementation(libs.junit.jupiter.api) testImplementation(libs.junit.jupiter.api) diff --git a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt index 63dc0d8d8..f2eb18c5c 100644 --- a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt +++ b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleExecutor.kt @@ -66,26 +66,22 @@ internal object GameTestGradleExecutor { } /** - * Every invocation is a separate --no-daemon Gradle process, and (at least for Archie-Test) - * they all depend on the same upstream composite-build artifact (e.g. Archie:common's - * remapped jar) - starting all of them at once races multiple processes rebuilding/rewriting - * that shared output concurrently, corrupting it (observed: `:Archie:common:remapJar FAILED - * ... ZipException: invalid stored block lengths`). Whichever invocation calls [start] first - * claims [primingClaimed] and runs a single, fast `assemble` build to force those shared - * outputs to exist once; everyone else blocks on [primingComplete] until that finishes. Once - * it's done, Gradle's up-to-date checks mean every invocation's own process only reads the - * already-built artifact, so all of them - including the priming one - start their real - * (long-running) invocation concurrently right after, instead of one invocation blocking - * every other one on its entire run. + * Every invocation is a separate --no-daemon Gradle process, and several products' invocations + * (`archie-gametest`'s own suite, `archie-test`'s own suite) depend on the same shared upstream + * build outputs (e.g. `archie-core-fabric`'s remapped jar) - starting all of them at once races + * multiple processes rebuilding/rewriting that shared output concurrently, corrupting it. + * Whichever invocation calls [start] first claims [primingClaimed] and runs a single, fast + * `assemble` build to force those shared outputs to exist once; everyone else blocks on + * [primingComplete] until that finishes. Once it's done, Gradle's up-to-date checks mean every + * invocation's own process only reads the already-built artifact, so all of them - including + * the priming one - start their real (long-running) invocation concurrently right after, + * instead of one invocation blocking every other one on its entire run. * - * This alone only serializes invocations launched from the same JVM. `Archie`'s and - * `Archie-Test`'s own `:*:test` tasks each run in a *separate* Gradle test JVM, but - * Archie-Test composite-includes `../Archie` (see its settings.gradle.kts), so both JVMs' - * priming runs `assemble` against the very same `Archie:common` build output concurrently - - * this in-process guard does nothing across that boundary (observed: - * `:common:remapJar FAILED ... NoSuchFileException: archie-common-1.0.0.jar.tmp`, one - * process's remap temp file vanishing out from under the other). [withCrossProcessPrimingLock] - * closes that gap with an OS-level file lock shared by both JVMs. + * This alone only serializes invocations launched from the same JVM. `archie-gametest-common`'s + * and `archie-test-common`'s own `:*:test` tasks each run in a *separate* Gradle test JVM, and + * both JVMs' priming runs `assemble` against the very same shared build outputs concurrently - + * this in-process guard does nothing across that boundary. [withCrossProcessPrimingLock] closes + * that gap with an OS-level file lock shared by both JVMs. */ private val primingClaimed = AtomicBoolean(false) private val primingComplete = CompletableFuture() @@ -191,18 +187,18 @@ internal object GameTestGradleExecutor { * it's acquired. * * Every invocation for one workspaceRoot is spawned as a child --no-daemon Gradle process from - * the same `:common:test` JVM, so a plain in-JVM lock is enough here - unlike - * [withCrossProcessPrimingLock], which guards a race between *separate* JVMs (Archie's and - * Archie-Test's own `:*:test` tasks) and needs an OS-level file lock. (A `FileChannel` lock - * would be wrong here for a different reason too: `java.nio.channels.FileLock` throws - * `OverlappingFileLockException` rather than blocking when a *second* lock on the same file - * is requested from within the same JVM - it's designed to guard against other processes, not - * queue other threads in this one.) + * the same `:*-common:test` JVM, so a plain in-JVM lock is enough here - unlike + * [withCrossProcessPrimingLock], which guards a race between *separate* JVMs + * (`archie-gametest-common`'s and `archie-test-common`'s own `:*:test` tasks) and needs an + * OS-level file lock. (A `FileChannel` lock would be wrong here for a different reason too: + * `java.nio.channels.FileLock` throws `OverlappingFileLockException` rather than blocking when + * a *second* lock on the same file is requested from within the same JVM - it's designed to + * guard against other processes, not queue other threads in this one.) * * Unlike [withCrossProcessPrimingLock]'s one-time shared-artifact priming, this serializes * the *actual* invocation runs for the same loader (e.g. fabric:server and fabric:client), - * which both depend on and mutate that loader subproject's own build outputs - * (`:fabric:processResources` etc.) via their own separate, concurrently-launched + * which both depend on and mutate that loader project's own build outputs + * (`processResources` etc.) via their own separate, concurrently-launched * --no-daemon Gradle processes. Without this, one invocation's spawned Minecraft process can * read e.g. `fabric.mod.json` straight off disk at the exact moment the other invocation's * own build is mid-rewrite of that same file - observed as a `ParseMetadataException: diff --git a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt index aedb7de4f..48cb0f36c 100644 --- a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt +++ b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt @@ -12,22 +12,25 @@ enum class Side { CLIENT, } -/** One `loader:side` entry from [GameTestRunner]'s matrix, resolving to a single Gradle [taskPath] to run. */ +/** + * One `loader:side` entry from [GameTestRunner]'s matrix, resolving to a single Gradle [taskPath] + * to run. [projectPrefix] is which product's Gradle projects to target - e.g. `"archie-gametest"` + * for `archie-gametest`'s own self-test suite, `"archie-test"` for the playground mod's - since + * the repo root is now a single Gradle build shared by every product, not one workspace root per + * product the way it used to be. + */ data class GameTestGradleInvocation( val loader: Loader, val side: Side, + val projectPrefix: String, ) { - /** The fully-qualified Gradle task path that launches this invocation, e.g. `:fabric:runGametest`. */ + /** The fully-qualified Gradle task path that launches this invocation, e.g. `:archie-gametest-fabric:runGametest`. */ val taskPath: String - get() = when (loader) { - Loader.FABRIC -> when (side) { - Side.SERVER -> ":fabric:runGametest" - Side.CLIENT -> ":fabric:runGametestClient" - } - - Loader.NEOFORGE -> when (side) { - Side.SERVER -> ":neoforge:runGametest" - Side.CLIENT -> ":neoforge:runGametestClient" + get() { + val project = "$projectPrefix-${loader.name.lowercase()}" + return when (side) { + Side.SERVER -> ":$project:runGametest" + Side.CLIENT -> ":$project:runGametestClient" } } @@ -38,11 +41,11 @@ data class GameTestGradleInvocation( companion object { /** * Parses a comma-separated list of `loader:side` tokens (e.g. `"fabric:server,neoforge:client"`) - * into invocations, as used by [GameTestRunner.PROP_MATRIX]. + * into invocations targeting [projectPrefix], as used by [GameTestRunner.PROP_MATRIX]. * * @throws IllegalArgumentException if a token isn't in `loader:side` form or names an unknown [Loader]/[Side]. */ - fun parseMatrix(value: String): List { + fun parseMatrix(value: String, projectPrefix: String): List { if (value.isBlank()) return emptyList() return value.split(',').map { token -> val parts = token.trim().split(':') @@ -52,6 +55,7 @@ data class GameTestGradleInvocation( GameTestGradleInvocation( loader = Loader.valueOf(parts[0].trim().uppercase()), side = Side.valueOf(parts[1].trim().uppercase()), + projectPrefix = projectPrefix, ) } } diff --git a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt index 75781b05f..4c38ba4e1 100644 --- a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt +++ b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt @@ -39,9 +39,10 @@ object GameTestRunner * in the configured matrix, each running the loader's GameTest Gradle task once and then * reporting one [DynamicTest] per test method declared via [tests] (an * [AGametestEvents.ArchieGameTestBuilder] receiver, same DSL as [AGametestEvents.REGISTER_GAME_TEST]) whose - * side matches that invocation. + * side matches that invocation. [projectPrefix] is which product's Gradle projects the launched + * task targets (e.g. `"archie-gametest"`, `"archie-test"`) - see [GameTestGradleInvocation]. */ - fun tests(modID: String, tests: AGametestEvents.ArchieGameTestBuilder.() -> Unit): Collection + fun tests(modID: String, projectPrefix: String, tests: AGametestEvents.ArchieGameTestBuilder.() -> Unit): Collection { val enabled = System.getProperty(PROP_ENABLED)?.toBooleanStrictOrNull() == true if (!enabled) { @@ -55,7 +56,7 @@ object GameTestRunner } val matrixValue = System.getProperty(PROP_MATRIX) ?: DEFAULT_MATRIX - val invocations = GameTestGradleInvocation.parseMatrix(matrixValue) + val invocations = GameTestGradleInvocation.parseMatrix(matrixValue, projectPrefix) require(invocations.isNotEmpty()) { "No GameTest invocations configured. Set -D$PROP_MATRIX with at least one loader:side pair." } diff --git a/gametest/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt b/gametest/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt index d29995882..98042c291 100644 --- a/gametest/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt +++ b/gametest/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt @@ -12,5 +12,5 @@ import org.junit.jupiter.api.parallel.ExecutionMode class GameTests { @TestFactory - fun tests(): Collection = GameTestRunner.tests("archie", AGametestEvents.ArchieGameTestBuilder::archieGameTests) + fun tests(): Collection = GameTestRunner.tests("archie", "archie-gametest", AGametestEvents.ArchieGameTestBuilder::archieGameTests) } \ No newline at end of file diff --git a/gametest/fabric/build.gradle.kts b/gametest/fabric/build.gradle.kts index 2a5dbda8c..2fd16bead 100644 --- a/gametest/fabric/build.gradle.kts +++ b/gametest/fabric/build.gradle.kts @@ -45,7 +45,7 @@ loom { property("fabric-api.gametest") property("archie.gametest", "true") property("archie.gametest.side", "server") - property("archie.gametest.modid", "archie_gametest") + property("archie.gametest.modid", "archie") } create("gametestClient") { client() @@ -53,7 +53,7 @@ loom { property("fabric-api.gametest") property("archie.gametest", "true") property("archie.gametest.side", "client") - property("archie.gametest.modid", "archie_gametest") + property("archie.gametest.modid", "archie") } } } @@ -63,6 +63,11 @@ dependencies { modApi(libs.fabric.api) modImplementation(libs.kotlin.fabric) compileOnly(libs.kotlinx.serialization) + // archie-core-fabric's config/serialization code touches cloth-config's Color class + // unconditionally at class-init time even though the dependency itself is compileOnly in the + // shipped jar - dev-only runs (this module's own runGametest/runGametestClient, which load + // archie-core as a mod dependency) need it on the runtime classpath or that class-init crashes. + modLocalRuntime(libs.clothConfig.fabric) implementation(libs.junit.jupiter.api) testImplementation(libs.junit.jupiter.api) diff --git a/gametest/neoforge/build.gradle.kts b/gametest/neoforge/build.gradle.kts index 980954975..7219219d9 100644 --- a/gametest/neoforge/build.gradle.kts +++ b/gametest/neoforge/build.gradle.kts @@ -51,7 +51,7 @@ loom { property("neoforge.enableGameTest", "true") property("neoforge.gameTestServer", "true") property("archie.gametest", "true") - property("archie.gametest.modid", "archie_gametest") + property("archie.gametest.modid", "archie") property("kotlinx.coroutines.debug", "off") } create("gametestClient") { @@ -60,7 +60,7 @@ loom { property("neoforge.enableGameTest", "true") property("archie.gametest.side", "client") property("archie.gametest", "true") - property("archie.gametest.modid", "archie_gametest") + property("archie.gametest.modid", "archie") property("kotlinx.coroutines.debug", "off") } } @@ -70,6 +70,8 @@ dependencies { "neoForge"(libs.neoforge) implementation(libs.kotlin.neoforge) compileOnly(libs.kotlinx.serialization) + // See the matching comment in gametest/fabric/build.gradle.kts. + modRuntimeOnly(libs.clothConfig.neoforge) implementation(libs.junit.jupiter.api) testImplementation(libs.junit.jupiter.api) diff --git a/test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt b/test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt index 60a2e712a..2b2fa8fec 100644 --- a/test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt +++ b/test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt @@ -12,5 +12,5 @@ import org.junit.jupiter.api.parallel.ExecutionMode class GameTests { @TestFactory - fun tests(): Collection = GameTestRunner.tests("archie_test", AGametestEvents.ArchieGameTestBuilder::archieTestGameTests) + fun tests(): Collection = GameTestRunner.tests("archie_test", "archie-test", AGametestEvents.ArchieGameTestBuilder::archieTestGameTests) } \ No newline at end of file From 699627616eee9ec24175ad0e209237e2694ed3ad Mon Sep 17 00:00:00 2001 From: KernelPanic Date: Tue, 11 Aug 2026 12:59:38 -0400 Subject: [PATCH 8/9] fix: NeoForge JPMS split-packages, dev-mode mixin visibility, and two real mixin/NeoForge-version bugs Six more JPMS split-package overlaps (beyond the ones already fixed) between products sharing a NeoForge module layer, found via a systematic package scan and fixed by relocating the smaller side into a distinguishing sub-package with import fixes at every consumer: - core's `gametest` -> `gametest.platform` - core's `events` -> `events.base` - core's bare `data` -> `data.platform` - core's `data.common.tags` -> `data.common.tags.platform` - datagen's `data.common.conditions` -> `data.common.conditions.gen` - datagen/gametest's bare `mixin.neoforge` -> `mixin.neoforge.{datagen,gametest}` - datagen's and gametest's own `events` (ADatagenEvents/AGametestEvents, surfaced only once all four NeoForge mods load together via archie-test-neoforge) -> `events.{datagen,gametest}` Root-caused and fixed archie-core-neoforge's own dev-mode mixin visibility gap: its `mods{}` registration only listed its own sourceSet, never archie-core-common's, so any Java mixin class that isn't part of a Kotlin expect/actual pair (the actualizer plugin only merges Kotlin source) was invisible to FML's dev-mode module layer - "specified mixin ... was not found" on a plain `:archie-core-neoforge:runClient`, unrelated to anything this module split touched. For gametest/datagen/test-neoforge's own consumption of core (and, for test-neoforge, of datagen and gametest too): their "namedElements" dev-jar dependencies are production-*shaped* but not production-*complete* (the shadowJar merge that bakes archie-core-common's classes in is a production-only step dev mode skips), so cross-project consumers loaded an incomplete "archie" mod at runtime. Fixed by keeping namedElements compileOnly and adding a `modRuntimeOnly(files(...))` dependency on each upstream product's real `remapJar` output instead - the same jar a real downstream consumer would use. Found and fixed two genuine, pre-existing NeoForge-specific mixin bugs, invisible until a real NeoForge client boot got this far for the first time this session: NeoForge's own patches to `AbstractContainerScreen` (added between the project's 20.6.5-beta -> 21.1.80 NeoForge bump, i.e. predating this whole modularization effort) moved the `GuiGraphics#renderItem` call out of `renderSlot` into a new `renderSlotContents` extension point, and moved the `renderSlotHighlight` call in `render` onto a new protected instance overload - silently breaking two shared-common `@Redirect` mixins on NeoForge (Fabric's unpatched vanilla bytecode was never affected, so it kept working). Split both into loader-specific mixins targeting each loader's real call site; `SlotLayerDepthContext` gained a pending-override slot so the split-off pieces can still hand data to each other without a `@Unique` field (those don't cross mixin classes). Also: added architectury-api and (for datagen specifically, which touches Archie's own config/ TomlConfigSerializer at mod-construct time regardless of what the product itself needs) kotlinx-serialization/compose-runtime bundleRuntimeLibrary declarations directly to gametest/datagen-neoforge and datagen-fabric - archie-core's own bundleRuntimeLibrary calls only wire up its OWN dev-mode "userdev mods and services" locator, which doesn't carry over to a project consuming it as a dependency. Verified via real `./gradlew check` runs matching CI's exact invocation for both loaders (not just green builds) - 14/14 test suites, 0 failures/errors, real JUnit XML output inspected. Co-Authored-By: Claude Sonnet 5 --- .../gui/access/SlotLayerDepthContext.java | 20 +++++ .../gui/AbstractContainerScreenMixin.java | 79 +++---------------- .../net/kernelpanicsoft/archie/Archie.kt | 10 +-- .../kernelpanicsoft/archie/ArchieExtension.kt | 4 +- .../data/common/conditions/IACondition.kt | 2 +- .../common/tags/{ => platform}/ACommonTags.kt | 2 +- .../ADataGeneratorPlatform.common.kt | 2 +- .../events/{ => base}/ABasicEventObject.kt | 2 +- .../archie/events/{ => base}/AEventObject.kt | 2 +- .../ADedicatedServerPlatform.common.kt | 2 +- .../AGameTestPlatform.common.kt | 2 +- .../gametest/{ => platform}/ThreadingImpl.kt | 4 +- .../main/resources/archie-common.mixins.json | 1 + ...bstractContainerScreenItemRenderMixin.java | 52 ++++++++++++ ...ractContainerScreenSlotHighlightMixin.java | 59 ++++++++++++++ .../threading/MinecraftClientMixin.java | 2 +- .../mixin/fabric/threading/ServerMixin.java | 4 +- .../kernelpanicsoft/archie/ArchieFabric.kt | 2 +- .../ADataGeneratorPlatform.fabric.kt | 2 +- .../ADedicatedServerPlatform.kt | 2 +- .../ADedicatedServerPlatformInternal.kt | 2 +- .../{ => platform}/AGameTestPlatform.kt | 2 +- .../AGameTestPlatformInternal.kt | 2 +- .../src/main/resources/archie.mixins.json | 4 +- core/neoforge/build.gradle.kts | 5 ++ ...bstractContainerScreenItemRenderMixin.java | 53 +++++++++++++ ...ractContainerScreenSlotHighlightMixin.java | 67 ++++++++++++++++ .../threading/MinecraftClientMixin.java | 2 +- .../mixin/neoforge/threading/ServerMixin.java | 4 +- .../kernelpanicsoft/archie/ArchieNeoForge.kt | 2 +- .../ADataGeneratorPlatform.neoforge.kt | 2 +- .../ADedicatedServerPlatform.kt | 2 +- .../ADedicatedServerPlatformInternal.kt | 2 +- .../{ => platform}/AGameTestPlatform.kt | 2 +- .../AGameTestPlatformInternal.kt | 2 +- .../src/main/resources/archie.mixins.json | 4 +- .../archie/data/ADatagenEventObject.kt | 6 +- .../conditions/{ => gen}/AConditionBuilder.kt | 13 ++- .../ADatagenConditionsPlatform.common.kt | 8 +- .../common/conditions/{ => gen}/Extensions.kt | 3 +- .../data/common/crafting/ARecipeProvider.kt | 2 +- .../crafting/recipies/IARecipeBuilder.kt | 4 +- .../archie/data/internal/ArchieDatagen.kt | 4 +- .../common/tags/AInternalBiomeTagsProvider.kt | 2 +- .../common/tags/AInternalBlockTagsProvider.kt | 2 +- .../tags/AInternalEntityTypeTagsProvider.kt | 2 +- .../common/tags/AInternalFluidTagsProvider.kt | 2 +- .../common/tags/AInternalItemTagsProvider.kt | 2 +- .../events/{ => datagen}/ADatagenEvents.kt | 4 +- datagen/fabric/build.gradle.kts | 16 ++++ .../fabric/FabricDataGenHelperMixin.java | 2 +- .../data/ADataGeneratorPlatformInternal.kt | 4 +- .../ADatagenConditionsPlatform.fabric.kt | 3 +- datagen/neoforge/build.gradle.kts | 34 +++++++- .../{ => datagen}/DatagenModLoaderMixin.java | 4 +- .../data/ADataGeneratorPlatformInternal.kt | 4 +- .../ADatagenConditionsPlatform.neoforge.kt | 3 +- .../main/resources/archie_datagen.mixins.json | 2 +- .../events/{ => gametest}/AGametestEvents.kt | 8 +- .../archie/gametest/AClientGameTestHarness.kt | 5 ++ .../archie/gametest/AGameTestEventObject.kt | 4 +- .../archie/gametest/NoOpGameTest.kt | 4 +- .../gametest/internal/ArchieGameTest.kt | 2 +- .../junit/GameTestGradleInvocation.kt | 2 +- .../archie/gametest/junit/GameTestRunner.kt | 2 +- .../archie/testing/GameTests.kt | 2 +- gametest/fabric/build.gradle.kts | 5 +- .../FabricGameTestModInitializerMixin.java | 2 +- .../AGameTestClientHarnessInternal.kt | 5 +- .../gametest/AGameTestRegistrationBridge.kt | 11 ++- gametest/neoforge/build.gradle.kts | 32 +++++++- .../{ => gametest}/GameTestHooksMixin.java | 4 +- .../AGameTestClientHarnessInternal.kt | 5 +- .../gametest/AGameTestRegistrationBridge.kt | 4 +- .../resources/archie_gametest.mixins.json | 2 +- .../kernelpanicsoft/archie/test/ArchieTest.kt | 8 +- .../test/gametest/ArchieTestGameTest.kt | 2 +- .../archie/test/testing/GameTests.kt | 2 +- test/fabric/build.gradle.kts | 2 + test/neoforge/build.gradle.kts | 25 +++++- 80 files changed, 506 insertions(+), 170 deletions(-) rename core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/{ => platform}/ACommonTags.kt (99%) rename core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/{ => platform}/ADataGeneratorPlatform.common.kt (85%) rename core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/{ => base}/ABasicEventObject.kt (93%) rename core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/{ => base}/AEventObject.kt (97%) rename core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/{ => platform}/ADedicatedServerPlatform.common.kt (94%) rename core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/{ => platform}/AGameTestPlatform.common.kt (98%) rename core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/{ => platform}/ThreadingImpl.kt (99%) create mode 100644 core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/client/gui/AbstractContainerScreenItemRenderMixin.java create mode 100644 core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/client/gui/AbstractContainerScreenSlotHighlightMixin.java rename core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/{ => platform}/ADataGeneratorPlatform.fabric.kt (86%) rename core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/{ => platform}/ADedicatedServerPlatform.kt (98%) rename core/{neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest => fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform}/ADedicatedServerPlatformInternal.kt (97%) rename core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/{ => platform}/AGameTestPlatform.kt (96%) rename core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/{ => platform}/AGameTestPlatformInternal.kt (92%) create mode 100644 core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenItemRenderMixin.java create mode 100644 core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenSlotHighlightMixin.java rename core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/{ => platform}/ADataGeneratorPlatform.neoforge.kt (85%) rename core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/{ => platform}/ADedicatedServerPlatform.kt (98%) rename core/{fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest => neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform}/ADedicatedServerPlatformInternal.kt (97%) rename core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/{ => platform}/AGameTestPlatform.kt (96%) rename core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/{ => platform}/AGameTestPlatformInternal.kt (90%) rename datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/{ => gen}/AConditionBuilder.kt (77%) rename datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/{ => gen}/ADatagenConditionsPlatform.common.kt (67%) rename datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/{ => gen}/Extensions.kt (85%) rename datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/{ => datagen}/ADatagenEvents.kt (92%) rename datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/{ => gen}/ADatagenConditionsPlatform.fabric.kt (94%) rename datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/{ => datagen}/DatagenModLoaderMixin.java (91%) rename datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/{ => gen}/ADatagenConditionsPlatform.neoforge.kt (93%) rename gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/{ => gametest}/AGametestEvents.kt (93%) rename gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/{ => gametest}/GameTestHooksMixin.java (94%) diff --git a/core/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java b/core/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java index 30e433a7a..29ace0163 100644 --- a/core/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java +++ b/core/common/src/main/java/net/kernelpanicsoft/archie/gui/access/SlotLayerDepthContext.java @@ -11,11 +11,31 @@ public final class SlotLayerDepthContext { private static final ThreadLocal> DEPTHS = ThreadLocal.withInitial(ArrayDeque::new); + private static final ThreadLocal PENDING_OVERRIDE = new ThreadLocal<>(); private SlotLayerDepthContext() { } + /** + * Hands off a depth override computed while still inside {@code renderSlot} (see + * AbstractContainerScreenMixin) to the loader-specific mixin that wraps the actual + * {@code GuiGraphics#renderItem} call - NeoForge moved that call out of {@code renderSlot} + * into its own {@code renderSlotContents} method, so the two can no longer share a + * {@code @Unique} field on one mixin class. + */ + public static void setPendingOverride(Float depth) + { + PENDING_OVERRIDE.set(depth); + } + + public static Float takePendingOverride() + { + Float depth = PENDING_OVERRIDE.get(); + PENDING_OVERRIDE.remove(); + return depth; + } + public static void push(float depth) { Deque depths = DEPTHS.get(); diff --git a/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java b/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java index 14c7664d7..e4d69b4c1 100644 --- a/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java +++ b/core/common/src/main/java/net/kernelpanicsoft/archie/mixin/client/gui/AbstractContainerScreenMixin.java @@ -1,10 +1,8 @@ package net.kernelpanicsoft.archie.mixin.client.gui; import net.kernelpanicsoft.archie.Archie; -import net.kernelpanicsoft.archie.gui.access.SlotHighlightClipProvider; import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthContext; import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthProvider; -import net.kernelpanicsoft.archie.gui.layout.IntRect; import net.minecraft.client.gui.GuiGraphics; import net.minecraft.client.gui.screens.Screen; import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; @@ -12,101 +10,44 @@ import net.minecraft.network.chat.Component; import net.minecraft.world.inventory.AbstractContainerMenu; import net.minecraft.world.inventory.Slot; -import net.minecraft.world.item.ItemStack; import org.spongepowered.asm.mixin.Debug; import org.spongepowered.asm.mixin.Mixin; -import org.spongepowered.asm.mixin.Unique; import org.spongepowered.asm.mixin.injection.At; import org.spongepowered.asm.mixin.injection.ModifyArgs; -import org.spongepowered.asm.mixin.injection.Redirect; import org.spongepowered.asm.mixin.injection.invoke.arg.Args; @Debug(export = true) @Mixin(AbstractContainerScreen.class) public abstract class AbstractContainerScreenMixin extends Screen implements MenuAccess { - @Unique - private Float archie$slotDepthOverride; - protected AbstractContainerScreenMixin(Component title) { super(title); } + /** + * Computes the depth override and hands it off via {@link SlotLayerDepthContext} rather than + * a {@code @Unique} field - the actual {@code GuiGraphics#renderItem} call this override + * applies to is wrapped by a loader-specific mixin (NeoForge moved it out of this method into + * its own {@code renderSlotContents}, see AbstractContainerScreenItemRenderMixin in each + * loader module), so the two can no longer be @Unique fields on the same mixin class. + */ @ModifyArgs(method = "renderSlot", at = @At(value = "INVOKE", target = "Lcom/mojang/blaze3d/vertex/PoseStack;translate(FFF)V")) private void archie$adjustSlotLayer(Args args, GuiGraphics guiGraphics, Slot slot) { float originalZ = args.get(2); float adjustedZ = originalZ; - archie$slotDepthOverride = null; + Float custom = null; if (this instanceof SlotLayerDepthProvider provider) { - Float custom = provider.slotRenderLayerOffset(slot); - archie$slotDepthOverride = custom; + custom = provider.slotRenderLayerOffset(slot); if (custom != null) { adjustedZ = custom; Archie.LOGGER.debug("Adjusting slot layer depth for {} to {}", slot, custom); } } + SlotLayerDepthContext.setPendingOverride(custom); args.set(2, adjustedZ); } - - @Redirect(method = "renderSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;renderItem(Lnet/minecraft/world/item/ItemStack;III)V")) - private void archie$wrapSlotItemRender(GuiGraphics guiGraphics, ItemStack stack, int x, int y, int seed) - { - boolean pushed = false; - if (archie$slotDepthOverride != null) - { - SlotLayerDepthContext.push(archie$slotDepthOverride); - pushed = true; - } - try - { - guiGraphics.renderItem(stack, x, y, seed); - } - finally - { - if (pushed) - { - SlotLayerDepthContext.pop(); - } - archie$slotDepthOverride = null; - } - } - - /** - * Vanilla's per-slot hover highlight is drawn via a static helper that only takes the - * slot's raw x/y/blitOffset - there's no per-slot instance override point to clip it the - * way {@link #archie$adjustSlotLayer} clips the item icon, so this redirects the call - * site directly instead. - */ - @Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;renderSlotHighlight(Lnet/minecraft/client/gui/GuiGraphics;III)V")) - private void archie$clipSlotHighlight(GuiGraphics guiGraphics, int x, int y, int blitOffset) - { - IntRect clip = null; - if (this instanceof SlotHighlightClipProvider provider) - { - clip = provider.slotHighlightClipRect(x, y); - if (clip == null) - { - return; - } - } - if (clip != null) - { - guiGraphics.enableScissor(clip.getMinX(), clip.getMinY(), clip.getMaxX(), clip.getMaxY()); - } - try - { - AbstractContainerScreen.renderSlotHighlight(guiGraphics, x, y, blitOffset); - } - finally - { - if (clip != null) - { - guiGraphics.disableScissor(); - } - } - } } diff --git a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt index 590dc67d8..6a2df4b29 100644 --- a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt +++ b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt @@ -10,13 +10,13 @@ import net.kernelpanicsoft.archie.config.CategorySpec import net.kernelpanicsoft.archie.config.ConfigContainer import net.kernelpanicsoft.archie.config.ConfigSpec import net.kernelpanicsoft.archie.config.DataSpec -import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform +import net.kernelpanicsoft.archie.data.platform.ADataGeneratorPlatform import net.kernelpanicsoft.archie.data.common.conditions.ABuiltinConditions import net.kernelpanicsoft.archie.data.common.crafting.ingredients.ABuiltinIngredients -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags -import net.kernelpanicsoft.archie.gametest.AGameTestPlatform -import net.kernelpanicsoft.archie.gametest.AGameTestSide -import net.kernelpanicsoft.archie.gametest.ThreadingImpl +import net.kernelpanicsoft.archie.data.common.tags.platform.ACommonTags +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform +import net.kernelpanicsoft.archie.gametest.platform.AGameTestSide +import net.kernelpanicsoft.archie.gametest.platform.ThreadingImpl import net.kernelpanicsoft.archie.gui.blockentity.BlockEntityStateManager import net.kernelpanicsoft.archie.gui.item.ItemStateManager import net.kernelpanicsoft.archie.gui.theme.ThemeManifestResourceListener diff --git a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt index 4526a34a0..0ccae8b64 100644 --- a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt +++ b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt @@ -9,12 +9,12 @@ package net.kernelpanicsoft.archie */ interface ArchieExtension { - /** Called from [Archie.init] when running under a datagen task ([net.kernelpanicsoft.archie.data.ADataGeneratorPlatform.isDataGen]). */ + /** Called from [Archie.init] when running under a datagen task ([net.kernelpanicsoft.archie.data.platform.ADataGeneratorPlatform.isDataGen]). */ fun onDataGen() { } - /** Called from [Archie.init] when running under a GameTest task ([net.kernelpanicsoft.archie.gametest.AGameTestPlatform.isGameTest]). */ + /** Called from [Archie.init] when running under a GameTest task ([net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform.isGameTest]). */ fun onGameTest() { } diff --git a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt index de779e370..a5b1fae2b 100644 --- a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt +++ b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/IACondition.kt @@ -16,7 +16,7 @@ import net.minecraft.tags.TagKey * * Built-in implementations live alongside this file (e.g. [AAndCondition], [AOrCondition], * [ANotCondition], [ATrueCondition], [AModLoadedCondition], [ARegistryCondition]); see - * [ABuiltinConditions] for the full set and [AConditionBuilder] for a DSL to combine them. + * [ABuiltinConditions] for the full set and `archie-datagen`'s `AConditionBuilder` for a DSL to combine them. * Register custom conditions with [register]. */ interface IACondition diff --git a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/platform/ACommonTags.kt similarity index 99% rename from core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/platform/ACommonTags.kt index af825addb..e9b596d06 100644 --- a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/ACommonTags.kt +++ b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/tags/platform/ACommonTags.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.data.common.tags +package net.kernelpanicsoft.archie.data.common.tags.platform import net.minecraft.core.Registry import net.minecraft.core.registries.Registries diff --git a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/platform/ADataGeneratorPlatform.common.kt similarity index 85% rename from core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/platform/ADataGeneratorPlatform.common.kt index 94c57929b..424f496d1 100644 --- a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.common.kt +++ b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/data/platform/ADataGeneratorPlatform.common.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.data +package net.kernelpanicsoft.archie.data.platform /** * Cross-loader switch reporting whether the current run is a datagen run. diff --git a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/base/ABasicEventObject.kt similarity index 93% rename from core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/base/ABasicEventObject.kt index e53aa4518..d85a42fbd 100644 --- a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ABasicEventObject.kt +++ b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/base/ABasicEventObject.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.events +package net.kernelpanicsoft.archie.events.base import dev.architectury.event.Event diff --git a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/base/AEventObject.kt similarity index 97% rename from core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/base/AEventObject.kt index 37a2282d6..a4c456b84 100644 --- a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AEventObject.kt +++ b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/events/base/AEventObject.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.events +package net.kernelpanicsoft.archie.events.base import dev.architectury.event.Event import dev.architectury.platform.Mod diff --git a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatform.common.kt similarity index 94% rename from core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatform.common.kt index 83c95fae7..fba247ac7 100644 --- a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.common.kt +++ b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatform.common.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.gametest +package net.kernelpanicsoft.archie.gametest.platform import java.nio.file.Path import java.util.Properties diff --git a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatform.common.kt similarity index 98% rename from core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatform.common.kt index e903142f3..fe315d56f 100644 --- a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.common.kt +++ b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatform.common.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.gametest +package net.kernelpanicsoft.archie.gametest.platform import dev.architectury.platform.Mod import dev.architectury.utils.Env diff --git a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ThreadingImpl.kt similarity index 99% rename from core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt rename to core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ThreadingImpl.kt index 08367c1bb..06d815915 100644 --- a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ThreadingImpl.kt +++ b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ThreadingImpl.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.gametest +package net.kernelpanicsoft.archie.gametest.platform import net.minecraft.client.Minecraft import java.util.concurrent.Phaser @@ -27,7 +27,7 @@ private val serverRegisteredThread = AtomicReference(null) * the gametest thread to client/server threads. */ object ThreadingImpl { - private const val THREAD_IMPL_CLASS_NAME = "net.kernelpanicsoft.archie.gametest.ThreadingImpl" + private const val THREAD_IMPL_CLASS_NAME = "net.kernelpanicsoft.archie.gametest.platform.ThreadingImpl" private const val TASK_ON_THIS_THREAD_METHOD_NAME = "runTaskOnThisThread" private const val TASK_ON_OTHER_THREAD_METHOD_NAME = "runTaskOnOtherThread" private const val PHASE_MASK = 3 diff --git a/core/common/src/main/resources/archie-common.mixins.json b/core/common/src/main/resources/archie-common.mixins.json index 6c5186a87..2355de3f2 100644 --- a/core/common/src/main/resources/archie-common.mixins.json +++ b/core/common/src/main/resources/archie-common.mixins.json @@ -3,6 +3,7 @@ "package": "net.kernelpanicsoft.archie.mixin", "compatibilityLevel": "JAVA_17", "minVersion": "0.8", + "remap": false, "client": [ "client.gui.AbstractContainerScreenDepthMixin", "client.gui.AbstractContainerScreenMixin", diff --git a/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/client/gui/AbstractContainerScreenItemRenderMixin.java b/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/client/gui/AbstractContainerScreenItemRenderMixin.java new file mode 100644 index 000000000..829b9e0aa --- /dev/null +++ b/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/client/gui/AbstractContainerScreenItemRenderMixin.java @@ -0,0 +1,52 @@ +package net.kernelpanicsoft.archie.mixin.fabric.client.gui; + +import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthContext; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.network.chat.Component; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Wraps the {@code GuiGraphics#renderItem} call inside vanilla's own {@code renderSlot} with the + * depth override {@code AbstractContainerScreenMixin#archie$adjustSlotLayer} computed just before + * it. NeoForge's own patched {@code AbstractContainerScreen} moves this call into a separate + * {@code renderSlotContents} method, hence a per-loader mixin - see the NeoForge module's own + * copy of this class. + */ +@Mixin(AbstractContainerScreen.class) +public abstract class AbstractContainerScreenItemRenderMixin extends Screen implements MenuAccess +{ + protected AbstractContainerScreenItemRenderMixin(Component title) + { + super(title); + } + + @Redirect(method = "renderSlot", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;renderItem(Lnet/minecraft/world/item/ItemStack;III)V")) + private void archie$wrapSlotItemRender(GuiGraphics guiGraphics, ItemStack stack, int x, int y, int seed) + { + Float override = SlotLayerDepthContext.takePendingOverride(); + boolean pushed = false; + if (override != null) + { + SlotLayerDepthContext.push(override); + pushed = true; + } + try + { + guiGraphics.renderItem(stack, x, y, seed); + } + finally + { + if (pushed) + { + SlotLayerDepthContext.pop(); + } + } + } +} diff --git a/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/client/gui/AbstractContainerScreenSlotHighlightMixin.java b/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/client/gui/AbstractContainerScreenSlotHighlightMixin.java new file mode 100644 index 000000000..b74a8509c --- /dev/null +++ b/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/client/gui/AbstractContainerScreenSlotHighlightMixin.java @@ -0,0 +1,59 @@ +package net.kernelpanicsoft.archie.mixin.fabric.client.gui; + +import net.kernelpanicsoft.archie.gui.access.SlotHighlightClipProvider; +import net.kernelpanicsoft.archie.gui.layout.IntRect; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.network.chat.Component; +import net.minecraft.world.inventory.AbstractContainerMenu; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Vanilla's per-slot hover highlight is drawn via a static helper that only takes the slot's raw + * x/y/blitOffset - there's no per-slot instance override point to clip it the way + * AbstractContainerScreenMixin#archie$adjustSlotLayer clips the item icon, so this redirects the + * call site directly instead. NeoForge's own patched {@code AbstractContainerScreen} calls a + * different, newer overload here, hence a per-loader mixin - see the NeoForge module's own copy + * of this class. + */ +@Mixin(AbstractContainerScreen.class) +public abstract class AbstractContainerScreenSlotHighlightMixin extends Screen implements MenuAccess +{ + protected AbstractContainerScreenSlotHighlightMixin(Component title) + { + super(title); + } + + @Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;renderSlotHighlight(Lnet/minecraft/client/gui/GuiGraphics;III)V")) + private void archie$clipSlotHighlight(GuiGraphics guiGraphics, int x, int y, int blitOffset) + { + IntRect clip = null; + if (this instanceof SlotHighlightClipProvider provider) + { + clip = provider.slotHighlightClipRect(x, y); + if (clip == null) + { + return; + } + } + if (clip != null) + { + guiGraphics.enableScissor(clip.getMinX(), clip.getMinY(), clip.getMaxX(), clip.getMaxY()); + } + try + { + AbstractContainerScreen.renderSlotHighlight(guiGraphics, x, y, blitOffset); + } + finally + { + if (clip != null) + { + guiGraphics.disableScissor(); + } + } + } +} diff --git a/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java b/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java index ba85c6110..0e4b5b141 100644 --- a/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java +++ b/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/MinecraftClientMixin.java @@ -1,6 +1,6 @@ package net.kernelpanicsoft.archie.mixin.fabric.threading; -import net.kernelpanicsoft.archie.gametest.ThreadingImpl; +import net.kernelpanicsoft.archie.gametest.platform.ThreadingImpl; import net.minecraft.client.Minecraft; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; diff --git a/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java b/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java index b1c4ed829..73624b45d 100644 --- a/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java +++ b/core/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/threading/ServerMixin.java @@ -1,7 +1,7 @@ package net.kernelpanicsoft.archie.mixin.fabric.threading; -import net.kernelpanicsoft.archie.gametest.ThreadingImpl; -import net.kernelpanicsoft.archie.gametest.ADedicatedServerPlatformInternal; +import net.kernelpanicsoft.archie.gametest.platform.ThreadingImpl; +import net.kernelpanicsoft.archie.gametest.platform.ADedicatedServerPlatformInternal; import net.minecraft.server.MinecraftServer; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; diff --git a/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt index 640a5648d..39d52c6b7 100644 --- a/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt +++ b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/ArchieFabric.kt @@ -2,7 +2,7 @@ package net.kernelpanicsoft.archie import net.fabricmc.api.ClientModInitializer import net.fabricmc.api.ModInitializer -import net.kernelpanicsoft.archie.gametest.ThreadingImpl +import net.kernelpanicsoft.archie.gametest.platform.ThreadingImpl /** * Fabric entrypoint for the mod (`fabric.mod.json` `main`/`client` entrypoints). diff --git a/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/platform/ADataGeneratorPlatform.fabric.kt similarity index 86% rename from core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/platform/ADataGeneratorPlatform.fabric.kt index 771eb2f1f..76aa61354 100644 --- a/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.fabric.kt +++ b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/platform/ADataGeneratorPlatform.fabric.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.data +package net.kernelpanicsoft.archie.data.platform /** Fabric implementation of [ADataGeneratorPlatform]. */ @Suppress("unused") diff --git a/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatform.kt similarity index 98% rename from core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatform.kt index 8426bd1db..578643320 100644 --- a/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt +++ b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatform.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.gametest +package net.kernelpanicsoft.archie.gametest.platform import net.minecraft.Util import net.minecraft.server.Main diff --git a/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatformInternal.kt similarity index 97% rename from core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatformInternal.kt index 0f5163f50..56f3a5699 100644 --- a/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt +++ b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatformInternal.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.gametest +package net.kernelpanicsoft.archie.gametest.platform import net.minecraft.server.MinecraftServer import net.minecraft.server.dedicated.DedicatedServer diff --git a/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatform.kt similarity index 96% rename from core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatform.kt index 8d4dfcdbb..9a05b0aeb 100644 --- a/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt +++ b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatform.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.gametest +package net.kernelpanicsoft.archie.gametest.platform import dev.architectury.platform.Mod import dev.architectury.platform.Platform diff --git a/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatformInternal.kt similarity index 92% rename from core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt rename to core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatformInternal.kt index cc1535a42..90bb628ba 100644 --- a/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt +++ b/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatformInternal.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.gametest +package net.kernelpanicsoft.archie.gametest.platform import dev.architectury.platform.Mod diff --git a/core/fabric/src/main/resources/archie.mixins.json b/core/fabric/src/main/resources/archie.mixins.json index a4f977bee..489fa7330 100644 --- a/core/fabric/src/main/resources/archie.mixins.json +++ b/core/fabric/src/main/resources/archie.mixins.json @@ -5,7 +5,9 @@ "compatibilityLevel": "JAVA_17", "minVersion": "0.8", "client": [ - "threading.MinecraftClientMixin" + "threading.MinecraftClientMixin", + "client.gui.AbstractContainerScreenItemRenderMixin", + "client.gui.AbstractContainerScreenSlotHighlightMixin" ], "mixins": [ "threading.ServerMixin" diff --git a/core/neoforge/build.gradle.kts b/core/neoforge/build.gradle.kts index f02041c19..2416714a6 100644 --- a/core/neoforge/build.gradle.kts +++ b/core/neoforge/build.gradle.kts @@ -36,6 +36,11 @@ loom { mods { maybeCreate("main").apply { sourceSet(sourceSets.main.get()) + // actualizer only merges Kotlin expect/actual source into this project's own + // compilation - plain Java files in archie-core-common (e.g. mixin classes with no + // actual/expect involvement) never get copied in, so they're invisible to FML's + // dev-mode module layer unless their sourceSet is also registered here directly. + sourceSet(project(":archie-core-common").sourceSets.main.get()) } } diff --git a/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenItemRenderMixin.java b/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenItemRenderMixin.java new file mode 100644 index 000000000..9a14bb5b8 --- /dev/null +++ b/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenItemRenderMixin.java @@ -0,0 +1,53 @@ +package net.kernelpanicsoft.archie.mixin.neoforge.client.gui; + +import net.kernelpanicsoft.archie.gui.access.SlotLayerDepthContext; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.network.chat.Component; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.item.ItemStack; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * Wraps the {@code GuiGraphics#renderItem} call with the depth override + * {@code AbstractContainerScreenMixin#archie$adjustSlotLayer} computed just before it. + * NeoForge's own patched {@code AbstractContainerScreen} moves this call out of vanilla's + * {@code renderSlot} into its own {@code renderSlotContents} extension point (added between + * NeoForge 20.6.5-beta and 21.1.80), so this targets that method instead - see the Fabric + * module's own copy of this class, which targets {@code renderSlot} directly. + */ +@Mixin(AbstractContainerScreen.class) +public abstract class AbstractContainerScreenItemRenderMixin extends Screen implements MenuAccess +{ + protected AbstractContainerScreenItemRenderMixin(Component title) + { + super(title); + } + + @Redirect(method = "renderSlotContents", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/GuiGraphics;renderItem(Lnet/minecraft/world/item/ItemStack;III)V")) + private void archie$wrapSlotItemRender(GuiGraphics guiGraphics, ItemStack stack, int x, int y, int seed) + { + Float override = SlotLayerDepthContext.takePendingOverride(); + boolean pushed = false; + if (override != null) + { + SlotLayerDepthContext.push(override); + pushed = true; + } + try + { + guiGraphics.renderItem(stack, x, y, seed); + } + finally + { + if (pushed) + { + SlotLayerDepthContext.pop(); + } + } + } +} diff --git a/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenSlotHighlightMixin.java b/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenSlotHighlightMixin.java new file mode 100644 index 000000000..8c90da191 --- /dev/null +++ b/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/client/gui/AbstractContainerScreenSlotHighlightMixin.java @@ -0,0 +1,67 @@ +package net.kernelpanicsoft.archie.mixin.neoforge.client.gui; + +import net.kernelpanicsoft.archie.gui.access.SlotHighlightClipProvider; +import net.kernelpanicsoft.archie.gui.layout.IntRect; +import net.minecraft.client.gui.GuiGraphics; +import net.minecraft.client.gui.screens.Screen; +import net.minecraft.client.gui.screens.inventory.AbstractContainerScreen; +import net.minecraft.client.gui.screens.inventory.MenuAccess; +import net.minecraft.network.chat.Component; +import net.minecraft.world.inventory.AbstractContainerMenu; +import net.minecraft.world.inventory.Slot; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.gen.Invoker; +import org.spongepowered.asm.mixin.injection.At; +import org.spongepowered.asm.mixin.injection.Redirect; + +/** + * NeoForge-specific counterpart to AbstractContainerScreenMixin#archie$clipSlotHighlight: NeoForge + * moved the per-slot highlight draw out of the plain static {@code renderSlotHighlight(GuiGraphics, + * int, int, int)} helper vanilla's {@code render} calls directly, replacing that call site with a + * new protected instance overload {@code renderSlotHighlight(GuiGraphics, Slot, int, int, float)} + * (added between NeoForge 20.6.5-beta and 21.1.80) that computes the slot's own pixel position and + * highlight color before delegating to a *different* static overload. The clip rect this mixin + * applies is keyed on the highlight's actual pixel position, so it reads that from the Slot + * directly (slot.x/slot.y) rather than from this overload's int/int params, which are mouseX/mouseY + * here, not the highlight position. + */ +@Mixin(AbstractContainerScreen.class) +public abstract class AbstractContainerScreenSlotHighlightMixin extends Screen implements MenuAccess +{ + protected AbstractContainerScreenSlotHighlightMixin(Component title) + { + super(title); + } + + @Invoker("renderSlotHighlight") + abstract void archie$callRenderSlotHighlight(GuiGraphics guiGraphics, Slot slot, int mouseX, int mouseY, float partialTick); + + @Redirect(method = "render", at = @At(value = "INVOKE", target = "Lnet/minecraft/client/gui/screens/inventory/AbstractContainerScreen;renderSlotHighlight(Lnet/minecraft/client/gui/GuiGraphics;Lnet/minecraft/world/inventory/Slot;IIF)V")) + private void archie$clipSlotHighlight(AbstractContainerScreen self, GuiGraphics guiGraphics, Slot slot, int mouseX, int mouseY, float partialTick) + { + IntRect clip = null; + if (this instanceof SlotHighlightClipProvider provider) + { + clip = provider.slotHighlightClipRect(slot.x, slot.y); + if (clip == null) + { + return; + } + } + if (clip != null) + { + guiGraphics.enableScissor(clip.getMinX(), clip.getMinY(), clip.getMaxX(), clip.getMaxY()); + } + try + { + archie$callRenderSlotHighlight(guiGraphics, slot, mouseX, mouseY, partialTick); + } + finally + { + if (clip != null) + { + guiGraphics.disableScissor(); + } + } + } +} diff --git a/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java b/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java index 262da08ba..aad026997 100644 --- a/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java +++ b/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/MinecraftClientMixin.java @@ -1,6 +1,6 @@ package net.kernelpanicsoft.archie.mixin.neoforge.threading; -import net.kernelpanicsoft.archie.gametest.ThreadingImpl; +import net.kernelpanicsoft.archie.gametest.platform.ThreadingImpl; import net.minecraft.client.Minecraft; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; diff --git a/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java b/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java index 15636d026..e83579724 100644 --- a/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java +++ b/core/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/threading/ServerMixin.java @@ -1,7 +1,7 @@ package net.kernelpanicsoft.archie.mixin.neoforge.threading; -import net.kernelpanicsoft.archie.gametest.ThreadingImpl; -import net.kernelpanicsoft.archie.gametest.ADedicatedServerPlatformInternal; +import net.kernelpanicsoft.archie.gametest.platform.ThreadingImpl; +import net.kernelpanicsoft.archie.gametest.platform.ADedicatedServerPlatformInternal; import net.minecraft.server.MinecraftServer; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; diff --git a/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt index a84760b99..87fbfcf1f 100644 --- a/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt +++ b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/ArchieNeoForge.kt @@ -2,7 +2,7 @@ package net.kernelpanicsoft.archie import dev.architectury.event.events.client.ClientTickEvent import dev.nyon.klf.MOD_BUS -import net.kernelpanicsoft.archie.gametest.ThreadingImpl +import net.kernelpanicsoft.archie.gametest.platform.ThreadingImpl import net.neoforged.fml.common.Mod import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent diff --git a/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/platform/ADataGeneratorPlatform.neoforge.kt similarity index 85% rename from core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/platform/ADataGeneratorPlatform.neoforge.kt index a447ec655..e0a758177 100644 --- a/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatform.neoforge.kt +++ b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/platform/ADataGeneratorPlatform.neoforge.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.data +package net.kernelpanicsoft.archie.data.platform /** NeoForge implementation of [ADataGeneratorPlatform]. */ diff --git a/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatform.kt similarity index 98% rename from core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatform.kt index 8d3defcc2..c8d53a149 100644 --- a/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatform.kt +++ b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatform.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.gametest +package net.kernelpanicsoft.archie.gametest.platform import net.minecraft.Util import net.minecraft.server.Main diff --git a/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatformInternal.kt similarity index 97% rename from core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatformInternal.kt index 0f5163f50..56f3a5699 100644 --- a/core/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ADedicatedServerPlatformInternal.kt +++ b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/ADedicatedServerPlatformInternal.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.gametest +package net.kernelpanicsoft.archie.gametest.platform import net.minecraft.server.MinecraftServer import net.minecraft.server.dedicated.DedicatedServer diff --git a/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatform.kt similarity index 96% rename from core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatform.kt index 2716dbe42..f80ef4905 100644 --- a/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatform.kt +++ b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatform.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.gametest +package net.kernelpanicsoft.archie.gametest.platform import dev.architectury.platform.Mod import dev.architectury.platform.Platform diff --git a/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatformInternal.kt similarity index 90% rename from core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt rename to core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatformInternal.kt index ae4cb5b02..42ef35972 100644 --- a/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestPlatformInternal.kt +++ b/core/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/platform/AGameTestPlatformInternal.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.gametest +package net.kernelpanicsoft.archie.gametest.platform import dev.architectury.platform.Mod diff --git a/core/neoforge/src/main/resources/archie.mixins.json b/core/neoforge/src/main/resources/archie.mixins.json index 03445fbc6..f45b199f2 100644 --- a/core/neoforge/src/main/resources/archie.mixins.json +++ b/core/neoforge/src/main/resources/archie.mixins.json @@ -4,7 +4,9 @@ "compatibilityLevel": "JAVA_17", "minVersion": "0.8", "client": [ - "threading.MinecraftClientMixin" + "threading.MinecraftClientMixin", + "client.gui.AbstractContainerScreenItemRenderMixin", + "client.gui.AbstractContainerScreenSlotHighlightMixin" ], "mixins": [ "threading.ServerMixin" diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt index 1a91f6c7d..f0f5b1fe6 100644 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt +++ b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/ADatagenEventObject.kt @@ -1,8 +1,8 @@ package net.kernelpanicsoft.archie.data -import net.kernelpanicsoft.archie.events.ADatagenEvents -import net.kernelpanicsoft.archie.events.ADatagenEvents.GatherDataHandler -import net.kernelpanicsoft.archie.events.AEventObject +import net.kernelpanicsoft.archie.events.datagen.ADatagenEvents +import net.kernelpanicsoft.archie.events.datagen.ADatagenEvents.GatherDataHandler +import net.kernelpanicsoft.archie.events.base.AEventObject import dev.architectury.event.Event import dev.architectury.platform.Mod diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/AConditionBuilder.kt similarity index 77% rename from datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/AConditionBuilder.kt index f16f848c2..23af0b34e 100644 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/AConditionBuilder.kt +++ b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/AConditionBuilder.kt @@ -1,5 +1,16 @@ -package net.kernelpanicsoft.archie.data.common.conditions +package net.kernelpanicsoft.archie.data.common.conditions.gen +import net.kernelpanicsoft.archie.data.common.conditions.AAndCondition +import net.kernelpanicsoft.archie.data.common.conditions.AEqualsCondition +import net.kernelpanicsoft.archie.data.common.conditions.AFalseCondition +import net.kernelpanicsoft.archie.data.common.conditions.AModLoadedCondition +import net.kernelpanicsoft.archie.data.common.conditions.ANotCondition +import net.kernelpanicsoft.archie.data.common.conditions.AOrCondition +import net.kernelpanicsoft.archie.data.common.conditions.APlatformCondition +import net.kernelpanicsoft.archie.data.common.conditions.ARegistryCondition +import net.kernelpanicsoft.archie.data.common.conditions.ATrueCondition +import net.kernelpanicsoft.archie.data.common.conditions.AXorCondition +import net.kernelpanicsoft.archie.data.common.conditions.IACondition import net.minecraft.core.Registry import net.minecraft.resources.ResourceKey import net.minecraft.resources.ResourceLocation diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.common.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/ADatagenConditionsPlatform.common.kt similarity index 67% rename from datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.common.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/ADatagenConditionsPlatform.common.kt index a5ae75998..faeebcec3 100644 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.common.kt +++ b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/ADatagenConditionsPlatform.common.kt @@ -1,5 +1,6 @@ -package net.kernelpanicsoft.archie.data.common.conditions +package net.kernelpanicsoft.archie.data.common.conditions.gen +import net.kernelpanicsoft.archie.data.common.conditions.IACondition import net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider import net.minecraft.core.HolderLookup import net.minecraft.data.recipes.RecipeOutput @@ -7,8 +8,9 @@ import net.minecraft.data.recipes.RecipeProvider import java.util.concurrent.CompletableFuture /** - * Datagen-only half of [AConditionsPlatform] - attaching conditions to a *generated* recipe. - * Split out from [AConditionsPlatform] itself since neither member has any runtime presence. + * Datagen-only half of [net.kernelpanicsoft.archie.data.common.conditions.AConditionsPlatform] - + * attaching conditions to a *generated* recipe. Split out from that class itself since neither + * member has any runtime presence. */ expect object ADatagenConditionsPlatform { diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/Extensions.kt similarity index 85% rename from datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/Extensions.kt index 62c67d288..0f5e4b668 100644 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/Extensions.kt +++ b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/Extensions.kt @@ -1,5 +1,6 @@ -package net.kernelpanicsoft.archie.data.common.conditions +package net.kernelpanicsoft.archie.data.common.conditions.gen +import net.kernelpanicsoft.archie.data.common.conditions.IACondition import net.minecraft.data.recipes.RecipeOutput /** Attaches [condition] to the next recipe written to this [RecipeOutput]. */ diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt index e1eff9942..57a522fe4 100644 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt +++ b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/ARecipeProvider.kt @@ -2,7 +2,7 @@ package net.kernelpanicsoft.archie.data.common.crafting import net.kernelpanicsoft.archie.Archie import net.kernelpanicsoft.archie.data.IADataProvider -import net.kernelpanicsoft.archie.data.common.conditions.ADatagenConditionsPlatform +import net.kernelpanicsoft.archie.data.common.conditions.gen.ADatagenConditionsPlatform import net.kernelpanicsoft.archie.data.common.crafting.recipies.ArchieCookingRecipeBuilder import net.kernelpanicsoft.archie.data.common.crafting.recipies.ArchieShapedRecipeBuilder import net.kernelpanicsoft.archie.data.common.crafting.recipies.ArchieShapelessRecipeBuilder diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt index f9f4a4040..6303386ca 100644 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt +++ b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/common/crafting/recipies/IARecipeBuilder.kt @@ -1,8 +1,8 @@ package net.kernelpanicsoft.archie.data.common.crafting.recipies -import net.kernelpanicsoft.archie.data.common.conditions.AConditionBuilder import net.kernelpanicsoft.archie.data.common.conditions.IACondition -import net.kernelpanicsoft.archie.data.common.conditions.withCondition +import net.kernelpanicsoft.archie.data.common.conditions.gen.AConditionBuilder +import net.kernelpanicsoft.archie.data.common.conditions.gen.withCondition import net.minecraft.data.recipes.RecipeBuilder import net.minecraft.data.recipes.RecipeOutput import net.minecraft.resources.ResourceLocation diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt index 91a3573ce..4d38cef3b 100644 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt +++ b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/ArchieDatagen.kt @@ -3,9 +3,9 @@ package net.kernelpanicsoft.archie.data.internal import net.kernelpanicsoft.archie.Archie import net.kernelpanicsoft.archie.data.ADataGenerator import net.kernelpanicsoft.archie.data.ADatagenEventObject -import net.kernelpanicsoft.archie.data.common.conditions.withCondition +import net.kernelpanicsoft.archie.data.common.conditions.gen.withCondition import net.kernelpanicsoft.archie.data.common.crafting.ingredients.AComponentsIngredient -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags +import net.kernelpanicsoft.archie.data.common.tags.platform.ACommonTags import net.kernelpanicsoft.archie.data.internal.common.tags.* import net.minecraft.core.component.DataComponents import net.minecraft.data.recipes.RecipeCategory diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt index 52e0f7ee2..e0dea024a 100644 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt +++ b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBiomeTagsProvider.kt @@ -2,7 +2,7 @@ package net.kernelpanicsoft.archie.data.internal.common.tags import net.kernelpanicsoft.archie.Archie import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags +import net.kernelpanicsoft.archie.data.common.tags.platform.ACommonTags import net.minecraft.core.HolderLookup import net.minecraft.data.PackOutput import net.minecraft.tags.BiomeTags diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt index 53bf4cd82..170c96dbe 100644 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt +++ b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalBlockTagsProvider.kt @@ -2,7 +2,7 @@ package net.kernelpanicsoft.archie.data.internal.common.tags import net.kernelpanicsoft.archie.Archie import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags +import net.kernelpanicsoft.archie.data.common.tags.platform.ACommonTags import dev.architectury.platform.Platform import net.minecraft.core.HolderLookup import net.minecraft.core.registries.BuiltInRegistries diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt index 8b492d8bd..1d0b14d73 100644 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt +++ b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalEntityTypeTagsProvider.kt @@ -2,7 +2,7 @@ package net.kernelpanicsoft.archie.data.internal.common.tags import net.kernelpanicsoft.archie.Archie import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags +import net.kernelpanicsoft.archie.data.common.tags.platform.ACommonTags import net.minecraft.core.HolderLookup import net.minecraft.data.PackOutput import net.minecraft.world.entity.EntityType diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt index 7dd19cfef..a0fd716d0 100644 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt +++ b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalFluidTagsProvider.kt @@ -2,7 +2,7 @@ package net.kernelpanicsoft.archie.data.internal.common.tags import net.kernelpanicsoft.archie.Archie import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags +import net.kernelpanicsoft.archie.data.common.tags.platform.ACommonTags import net.minecraft.core.HolderLookup import net.minecraft.data.PackOutput import net.minecraft.world.level.material.Fluids diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt index 00ed7dfb7..2594d64be 100644 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt +++ b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/common/tags/AInternalItemTagsProvider.kt @@ -2,7 +2,7 @@ package net.kernelpanicsoft.archie.data.internal.common.tags import net.kernelpanicsoft.archie.Archie import net.kernelpanicsoft.archie.data.common.tags.ATagsProvider -import net.kernelpanicsoft.archie.data.common.tags.ACommonTags +import net.kernelpanicsoft.archie.data.common.tags.platform.ACommonTags import dev.architectury.platform.Platform import net.minecraft.core.HolderLookup import net.minecraft.core.registries.BuiltInRegistries diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ADatagenEvents.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/datagen/ADatagenEvents.kt similarity index 92% rename from datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ADatagenEvents.kt rename to datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/datagen/ADatagenEvents.kt index 3b822a2c2..faff19876 100644 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/ADatagenEvents.kt +++ b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/events/datagen/ADatagenEvents.kt @@ -1,10 +1,12 @@ -package net.kernelpanicsoft.archie.events +package net.kernelpanicsoft.archie.events.datagen import dev.architectury.event.Event import dev.architectury.event.EventFactory import dev.architectury.event.EventResult import dev.architectury.platform.Mod import net.kernelpanicsoft.archie.data.ADataGenerator +import net.kernelpanicsoft.archie.events.base.Handler +import net.kernelpanicsoft.archie.events.base.HandlerConstructor /** * `archie-datagen`'s central, mod-scoped event registry, built on top of Architectury's event diff --git a/datagen/fabric/build.gradle.kts b/datagen/fabric/build.gradle.kts index f7ab6d928..53f395cf6 100644 --- a/datagen/fabric/build.gradle.kts +++ b/datagen/fabric/build.gradle.kts @@ -1,3 +1,5 @@ +import net.kernelpanicsoft.archie.plugin.bundleRuntimeLibrary + plugins { alias(libs.plugins.archie) } @@ -67,6 +69,18 @@ dependencies { compileOnly(libs.kotlinx.serialization) // See the matching comment in gametest/fabric/build.gradle.kts. modLocalRuntime(libs.clothConfig.fabric) + // Archie's own mod init (ArchieFabric -> Archie.init -> ConfigContainer) touches these at + // class-load time regardless of what this product actually needs - archie-core-fabric's own + // bundleRuntimeLibrary calls only cover ITS OWN dev-mode run, which doesn't carry over to a + // project consuming it as a dependency, so this needs its own copies (matching + // core/fabric/build.gradle.kts's set exactly). + bundleRuntimeLibrary(libs.kotlinx.serialization) + bundleRuntimeLibrary(libs.kotlinx.serialization.json) + bundleRuntimeLibrary(libs.kotlinx.serialization.nbt) + bundleRuntimeLibrary(libs.kotlinx.serialization.toml) + bundleRuntimeLibrary(libs.kotlinx.serialization.json5) + bundleRuntimeLibrary(libs.kotlinx.serialization.cbor) + bundleRuntimeLibrary(compose.runtime) implementation(libs.junit.jupiter.api) testImplementation(libs.junit.jupiter.api) @@ -74,6 +88,8 @@ dependencies { "common"(project(":archie-datagen-common", "namedElements")) { isTransitive = false } api(project(":archie-core-fabric", "namedElements")) + // See the matching comment in gametest/fabric/build.gradle.kts. + runtimeOnly(project(":archie-core-common", "namedElements")) { isTransitive = false } } modResources { diff --git a/datagen/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java b/datagen/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java index 033370dc0..ed88832ac 100644 --- a/datagen/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java +++ b/datagen/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricDataGenHelperMixin.java @@ -3,7 +3,7 @@ import com.llamalad7.mixinextras.sugar.Local; import com.llamalad7.mixinextras.sugar.ref.LocalRef; import net.kernelpanicsoft.archie.Archie; -import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform; +import net.kernelpanicsoft.archie.data.platform.ADataGeneratorPlatform; import net.fabricmc.fabric.api.datagen.v1.DataGeneratorEntrypoint; import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper; import net.fabricmc.loader.api.entrypoint.EntrypointContainer; diff --git a/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt b/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt index 4ee285d5a..19ec5c231 100644 --- a/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt +++ b/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt @@ -6,8 +6,8 @@ import net.fabricmc.fabric.api.datagen.v1.FabricDataGenerator import net.fabricmc.loader.api.FabricLoader import net.fabricmc.loader.api.ModContainer import net.fabricmc.loader.api.entrypoint.EntrypointContainer -import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform.isDataGen -import net.kernelpanicsoft.archie.events.ADatagenEvents +import net.kernelpanicsoft.archie.data.platform.ADataGeneratorPlatform.isDataGen +import net.kernelpanicsoft.archie.events.datagen.ADatagenEvents import net.minecraft.core.RegistrySetBuilder /** diff --git a/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.fabric.kt b/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/ADatagenConditionsPlatform.fabric.kt similarity index 94% rename from datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.fabric.kt rename to datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/ADatagenConditionsPlatform.fabric.kt index 9669b94ac..f8abd4272 100644 --- a/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.fabric.kt +++ b/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/ADatagenConditionsPlatform.fabric.kt @@ -1,4 +1,4 @@ -package net.kernelpanicsoft.archie.data.common.conditions +package net.kernelpanicsoft.archie.data.common.conditions.gen import kotlinx.serialization.json.Json import net.fabricmc.fabric.api.datagen.v1.FabricDataOutput @@ -6,6 +6,7 @@ import net.fabricmc.fabric.api.datagen.v1.provider.FabricRecipeProvider import net.fabricmc.fabric.api.resource.conditions.v1.ResourceCondition import net.fabricmc.fabric.impl.datagen.FabricDataGenHelper import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.common.conditions.IACondition import net.kernelpanicsoft.archie.data.common.conditions.fabric import net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider import net.kernelpanicsoft.archie.serialization.kSerializer diff --git a/datagen/neoforge/build.gradle.kts b/datagen/neoforge/build.gradle.kts index 83f616e2f..eca2fc484 100644 --- a/datagen/neoforge/build.gradle.kts +++ b/datagen/neoforge/build.gradle.kts @@ -1,3 +1,6 @@ +import net.kernelpanicsoft.archie.plugin.bundleRuntimeLibrary +import org.gradle.api.tasks.bundling.AbstractArchiveTask + plugins { alias(libs.plugins.archie) } @@ -11,6 +14,11 @@ actualizer { actualizes(project(":archie-datagen-common")) } +// See the dependencies{} block below - project(...) inside dependencies{} resolves to +// DependencyHandler.project(...) (a ProjectDependency), not the real Project, so it can't be +// chased for .tasks there; look it up here instead, where project(...) is still the real Project. +val coreNeoForgeRemapJar = project(":archie-core-neoforge").tasks.named("remapJar", AbstractArchiveTask::class).flatMap { it.archiveFile } + configurations { create("common") configureEach { @@ -64,13 +72,37 @@ dependencies { compileOnly(libs.kotlinx.serialization) // See the matching comment in gametest/fabric/build.gradle.kts. modRuntimeOnly(libs.clothConfig.neoforge) + // archie-core-neoforge's own namedElements/remapJar (below) is compileOnly and only carries + // its own classes, not its transitive mod dependencies - declare architectury-api directly + // (matching core-neoforge's own dependency) so it's genuinely on this project's own runtime + // classpath too, needed by mixins that reference dev.architectury.platform.Mod. + modApi(libs.architectury.neoforge) + // Archie's own mod init (ArchieNeoForge -> Archie.init -> ConfigContainer) touches these at + // class-load time regardless of what this product actually needs - archie-core-neoforge's own + // bundleRuntimeLibrary calls only wire up ITS OWN dev-mode "userdev mods and services" locator, + // which doesn't carry over to a project consuming it as a dependency, so this needs its own + // copies (matching core/neoforge/build.gradle.kts's set exactly). + bundleRuntimeLibrary(libs.kotlinx.serialization) + bundleRuntimeLibrary(libs.kotlinx.serialization.json) + bundleRuntimeLibrary(libs.kotlinx.serialization.nbt) + bundleRuntimeLibrary(libs.kotlinx.serialization.toml) + bundleRuntimeLibrary(libs.kotlinx.serialization.json5) + bundleRuntimeLibrary(libs.kotlinx.serialization.cbor) + bundleRuntimeLibrary(compose.runtime) implementation(libs.junit.jupiter.api) testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) "common"(project(":archie-datagen-common", "namedElements")) { isTransitive = false } - api(project(":archie-core-neoforge", "namedElements")) + // Compile-only: archie-core-common's/archie-core-neoforge's real classes to compile against. + // NOT added to the runtime classpath - see the matching comment in + // gametest/neoforge/build.gradle.kts for why (archie-core-neoforge's own dev jars are + // production-shaped but not production-complete, and duplicate registration under two + // different FML-recognized mods breaks NeoForge's per-mod module layer). + compileOnly(project(":archie-core-common", "namedElements")) { isTransitive = false } + compileOnly(project(":archie-core-neoforge", "namedElements")) + modRuntimeOnly(files(coreNeoForgeRemapJar)) } modResources { diff --git a/datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java b/datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/datagen/DatagenModLoaderMixin.java similarity index 91% rename from datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java rename to datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/datagen/DatagenModLoaderMixin.java index 0b6ecc37e..e738102f2 100644 --- a/datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/DatagenModLoaderMixin.java +++ b/datagen/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/datagen/DatagenModLoaderMixin.java @@ -1,7 +1,7 @@ -package net.kernelpanicsoft.archie.mixin.neoforge; +package net.kernelpanicsoft.archie.mixin.neoforge.datagen; import net.kernelpanicsoft.archie.Archie; -import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform; +import net.kernelpanicsoft.archie.data.platform.ADataGeneratorPlatform; import net.kernelpanicsoft.archie.data.ADataGeneratorPlatformInternal; import net.neoforged.fml.ModList; import net.neoforged.neoforge.data.event.GatherDataEvent; diff --git a/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt b/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt index 006872eac..1eb89550c 100644 --- a/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt +++ b/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ADataGeneratorPlatformInternal.kt @@ -1,6 +1,8 @@ package net.kernelpanicsoft.archie.data -import net.kernelpanicsoft.archie.events.ADatagenEvents +import net.kernelpanicsoft.archie.data.platform.ADataGeneratorPlatform + +import net.kernelpanicsoft.archie.events.datagen.ADatagenEvents import net.neoforged.fml.ModList import net.neoforged.neoforge.data.event.GatherDataEvent diff --git a/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.neoforge.kt b/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/ADatagenConditionsPlatform.neoforge.kt similarity index 93% rename from datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.neoforge.kt rename to datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/ADatagenConditionsPlatform.neoforge.kt index 8e34ba7ba..cc7550d9f 100644 --- a/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/ADatagenConditionsPlatform.neoforge.kt +++ b/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/common/conditions/gen/ADatagenConditionsPlatform.neoforge.kt @@ -1,5 +1,6 @@ -package net.kernelpanicsoft.archie.data.common.conditions +package net.kernelpanicsoft.archie.data.common.conditions.gen +import net.kernelpanicsoft.archie.data.common.conditions.IACondition import net.kernelpanicsoft.archie.data.common.conditions.neoforge import net.kernelpanicsoft.archie.data.common.crafting.ARecipeProvider import net.minecraft.advancements.Advancement diff --git a/datagen/neoforge/src/main/resources/archie_datagen.mixins.json b/datagen/neoforge/src/main/resources/archie_datagen.mixins.json index 65dd2eae8..d65b1325c 100644 --- a/datagen/neoforge/src/main/resources/archie_datagen.mixins.json +++ b/datagen/neoforge/src/main/resources/archie_datagen.mixins.json @@ -1,6 +1,6 @@ { "required": true, - "package": "net.kernelpanicsoft.archie.mixin.neoforge", + "package": "net.kernelpanicsoft.archie.mixin.neoforge.datagen", "compatibilityLevel": "JAVA_17", "minVersion": "0.8", "mixins": [ diff --git a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AGametestEvents.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/gametest/AGametestEvents.kt similarity index 93% rename from gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AGametestEvents.kt rename to gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/gametest/AGametestEvents.kt index d27594dc9..acb8e6b04 100644 --- a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/AGametestEvents.kt +++ b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/events/gametest/AGametestEvents.kt @@ -1,11 +1,13 @@ -package net.kernelpanicsoft.archie.events +package net.kernelpanicsoft.archie.events.gametest import dev.architectury.event.Event import dev.architectury.event.EventFactory import dev.architectury.event.EventResult import dev.architectury.platform.Mod -import net.kernelpanicsoft.archie.gametest.AGameTestPlatform -import net.kernelpanicsoft.archie.gametest.AGameTestSide +import net.kernelpanicsoft.archie.events.base.Handler +import net.kernelpanicsoft.archie.events.base.HandlerConstructor +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform +import net.kernelpanicsoft.archie.gametest.platform.AGameTestSide /** * `archie-gametest`'s central, mod-scoped event registry, built on top of Architectury's event diff --git a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt index d3f868c88..d82c1c0ac 100644 --- a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt +++ b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AClientGameTestHarness.kt @@ -6,6 +6,11 @@ import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestCoroutineScheduler import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.gametest.platform.ADedicatedServerPlatform +import net.kernelpanicsoft.archie.gametest.platform.AGameTestModFilter +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform +import net.kernelpanicsoft.archie.gametest.platform.AGameTestSide +import net.kernelpanicsoft.archie.gametest.platform.ThreadingImpl import net.kernelpanicsoft.archie.gui.ComposeIdleAware import net.kernelpanicsoft.archie.gui.ComposeTestClockOverride import net.kernelpanicsoft.archie.gui.LayerManagerProvider diff --git a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt index e53281870..0ffc8970f 100644 --- a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt +++ b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestEventObject.kt @@ -1,7 +1,7 @@ package net.kernelpanicsoft.archie.gametest -import net.kernelpanicsoft.archie.events.AGametestEvents -import net.kernelpanicsoft.archie.events.AEventObject +import net.kernelpanicsoft.archie.events.gametest.AGametestEvents +import net.kernelpanicsoft.archie.events.base.AEventObject import dev.architectury.event.Event import dev.architectury.platform.Mod diff --git a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt index 6745acc6a..d6e681345 100644 --- a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt +++ b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt @@ -5,8 +5,8 @@ import net.minecraft.gametest.framework.GameTestHelper /** * A trivially-succeeding placeholder, registered by [AGameTestPlatformInternal] on each loader - * when a mod's [AGameTestModFilter]-selected suite has no real test functions for the current - * [AGameTestSide] - e.g. a mod with only client-side coverage (like Archie-Test, whose own suite + * when a mod's [net.kernelpanicsoft.archie.gametest.platform.AGameTestModFilter]-selected suite has no real test functions for the current + * [net.kernelpanicsoft.archie.gametest.platform.AGameTestSide] - e.g. a mod with only client-side coverage (like Archie-Test, whose own suite * registers just [net.kernelpanicsoft.archie.test.gametest.TestScreenGameTest]) running its * server invocation. Vanilla's `GameTestServer` refuses to boot with zero registered test * functions at all (`IllegalArgumentException: No test functions were given!`); this keeps that diff --git a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt index 9fd978f81..3d61fc9e4 100644 --- a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt +++ b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt @@ -1,7 +1,7 @@ package net.kernelpanicsoft.archie.gametest.internal import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.events.AGametestEvents +import net.kernelpanicsoft.archie.events.gametest.AGametestEvents import net.kernelpanicsoft.archie.gametest.AGameTestEventObject import net.kernelpanicsoft.archie.gametest.internal.tests.ArchieItemHandlerTests import net.kernelpanicsoft.archie.gametest.internal.tests.BlockEntityNBTHolderTests diff --git a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt index 48cb0f36c..125fe0e01 100644 --- a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt +++ b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestGradleInvocation.kt @@ -6,7 +6,7 @@ enum class Loader { NEOFORGE, } -/** The GameTest side (matches [net.kernelpanicsoft.archie.gametest.AGameTestSide]) to launch. */ +/** The GameTest side (matches [net.kernelpanicsoft.archie.gametest.platform.AGameTestSide]) to launch. */ enum class Side { SERVER, CLIENT, diff --git a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt index 4c38ba4e1..9307b6c8f 100644 --- a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt +++ b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/junit/GameTestRunner.kt @@ -1,6 +1,6 @@ package net.kernelpanicsoft.archie.gametest.junit -import net.kernelpanicsoft.archie.events.AGametestEvents +import net.kernelpanicsoft.archie.events.gametest.AGametestEvents import net.kernelpanicsoft.archie.gametest.ClientGameTest import net.minecraft.gametest.framework.GameTest import org.junit.jupiter.api.Assumptions.assumeTrue diff --git a/gametest/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt b/gametest/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt index 98042c291..2adb2e2b1 100644 --- a/gametest/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt +++ b/gametest/common/src/test/kotlin/net/kernelpanicsoft/archie/testing/GameTests.kt @@ -1,6 +1,6 @@ package net.kernelpanicsoft.archie.testing -import net.kernelpanicsoft.archie.events.AGametestEvents +import net.kernelpanicsoft.archie.events.gametest.AGametestEvents import net.kernelpanicsoft.archie.gametest.internal.archieGameTests import net.kernelpanicsoft.archie.gametest.junit.GameTestRunner import org.junit.jupiter.api.DynamicContainer diff --git a/gametest/fabric/build.gradle.kts b/gametest/fabric/build.gradle.kts index 2fd16bead..9f5bd89a3 100644 --- a/gametest/fabric/build.gradle.kts +++ b/gametest/fabric/build.gradle.kts @@ -63,10 +63,6 @@ dependencies { modApi(libs.fabric.api) modImplementation(libs.kotlin.fabric) compileOnly(libs.kotlinx.serialization) - // archie-core-fabric's config/serialization code touches cloth-config's Color class - // unconditionally at class-init time even though the dependency itself is compileOnly in the - // shipped jar - dev-only runs (this module's own runGametest/runGametestClient, which load - // archie-core as a mod dependency) need it on the runtime classpath or that class-init crashes. modLocalRuntime(libs.clothConfig.fabric) implementation(libs.junit.jupiter.api) @@ -75,6 +71,7 @@ dependencies { "common"(project(":archie-gametest-common", "namedElements")) { isTransitive = false } api(project(":archie-core-fabric", "namedElements")) + runtimeOnly(project(":archie-core-common", "namedElements")) { isTransitive = false } } modResources { diff --git a/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java b/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java index f2b750a74..3a9fe2cba 100644 --- a/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java +++ b/gametest/fabric/src/main/java/net/kernelpanicsoft/archie/mixin/fabric/FabricGameTestModInitializerMixin.java @@ -2,7 +2,7 @@ import net.fabricmc.fabric.impl.gametest.FabricGameTestModInitializer; import net.kernelpanicsoft.archie.Archie; -import net.kernelpanicsoft.archie.gametest.AGameTestPlatform; +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform; import net.kernelpanicsoft.archie.gametest.VerboseTestReporter; import net.minecraft.gametest.framework.GlobalTestReporter; import org.spongepowered.asm.mixin.Mixin; diff --git a/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt b/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt index 4fbbb1950..1cc365883 100644 --- a/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt +++ b/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt @@ -2,7 +2,10 @@ package net.kernelpanicsoft.archie.gametest import dev.architectury.platform.Mod import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.events.AGametestEvents +import net.kernelpanicsoft.archie.events.gametest.AGametestEvents +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform +import net.kernelpanicsoft.archie.gametest.platform.AGameTestSide +import net.kernelpanicsoft.archie.gametest.platform.ThreadingImpl import net.minecraft.client.Minecraft import java.util.concurrent.atomic.AtomicBoolean diff --git a/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt b/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt index 2066faffb..c8794f663 100644 --- a/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt +++ b/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt @@ -1,8 +1,10 @@ package net.kernelpanicsoft.archie.gametest import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.events.AGametestEvents -import net.kernelpanicsoft.archie.gametest.AGameTestPlatform.isGameTest +import net.kernelpanicsoft.archie.events.gametest.AGametestEvents +import net.kernelpanicsoft.archie.gametest.platform.AGameTestModFilter +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform.isGameTest import net.kernelpanicsoft.archie.mixin.fabric.FabricGameTestModInitializerMixin import net.minecraft.gametest.framework.GameTestRegistry import net.minecraft.gametest.framework.GlobalTestReporter @@ -12,8 +14,9 @@ import net.minecraft.gametest.framework.GlobalTestReporter * `FabricGameTestHelper.runHeadlessServer` - the flush step that drives Fabric's own GameTest * registry. Named distinctly from `archie-core`'s own (internal, `testClasses`-only) * `AGameTestPlatformInternal` to avoid a same-package class name collision on the runtime - * classpath - this reaches `archie-core`'s test class map via [AGameTestPlatform.testClasses] - * instead of touching that internal object directly. + * classpath - this reaches `archie-core`'s test class map via + * [net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform.testClasses] instead of + * touching that internal object directly. */ object AGameTestRegistrationBridge { diff --git a/gametest/neoforge/build.gradle.kts b/gametest/neoforge/build.gradle.kts index 7219219d9..f3b0c4f80 100644 --- a/gametest/neoforge/build.gradle.kts +++ b/gametest/neoforge/build.gradle.kts @@ -1,3 +1,5 @@ +import org.gradle.api.tasks.bundling.AbstractArchiveTask + plugins { alias(libs.plugins.archie) } @@ -11,6 +13,11 @@ actualizer { actualizes(project(":archie-gametest-common")) } +// See the dependencies{} block below - project(...) inside dependencies{} resolves to +// DependencyHandler.project(...) (a ProjectDependency), not the real Project, so it can't be +// chased for .tasks there; look it up here instead, where project(...) is still the real Project. +val coreNeoForgeRemapJar = project(":archie-core-neoforge").tasks.named("remapJar", AbstractArchiveTask::class).flatMap { it.archiveFile } + configurations { create("common") configureEach { @@ -72,13 +79,36 @@ dependencies { compileOnly(libs.kotlinx.serialization) // See the matching comment in gametest/fabric/build.gradle.kts. modRuntimeOnly(libs.clothConfig.neoforge) + // archie-core-neoforge's own namedElements/remapJar (below) is compileOnly and only carries + // its own classes, not its transitive mod dependencies - declare architectury-api directly + // (matching core-neoforge's own dependency) so it's genuinely on this project's own runtime + // classpath too, needed by mixins that reference dev.architectury.platform.Mod. + modApi(libs.architectury.neoforge) implementation(libs.junit.jupiter.api) testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) "common"(project(":archie-gametest-common", "namedElements")) { isTransitive = false } - api(project(":archie-core-neoforge", "namedElements")) + // Compile-only: archie-core-common's real classes for archie-gametest-common's source to + // compile against. NOT added to the runtime classpath (unlike the "common" config above) - + // its classes are also physically present in coreNeoForgeRemapJar below (merged in via + // archie-core-neoforge's shadowJar), and having both on the runtime classpath means two + // different FML-registered mods ("archie_gametest" folding in these raw classes, "archie" from + // the real jar) would each claim the same packages, which NeoForge's module layer rejects. + compileOnly(project(":archie-core-common", "namedElements")) { isTransitive = false } + // Compile-only for the same reason as above: archie-core-neoforge's own "namedElements" dev jar + // is production-*shaped* but not production-*complete* (it lacks archie-core-common's classes, + // only merged in via archie-core-neoforge's shadowJar, which dev mode skips), so mixin prepare + // fails "not found" at runtime despite compiling fine against it. Depend on its real remapJar + // output for the actual dev-mode run classpath instead (files(), not project(...), so this + // stays lazy/task-output-driven and doesn't hit the "mod* on a project reference reads the jar + // during configuration" issue namedElements avoids elsewhere in this build) - same jar a real + // downstream consumer would use, so its mixin refmap and merged classes are both genuinely + // complete. Kept off the runtime classpath alongside namedElements above, to avoid archie-core- + // neoforge's own classes appearing twice (dev jar + remapJar) at runtime. + compileOnly(project(":archie-core-neoforge", "namedElements")) + modRuntimeOnly(files(coreNeoForgeRemapJar)) } modResources { diff --git a/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java b/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/gametest/GameTestHooksMixin.java similarity index 94% rename from gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java rename to gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/gametest/GameTestHooksMixin.java index 00023e14a..f8a3cbb5c 100644 --- a/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/GameTestHooksMixin.java +++ b/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/gametest/GameTestHooksMixin.java @@ -1,7 +1,7 @@ -package net.kernelpanicsoft.archie.mixin.neoforge; +package net.kernelpanicsoft.archie.mixin.neoforge.gametest; import net.kernelpanicsoft.archie.Archie; -import net.kernelpanicsoft.archie.gametest.AGameTestPlatform; +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform; import net.kernelpanicsoft.archie.gametest.AGameTestRegistrationBridge; import net.kernelpanicsoft.archie.gametest.VerboseTestReporter; import net.minecraft.gametest.framework.GameTest; diff --git a/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt b/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt index 4fbbb1950..1cc365883 100644 --- a/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt +++ b/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestClientHarnessInternal.kt @@ -2,7 +2,10 @@ package net.kernelpanicsoft.archie.gametest import dev.architectury.platform.Mod import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.events.AGametestEvents +import net.kernelpanicsoft.archie.events.gametest.AGametestEvents +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform +import net.kernelpanicsoft.archie.gametest.platform.AGameTestSide +import net.kernelpanicsoft.archie.gametest.platform.ThreadingImpl import net.minecraft.client.Minecraft import java.util.concurrent.atomic.AtomicBoolean diff --git a/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt b/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt index 4bde72c69..e9f47e124 100644 --- a/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt +++ b/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt @@ -1,7 +1,9 @@ package net.kernelpanicsoft.archie.gametest import dev.architectury.platform.Mod -import net.kernelpanicsoft.archie.events.AGametestEvents +import net.kernelpanicsoft.archie.events.gametest.AGametestEvents +import net.kernelpanicsoft.archie.gametest.platform.AGameTestModFilter +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform import net.neoforged.fml.ModList import net.neoforged.neoforge.event.RegisterGameTestsEvent diff --git a/gametest/neoforge/src/main/resources/archie_gametest.mixins.json b/gametest/neoforge/src/main/resources/archie_gametest.mixins.json index ae0f33176..949eb01cb 100644 --- a/gametest/neoforge/src/main/resources/archie_gametest.mixins.json +++ b/gametest/neoforge/src/main/resources/archie_gametest.mixins.json @@ -7,7 +7,7 @@ "lifecycle.MinecraftClientMixin" ], "mixins": [ - "GameTestHooksMixin" + "gametest.GameTestHooksMixin" ], "injectors": { "defaultRequire": 1 diff --git a/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt index 5e2150ce8..4f6a1c3ba 100644 --- a/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt +++ b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt @@ -4,10 +4,10 @@ import com.mojang.logging.LogUtils import dev.architectury.platform.Mod import dev.architectury.platform.Platform import net.kernelpanicsoft.archie.Archie -import net.kernelpanicsoft.archie.data.ADataGeneratorPlatform -import net.kernelpanicsoft.archie.events.ADatagenEvents -import net.kernelpanicsoft.archie.events.AGametestEvents -import net.kernelpanicsoft.archie.gametest.AGameTestPlatform +import net.kernelpanicsoft.archie.data.platform.ADataGeneratorPlatform +import net.kernelpanicsoft.archie.events.datagen.ADatagenEvents +import net.kernelpanicsoft.archie.events.gametest.AGametestEvents +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform import net.kernelpanicsoft.archie.test.gametest.ArchieTestGameTest import net.kernelpanicsoft.archie.test.gametest.DataAttachmentTestFixtures import net.kernelpanicsoft.archie.test.gametest.CapabilityLookupTestFixtures diff --git a/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt index 04744fe0d..a48f45928 100644 --- a/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt +++ b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/gametest/ArchieTestGameTest.kt @@ -1,6 +1,6 @@ package net.kernelpanicsoft.archie.test.gametest -import net.kernelpanicsoft.archie.events.AGametestEvents +import net.kernelpanicsoft.archie.events.gametest.AGametestEvents import net.kernelpanicsoft.archie.gametest.AGameTestEventObject import net.kernelpanicsoft.archie.test.ArchieTest diff --git a/test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt b/test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt index 2b2fa8fec..c11cd59ca 100644 --- a/test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt +++ b/test/common/src/test/kotlin/net/kernelpanicsoft/archie/test/testing/GameTests.kt @@ -1,6 +1,6 @@ package net.kernelpanicsoft.archie.test.testing -import net.kernelpanicsoft.archie.events.AGametestEvents +import net.kernelpanicsoft.archie.events.gametest.AGametestEvents import net.kernelpanicsoft.archie.gametest.junit.GameTestRunner import net.kernelpanicsoft.archie.test.gametest.archieTestGameTests import org.junit.jupiter.api.DynamicContainer diff --git a/test/fabric/build.gradle.kts b/test/fabric/build.gradle.kts index 11ac8f767..fa0fe9dff 100644 --- a/test/fabric/build.gradle.kts +++ b/test/fabric/build.gradle.kts @@ -101,6 +101,8 @@ dependencies { api(project(":archie-core-fabric", "namedElements")) api(project(":archie-datagen-fabric", "namedElements")) api(project(":archie-gametest-fabric", "namedElements")) + // See the matching comment in gametest/fabric/build.gradle.kts. + runtimeOnly(project(":archie-core-common", "namedElements")) { isTransitive = false } } modResources { diff --git a/test/neoforge/build.gradle.kts b/test/neoforge/build.gradle.kts index e8f6213de..33ba4b670 100644 --- a/test/neoforge/build.gradle.kts +++ b/test/neoforge/build.gradle.kts @@ -1,4 +1,5 @@ import net.kernelpanicsoft.archie.plugin.bundleMod +import org.gradle.api.tasks.bundling.AbstractArchiveTask plugins { alias(libs.plugins.shadow) @@ -14,6 +15,16 @@ actualizer { actualizes(project(":archie-test-common")) } +// See the dependencies{} block below - project(...) inside dependencies{} resolves to +// DependencyHandler.project(...) (a ProjectDependency), not the real Project, so it can't be +// chased for .tasks there; look these up here instead, where project(...) is still the real +// Project. Each of these products' own "namedElements" dev jar is production-shaped but not +// production-complete (their shadowJar-merged classes are missing until a real build), so this +// project depends on their real remapJar output for its own runtime classpath instead. +val coreNeoForgeRemapJar = project(":archie-core-neoforge").tasks.named("remapJar", AbstractArchiveTask::class).flatMap { it.archiveFile } +val datagenNeoForgeRemapJar = project(":archie-datagen-neoforge").tasks.named("remapJar", AbstractArchiveTask::class).flatMap { it.archiveFile } +val gametestNeoForgeRemapJar = project(":archie-gametest-neoforge").tasks.named("remapJar", AbstractArchiveTask::class).flatMap { it.archiveFile } + configurations { create("common") create("shadowCommon") @@ -109,9 +120,17 @@ dependencies { "common"(project(":archie-test-common", "namedElements")) { isTransitive = false } "shadowCommon"(project(":archie-test-common", "transformProductionNeoForge")) { isTransitive = false } - api(project(":archie-core-neoforge", "namedElements")) - api(project(":archie-datagen-neoforge", "namedElements")) - api(project(":archie-gametest-neoforge", "namedElements")) + // Compile-only: real classes to compile against, kept off the runtime classpath - see the + // matching comment in gametest/neoforge/build.gradle.kts for why (each of these products' own + // dev jar is production-shaped but not production-complete, and duplicate registration under + // two different FML-recognized mods breaks NeoForge's per-mod module layer). + compileOnly(project(":archie-core-common", "namedElements")) { isTransitive = false } + compileOnly(project(":archie-core-neoforge", "namedElements")) + compileOnly(project(":archie-datagen-neoforge", "namedElements")) + compileOnly(project(":archie-gametest-neoforge", "namedElements")) + modRuntimeOnly(files(coreNeoForgeRemapJar)) + modRuntimeOnly(files(datagenNeoForgeRemapJar)) + modRuntimeOnly(files(gametestNeoForgeRemapJar)) } modResources { From 8626f0ea43f7379613982328489728271c0a9c91 Mon Sep 17 00:00:00 2001 From: KernelPanic Date: Tue, 11 Aug 2026 17:19:47 -0400 Subject: [PATCH 9/9] fix: real dev-jar completeness, native mod entrypoints, and NeoForge GameTest registration bugs archie-core-{fabric,neoforge}'s own `jar` task blanket-excluded all of archie-core-common's classes from the dev jar via `exclude("net/kernelpanicsoft/archie/**")`, instead of letting `duplicatesStrategy = EXCLUDE` handle just the actual/expect duplication it was meant to guard against - throwing away legitimate non-actualized content (plain mixin classes) along with it. Fixed by dropping the exclude and adding the matching `from(...)`/`duplicatesStrategy` merge to core-fabric's own jar task too (previously only core-neoforge had one at all). This makes `project(":x", "namedElements")` dependencies genuinely complete for cross-project consumers again, replacing the `modRuntimeOnly(files(remapJar))` workaround from the previous commit. Replaced the ArchieExtension/ServiceLoader mechanism (archie-core discovering archie-datagen/ archie-gametest's hooks via `META-INF/services`) with real per-loader mod entrypoints (ArchieDatagenFabric/NeoForge, ArchieGameTestFabric/NeoForge) - NeoForge's per-mod JPMS module boundaries make classic ServiceLoader discovery unreliable across mods once those boundaries are actually enforced (which they weren't, until the previous commit's fixes let a NeoForge boot get this far for the first time). AGameTestPlatform/ADataGeneratorPlatform etc. stay in archie-core for now - a further move of gametest-only platform code into archie-gametest is a natural follow-up, not done here. Found and fixed three more genuine, previously-unreached NeoForge GameTest bugs once registration itself started working: - `AGametestEvents += MOD` / `ADatagenEvents += MOD` were only ever called by archie-test's own init, never by Archie's own dogfooded ArchieGameTest/ArchieDatagen - gametest limped along on Fabric only via an `.ifEmpty { listOf(Archie.MOD) }` fallback NeoForge's own registration bridge was missing entirely (crashed instead of falling back); datagen had no such fallback on either loader, so ArchieDatagen's own dogfooded datagen silently never ran. Fixed by having each loader's new entrypoint register Archie's own mod for itself, plus adding the missing fallback to NeoForge's bridge to match Fabric's. - NeoForge's `GameTestRegistry#turnMethodIntoTestFunction` unconditionally wraps its result with `getTemplateNamespace(method) + ":"`, regardless of what the mixin-overridden `prefixGameTestTemplate()` returns - an already-namespaced `@GameTest(template = "archie:...")` string (needed since Fabric has no custom template-namespace resolution of its own and relies on the literal string being complete) came out double-prefixed on NeoForge specifically. Fixed with a new `GameTestRegistryMixin` that corrects just the `structureName` field of vanilla's own (otherwise-correct) result via MixinExtras' `@ModifyReturnValue`, rather than fighting the two upstream helper methods. Also fixed a real "specified mixin was not found"-class of mistake in GameTestHooksMixin - `cir.setReturnValue()` alone does not cancel a Mixin injection. - `archie-test`'s own `CapabilityLookupTestFixtures.init()` (dogfooding `exposeItemStorage`) touches `TileRegistry.TestTile`, a `by register(...)` deferred-registry property, from `ArchieTest.init()` - too early on NeoForge specifically, since a `DeferredRegister`-backed value there doesn't resolve until the registry unfreezes at `RegisterEvent`, well after `FMLConstructModEvent`. Moved the call into each loader's own entrypoint, timed correctly per loader (Fabric: immediately, no staged registry model to race; NeoForge: `RegisterCapabilitiesEvent`, the same event Common Storage Lib's own capability-exposure listens for internally). Also: two smaller mechanical fixes surfaced by this round - `evaluationDependsOn(":archie-core-neoforge")` (and the datagen/gametest equivalents) added to datagen/gametest/test-neoforge's build scripts, since referencing another project's task graph from a top-level `val` raced Gradle's own (possibly parallel) project configuration on a genuinely cold/from-scratch CI checkout ("Failed to setup Minecraft ... Failed to compute checksum") even though it never reproduced locally with a warm cache; and Fabric entrypoints for Kotlin `object`s need the `{"adapter": "kotlin", "value": "..."}` form in fabric.mod.json, not a bare class-name string (Kotlin objects have a private constructor Fabric Loader's default Java adapter can't call). Verified via real `./gradlew check` runs matching CI's exact invocation for both loaders - 14/14 test suites, 0 failures/errors, real JUnit XML output inspected directly (not just green builds). Co-Authored-By: Claude Sonnet 5 --- .github/workflows/check.yaml | 13 +++---- core/common/build.gradle.kts | 8 ---- .../net/kernelpanicsoft/archie/Archie.kt | 13 +------ .../kernelpanicsoft/archie/ArchieExtension.kt | 21 ---------- core/fabric/build.gradle.kts | 5 +++ core/neoforge/build.gradle.kts | 9 +---- datagen/common/build.gradle.kts | 4 -- .../data/internal/DatagenArchieExtension.kt | 12 ------ ...net.kernelpanicsoft.archie.ArchieExtension | 1 - datagen/fabric/build.gradle.kts | 15 ------- .../archie/data/ArchieDatagenFabric.kt | 26 +++++++++++++ .../fabric/src/main/resources/fabric.mod.json | 8 ++++ datagen/neoforge/build.gradle.kts | 31 +-------------- .../archie/data/ArchieDatagenNeoForge.kt | 29 ++++++++++++++ gametest/common/build.gradle.kts | 2 - .../archie/gametest/NoOpGameTest.kt | 3 +- .../gametest/internal/ArchieGameTest.kt | 7 +++- .../internal/GametestArchieExtension.kt | 12 ------ ...net.kernelpanicsoft.archie.ArchieExtension | 1 - gametest/fabric/build.gradle.kts | 1 - .../archie/gametest/ArchieGameTestFabric.kt | 26 +++++++++++++ .../fabric/src/main/resources/fabric.mod.json | 8 ++++ gametest/neoforge/build.gradle.kts | 30 +------------- .../neoforge/gametest/GameTestHooksMixin.java | 27 +++++++------ .../gametest/GameTestRegistryMixin.java | 39 +++++++++++++++++++ .../gametest/AGameTestRegistrationBridge.kt | 10 +++-- .../archie/gametest/ArchieGameTestNeoForge.kt | 29 ++++++++++++++ .../resources/archie_gametest.mixins.json | 3 +- .../kernelpanicsoft/archie/test/ArchieTest.kt | 6 --- .../archie/test/ArchieTestFabric.kt | 5 +++ test/neoforge/build.gradle.kts | 25 ++---------- .../archie/test/ArchieTestNeoForge.kt | 9 +++++ 32 files changed, 229 insertions(+), 209 deletions(-) delete mode 100644 core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt delete mode 100644 datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/DatagenArchieExtension.kt delete mode 100644 datagen/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension create mode 100644 datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ArchieDatagenFabric.kt create mode 100644 datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ArchieDatagenNeoForge.kt delete mode 100644 gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/GametestArchieExtension.kt delete mode 100644 gametest/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension create mode 100644 gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ArchieGameTestFabric.kt create mode 100644 gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/gametest/GameTestRegistryMixin.java create mode 100644 gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ArchieGameTestNeoForge.kt diff --git a/.github/workflows/check.yaml b/.github/workflows/check.yaml index 7bedfdd49..d3bdef1f5 100644 --- a/.github/workflows/check.yaml +++ b/.github/workflows/check.yaml @@ -24,6 +24,7 @@ jobs: fail-fast: false matrix: loader: [fabric, neoforge] + side: [server, client] runs-on: ubuntu-latest timeout-minutes: 40 env: @@ -63,25 +64,21 @@ jobs: env: GITHUB_ACTOR: ${{ github.actor }} GITHUB_TOKEN: ${{ github.token }} - run: xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" ./gradlew check --console=plain -Darchie.junit.gametest.matrix=${{ matrix.loader }}:server,${{ matrix.loader }}:client + run: xvfb-run --auto-servernum --server-args="-screen 0 1280x1024x24" ./gradlew check --console=plain -Darchie.junit.gametest.matrix=${{ matrix.loader }}:${{ matrix.side }} - name: Publish test report if: success() || failure() uses: mikepenz/action-junit-report@v6 with: - check_name: JUnit Test Report (${{ matrix.loader }}) - # Matrix expansion names this job "check (fabric)"/"check (neoforge)", not just "check" - - # without this, the action's default job_name (the raw job id "check") doesn't match any - # actual check-run on this commit, and its annotations silently land on an unrelated job - # (observed: the docs workflow's job) instead of failing loudly. - job_name: check (${{ matrix.loader }}) + check_name: JUnit Test Report (${{ matrix.loader }}, ${{ matrix.side }}) + job_name: check (${{ matrix.loader }}, ${{ matrix.side }}) report_paths: '**/build/test-results/test/TEST-*.xml' - name: Upload test reports if: always() uses: actions/upload-artifact@v7 with: - name: test-reports-${{ matrix.loader }} + name: test-reports-${{ matrix.loader }}-${{ matrix.side }} path: | core/**/build/reports/tests/** core/**/build/tmp/junit-gametest-runner/** diff --git a/core/common/build.gradle.kts b/core/common/build.gradle.kts index 78a3cc9d8..4038110c6 100644 --- a/core/common/build.gradle.kts +++ b/core/common/build.gradle.kts @@ -28,8 +28,6 @@ loom { dependencies { compileOnly(kotlin("reflect")) implementation(libs.junit.jupiter.api) - // Used by the client GameTest harness (archie-gametest) only, to give ComposeScreen a virtual - // clock/dispatcher during tests - never on a real player's classpath. compileOnly deliberately. compileOnly(libs.kotlinx.coroutines.test) testImplementation(libs.junit.jupiter.api) testImplementation(kotlin("reflect")) @@ -46,10 +44,7 @@ dependencies { modImplementation(libs.fabric.loader) modApi(libs.rei.common) - // catalogue.common deliberately omitted - common source never references it directly. modCompileOnly(libs.clothConfig.common) - // yacl.common deliberately omitted too - unused, and it's actually a Fabric-only build (its - // version coordinate ends in "-fabric"). modApi(libs.architectury.common) modApi(libs.storage.common) modApi(libs.storage.resources.common) @@ -87,9 +82,6 @@ tasks { dependsOn(verifyGuiSpriteAssets) } - // Keep stubUnfulfilledExpects()'s generated throwing-actual stubs out of what gets published - - // a consumer with both this jar and a real actual on its classpath must only ever see the - // real one, or Kotlin's actual-resolution can end up preferring the stub. jar { from(sourceSets.main.get().output) exclude("**/*StubKt.class") diff --git a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt index 6a2df4b29..2a18dba08 100644 --- a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt +++ b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/Archie.kt @@ -10,7 +10,6 @@ import net.kernelpanicsoft.archie.config.CategorySpec import net.kernelpanicsoft.archie.config.ConfigContainer import net.kernelpanicsoft.archie.config.ConfigSpec import net.kernelpanicsoft.archie.config.DataSpec -import net.kernelpanicsoft.archie.data.platform.ADataGeneratorPlatform import net.kernelpanicsoft.archie.data.common.conditions.ABuiltinConditions import net.kernelpanicsoft.archie.data.common.crafting.ingredients.ABuiltinIngredients import net.kernelpanicsoft.archie.data.common.tags.platform.ACommonTags @@ -32,7 +31,6 @@ import net.minecraft.world.item.BlockItem import net.minecraft.world.item.Items import net.minecraft.world.level.block.entity.BlockEntityType import org.slf4j.Logger -import java.util.ServiceLoader /** * Archie's mod object and library entrypoint. @@ -56,8 +54,8 @@ object Archie * * Wires up networking (skipped only for a server-only gametest run, since Architectury's * networking registration touches client-only classes), initializes block entity state - * syncing, built-in data providers, and Archie's own config, and activates the datagen/ - * gametest code paths when running under those tasks. + * syncing, built-in data providers, and Archie's own config. Datagen/GameTest code paths are + * activated separately, by archie-datagen/archie-gametest's own mod entrypoints. * * @throws IllegalStateException if running on LexForge, which is not supported. */ @@ -79,13 +77,6 @@ object Archie ACommonTags.init() Config.init() - - // Datagen and GameTest code paths are only activated in dedicated run configs, and only - // exist at all when archie-datagen/archie-gametest are present - see ArchieExtension. - if (AGameTestPlatform.isGameTest) - ServiceLoader.load(ArchieExtension::class.java).forEach { it.onGameTest() } - if (ADataGeneratorPlatform.isDataGen) - ServiceLoader.load(ArchieExtension::class.java).forEach { it.onDataGen() } onClient { ReloadListenerRegistry.register(PackType.CLIENT_RESOURCES, ThemeManifestResourceListener(), Archie % "theme_manifest") ReloadListenerRegistry.register(PackType.CLIENT_RESOURCES, ThemeResourceListener(), Archie % "theme") diff --git a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt b/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt deleted file mode 100644 index 0ccae8b64..000000000 --- a/core/common/src/main/kotlin/net/kernelpanicsoft/archie/ArchieExtension.kt +++ /dev/null @@ -1,21 +0,0 @@ -package net.kernelpanicsoft.archie - -/** - * Extension point for modules that hook into a dedicated datagen/gametest run without `archie-core` - * needing a compile-time dependency on them. `archie-datagen`/`archie-gametest` each register an - * implementation via `META-INF/services/net.kernelpanicsoft.archie.ArchieExtension` - * ([java.util.ServiceLoader]); [Archie.init] invokes whichever hook applies, and does nothing if - * neither module is on the classpath (the normal case for a production build). - */ -interface ArchieExtension -{ - /** Called from [Archie.init] when running under a datagen task ([net.kernelpanicsoft.archie.data.platform.ADataGeneratorPlatform.isDataGen]). */ - fun onDataGen() - { - } - - /** Called from [Archie.init] when running under a GameTest task ([net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform.isGameTest]). */ - fun onGameTest() - { - } -} diff --git a/core/fabric/build.gradle.kts b/core/fabric/build.gradle.kts index c3479659a..c54d928de 100644 --- a/core/fabric/build.gradle.kts +++ b/core/fabric/build.gradle.kts @@ -122,6 +122,11 @@ tasks { jar.get().archiveClassifier.set("dev") + jar { + duplicatesStrategy = DuplicatesStrategy.EXCLUDE + from(project(":archie-core-common").sourceSets.main.get().output) + } + sourcesJar { val commonSources = project(":archie-core-common").tasks.sourcesJar dependsOn(commonSources) diff --git a/core/neoforge/build.gradle.kts b/core/neoforge/build.gradle.kts index 2416714a6..2d93ab33f 100644 --- a/core/neoforge/build.gradle.kts +++ b/core/neoforge/build.gradle.kts @@ -136,14 +136,7 @@ tasks { jar { duplicatesStrategy = DuplicatesStrategy.EXCLUDE - from(project(":archie-core-common").sourceSets.main.get().output) { - // That output is common's own independently-compiled (stub-linked) classes - this - // module's own sourceSets.main.output already has a correctly-actualized copy of all - // of them via actualizes(project(":archie-core-common")) above. Exclude so the - // stub-linked copy can't win the duplicatesStrategy race - it did once, and threw at - // runtime (see today's Archie/neoforge/build.gradle.kts's matching comment). - exclude("net/kernelpanicsoft/archie/**") - } + from(project(":archie-core-common").sourceSets.main.get().output) } sourcesJar { diff --git a/datagen/common/build.gradle.kts b/datagen/common/build.gradle.kts index 0d4d95360..fd20fb2e3 100644 --- a/datagen/common/build.gradle.kts +++ b/datagen/common/build.gradle.kts @@ -11,10 +11,6 @@ loom { } dependencies { - // Plain api, explicit "namedElements" target - not modApi. mod* on a project(...) reference - // makes Loom eagerly read that project's output jar during *configuration*, which can't - // possibly exist yet on a from-scratch build (mod* is for real remapping needs; this and - // archie-core-common are already namespace-symmetric, nothing to remap). api(project(":archie-core-common", "namedElements")) modApi(libs.architectury.common) diff --git a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/DatagenArchieExtension.kt b/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/DatagenArchieExtension.kt deleted file mode 100644 index d36e7ca36..000000000 --- a/datagen/common/src/main/kotlin/net/kernelpanicsoft/archie/data/internal/DatagenArchieExtension.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.kernelpanicsoft.archie.data.internal - -import net.kernelpanicsoft.archie.ArchieExtension - -/** [ArchieExtension] hook that activates [ArchieDatagen] when `archie-datagen` is on the classpath. */ -internal class DatagenArchieExtension : ArchieExtension -{ - override fun onDataGen() - { - ArchieDatagen.init() - } -} diff --git a/datagen/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension b/datagen/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension deleted file mode 100644 index 66911e85a..000000000 --- a/datagen/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension +++ /dev/null @@ -1 +0,0 @@ -net.kernelpanicsoft.archie.data.internal.DatagenArchieExtension diff --git a/datagen/fabric/build.gradle.kts b/datagen/fabric/build.gradle.kts index 53f395cf6..9045cddfe 100644 --- a/datagen/fabric/build.gradle.kts +++ b/datagen/fabric/build.gradle.kts @@ -67,20 +67,7 @@ dependencies { modApi(libs.fabric.api) modImplementation(libs.kotlin.fabric) compileOnly(libs.kotlinx.serialization) - // See the matching comment in gametest/fabric/build.gradle.kts. modLocalRuntime(libs.clothConfig.fabric) - // Archie's own mod init (ArchieFabric -> Archie.init -> ConfigContainer) touches these at - // class-load time regardless of what this product actually needs - archie-core-fabric's own - // bundleRuntimeLibrary calls only cover ITS OWN dev-mode run, which doesn't carry over to a - // project consuming it as a dependency, so this needs its own copies (matching - // core/fabric/build.gradle.kts's set exactly). - bundleRuntimeLibrary(libs.kotlinx.serialization) - bundleRuntimeLibrary(libs.kotlinx.serialization.json) - bundleRuntimeLibrary(libs.kotlinx.serialization.nbt) - bundleRuntimeLibrary(libs.kotlinx.serialization.toml) - bundleRuntimeLibrary(libs.kotlinx.serialization.json5) - bundleRuntimeLibrary(libs.kotlinx.serialization.cbor) - bundleRuntimeLibrary(compose.runtime) implementation(libs.junit.jupiter.api) testImplementation(libs.junit.jupiter.api) @@ -88,8 +75,6 @@ dependencies { "common"(project(":archie-datagen-common", "namedElements")) { isTransitive = false } api(project(":archie-core-fabric", "namedElements")) - // See the matching comment in gametest/fabric/build.gradle.kts. - runtimeOnly(project(":archie-core-common", "namedElements")) { isTransitive = false } } modResources { diff --git a/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ArchieDatagenFabric.kt b/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ArchieDatagenFabric.kt new file mode 100644 index 000000000..916eb9258 --- /dev/null +++ b/datagen/fabric/src/main/kotlin/net/kernelpanicsoft/archie/data/ArchieDatagenFabric.kt @@ -0,0 +1,26 @@ +package net.kernelpanicsoft.archie.data + +import net.fabricmc.api.ModInitializer +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.internal.ArchieDatagen +import net.kernelpanicsoft.archie.data.platform.ADataGeneratorPlatform +import net.kernelpanicsoft.archie.events.datagen.ADatagenEvents + +/** + * Fabric entrypoint for archie-datagen (`fabric.mod.json` `main`). + * + * Activates [ArchieDatagen] directly rather than via `ArchieExtension`/`ServiceLoader` - NeoForge's + * own per-mod JPMS module boundaries make classic `META-INF/services` discovery unreliable across + * mods, so each product now triggers its own hooks from its own real entrypoint instead. Also + * registers [Archie.MOD] with [ADatagenEvents] itself, on Archie's behalf - `archie-core` has no + * datagen entrypoint of its own to do this from, since it doesn't know about datagen concepts. + */ +object ArchieDatagenFabric : ModInitializer +{ + override fun onInitialize() + { + if (!ADataGeneratorPlatform.isDataGen) return + ADatagenEvents += Archie.MOD + ArchieDatagen.init() + } +} diff --git a/datagen/fabric/src/main/resources/fabric.mod.json b/datagen/fabric/src/main/resources/fabric.mod.json index ec3006de0..918c90f50 100644 --- a/datagen/fabric/src/main/resources/fabric.mod.json +++ b/datagen/fabric/src/main/resources/fabric.mod.json @@ -13,6 +13,14 @@ }, "license": "${mod_license}", "environment": "*", + "entrypoints": { + "main": [ + { + "adapter": "kotlin", + "value": "net.kernelpanicsoft.archie.data.ArchieDatagenFabric" + } + ] + }, "mixins": [ "${mod_id}_datagen.mixins.json" ], diff --git a/datagen/neoforge/build.gradle.kts b/datagen/neoforge/build.gradle.kts index eca2fc484..58194507f 100644 --- a/datagen/neoforge/build.gradle.kts +++ b/datagen/neoforge/build.gradle.kts @@ -14,11 +14,6 @@ actualizer { actualizes(project(":archie-datagen-common")) } -// See the dependencies{} block below - project(...) inside dependencies{} resolves to -// DependencyHandler.project(...) (a ProjectDependency), not the real Project, so it can't be -// chased for .tasks there; look it up here instead, where project(...) is still the real Project. -val coreNeoForgeRemapJar = project(":archie-core-neoforge").tasks.named("remapJar", AbstractArchiveTask::class).flatMap { it.archiveFile } - configurations { create("common") configureEach { @@ -70,39 +65,15 @@ dependencies { "neoForge"(libs.neoforge) implementation(libs.kotlin.neoforge) compileOnly(libs.kotlinx.serialization) - // See the matching comment in gametest/fabric/build.gradle.kts. modRuntimeOnly(libs.clothConfig.neoforge) - // archie-core-neoforge's own namedElements/remapJar (below) is compileOnly and only carries - // its own classes, not its transitive mod dependencies - declare architectury-api directly - // (matching core-neoforge's own dependency) so it's genuinely on this project's own runtime - // classpath too, needed by mixins that reference dev.architectury.platform.Mod. modApi(libs.architectury.neoforge) - // Archie's own mod init (ArchieNeoForge -> Archie.init -> ConfigContainer) touches these at - // class-load time regardless of what this product actually needs - archie-core-neoforge's own - // bundleRuntimeLibrary calls only wire up ITS OWN dev-mode "userdev mods and services" locator, - // which doesn't carry over to a project consuming it as a dependency, so this needs its own - // copies (matching core/neoforge/build.gradle.kts's set exactly). - bundleRuntimeLibrary(libs.kotlinx.serialization) - bundleRuntimeLibrary(libs.kotlinx.serialization.json) - bundleRuntimeLibrary(libs.kotlinx.serialization.nbt) - bundleRuntimeLibrary(libs.kotlinx.serialization.toml) - bundleRuntimeLibrary(libs.kotlinx.serialization.json5) - bundleRuntimeLibrary(libs.kotlinx.serialization.cbor) - bundleRuntimeLibrary(compose.runtime) implementation(libs.junit.jupiter.api) testImplementation(libs.junit.jupiter.api) testRuntimeOnly(libs.junit.jupiter.engine) "common"(project(":archie-datagen-common", "namedElements")) { isTransitive = false } - // Compile-only: archie-core-common's/archie-core-neoforge's real classes to compile against. - // NOT added to the runtime classpath - see the matching comment in - // gametest/neoforge/build.gradle.kts for why (archie-core-neoforge's own dev jars are - // production-shaped but not production-complete, and duplicate registration under two - // different FML-recognized mods breaks NeoForge's per-mod module layer). - compileOnly(project(":archie-core-common", "namedElements")) { isTransitive = false } - compileOnly(project(":archie-core-neoforge", "namedElements")) - modRuntimeOnly(files(coreNeoForgeRemapJar)) + api(project(":archie-core-neoforge", "namedElements")) } modResources { diff --git a/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ArchieDatagenNeoForge.kt b/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ArchieDatagenNeoForge.kt new file mode 100644 index 000000000..d3c8a08d4 --- /dev/null +++ b/datagen/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/data/ArchieDatagenNeoForge.kt @@ -0,0 +1,29 @@ +package net.kernelpanicsoft.archie.data + +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.data.internal.ArchieDatagen +import net.kernelpanicsoft.archie.data.platform.ADataGeneratorPlatform +import net.kernelpanicsoft.archie.events.datagen.ADatagenEvents +import net.neoforged.fml.common.Mod + +/** + * NeoForge entrypoint for archie-datagen, registered via the `@Mod` annotation. + * + * Activates [ArchieDatagen] directly rather than via `ArchieExtension`/`ServiceLoader` - NeoForge's + * own per-mod JPMS module boundaries make classic `META-INF/services` discovery unreliable across + * mods, so each product now triggers its own hooks from its own real entrypoint instead. Also + * registers [Archie.MOD] with [ADatagenEvents] itself, on Archie's behalf - `archie-core` has no + * datagen entrypoint of its own to do this from, since it doesn't know about datagen concepts. + */ +@Mod("archie_datagen") +object ArchieDatagenNeoForge +{ + init + { + if (ADataGeneratorPlatform.isDataGen) + { + ADatagenEvents += Archie.MOD + ArchieDatagen.init() + } + } +} diff --git a/gametest/common/build.gradle.kts b/gametest/common/build.gradle.kts index 2f77ed2a9..9817344b3 100644 --- a/gametest/common/build.gradle.kts +++ b/gametest/common/build.gradle.kts @@ -20,8 +20,6 @@ dependencies { compileOnly(kotlin("reflect")) implementation(libs.junit.jupiter.api) - // Gives ComposeScreen a virtual clock/dispatcher during tests - never on a real player's - // classpath (archie-gametest is dev/test-only, never shipped in a production jar). implementation(libs.kotlinx.coroutines.test) testImplementation(libs.junit.jupiter.api) testImplementation(kotlin("reflect")) diff --git a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt index d6e681345..f937a5cac 100644 --- a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt +++ b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/NoOpGameTest.kt @@ -1,5 +1,6 @@ package net.kernelpanicsoft.archie.gametest +import net.kernelpanicsoft.archie.gametest.internal.EMPTY import net.minecraft.gametest.framework.GameTest import net.minecraft.gametest.framework.GameTestHelper @@ -14,7 +15,7 @@ import net.minecraft.gametest.framework.GameTestHelper */ @Suppress("unused") class NoOpGameTest { - @GameTest(template = "archie:gametest/empty") + @GameTest(template = EMPTY) fun GameTestHelper.testNoOpPlaceholder() { succeed() } diff --git a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt index 3d61fc9e4..8d81ca7c4 100644 --- a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt +++ b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/ArchieGameTest.kt @@ -13,7 +13,12 @@ import net.kernelpanicsoft.archie.gametest.internal.tests.ModalComponentsGameTes /** * ID of the empty structure template used by every GameTest in this suite; GameTests that don't - * need a specific structure should reference this via `@GameTest(template = EMPTY)`. + * need a specific structure should reference this via `@GameTest(template = EMPTY)`. Explicitly + * namespaced - Fabric has no custom template-namespace resolution of its own and relies entirely + * on this string being a complete id. NeoForge's `GameTestHooksMixin` mixes `turnMethodIntoTestFunction` + * itself (not just its two namespace/prefix helper methods) to use this value verbatim instead of + * NeoForge's own unconditional `getTemplateNamespace(method) + ":"` wrap, which would otherwise + * double the "archie:" prefix already present here. */ const val EMPTY = "archie:gametest/empty" diff --git a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/GametestArchieExtension.kt b/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/GametestArchieExtension.kt deleted file mode 100644 index 522a4c158..000000000 --- a/gametest/common/src/main/kotlin/net/kernelpanicsoft/archie/gametest/internal/GametestArchieExtension.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.kernelpanicsoft.archie.gametest.internal - -import net.kernelpanicsoft.archie.ArchieExtension - -/** [ArchieExtension] hook that activates [ArchieGameTest] when `archie-gametest` is on the classpath. */ -internal class GametestArchieExtension : ArchieExtension -{ - override fun onGameTest() - { - ArchieGameTest.init() - } -} diff --git a/gametest/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension b/gametest/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension deleted file mode 100644 index d945124ce..000000000 --- a/gametest/common/src/main/resources/META-INF/services/net.kernelpanicsoft.archie.ArchieExtension +++ /dev/null @@ -1 +0,0 @@ -net.kernelpanicsoft.archie.gametest.internal.GametestArchieExtension diff --git a/gametest/fabric/build.gradle.kts b/gametest/fabric/build.gradle.kts index 9f5bd89a3..b64d41606 100644 --- a/gametest/fabric/build.gradle.kts +++ b/gametest/fabric/build.gradle.kts @@ -71,7 +71,6 @@ dependencies { "common"(project(":archie-gametest-common", "namedElements")) { isTransitive = false } api(project(":archie-core-fabric", "namedElements")) - runtimeOnly(project(":archie-core-common", "namedElements")) { isTransitive = false } } modResources { diff --git a/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ArchieGameTestFabric.kt b/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ArchieGameTestFabric.kt new file mode 100644 index 000000000..7392d56db --- /dev/null +++ b/gametest/fabric/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ArchieGameTestFabric.kt @@ -0,0 +1,26 @@ +package net.kernelpanicsoft.archie.gametest + +import net.fabricmc.api.ModInitializer +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.events.gametest.AGametestEvents +import net.kernelpanicsoft.archie.gametest.internal.ArchieGameTest +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform + +/** + * Fabric entrypoint for archie-gametest (`fabric.mod.json` `main`). + * + * Activates [ArchieGameTest] directly rather than via `ArchieExtension`/`ServiceLoader` - NeoForge's + * own per-mod JPMS module boundaries make classic `META-INF/services` discovery unreliable across + * mods, so each product now triggers its own hooks from its own real entrypoint instead. Also + * registers [Archie.MOD] with [AGametestEvents] itself, on Archie's behalf - `archie-core` has no + * gametest entrypoint of its own to do this from, since it doesn't know about gametest concepts. + */ +object ArchieGameTestFabric : ModInitializer +{ + override fun onInitialize() + { + if (!AGameTestPlatform.isGameTest) return + AGametestEvents += Archie.MOD + ArchieGameTest.init() + } +} diff --git a/gametest/fabric/src/main/resources/fabric.mod.json b/gametest/fabric/src/main/resources/fabric.mod.json index 9189dd54f..9e3a5b902 100644 --- a/gametest/fabric/src/main/resources/fabric.mod.json +++ b/gametest/fabric/src/main/resources/fabric.mod.json @@ -13,6 +13,14 @@ }, "license": "${mod_license}", "environment": "*", + "entrypoints": { + "main": [ + { + "adapter": "kotlin", + "value": "net.kernelpanicsoft.archie.gametest.ArchieGameTestFabric" + } + ] + }, "mixins": [ "${mod_id}_gametest.mixins.json" ], diff --git a/gametest/neoforge/build.gradle.kts b/gametest/neoforge/build.gradle.kts index f3b0c4f80..0831174c5 100644 --- a/gametest/neoforge/build.gradle.kts +++ b/gametest/neoforge/build.gradle.kts @@ -13,11 +13,6 @@ actualizer { actualizes(project(":archie-gametest-common")) } -// See the dependencies{} block below - project(...) inside dependencies{} resolves to -// DependencyHandler.project(...) (a ProjectDependency), not the real Project, so it can't be -// chased for .tasks there; look it up here instead, where project(...) is still the real Project. -val coreNeoForgeRemapJar = project(":archie-core-neoforge").tasks.named("remapJar", AbstractArchiveTask::class).flatMap { it.archiveFile } - configurations { create("common") configureEach { @@ -77,12 +72,7 @@ dependencies { "neoForge"(libs.neoforge) implementation(libs.kotlin.neoforge) compileOnly(libs.kotlinx.serialization) - // See the matching comment in gametest/fabric/build.gradle.kts. modRuntimeOnly(libs.clothConfig.neoforge) - // archie-core-neoforge's own namedElements/remapJar (below) is compileOnly and only carries - // its own classes, not its transitive mod dependencies - declare architectury-api directly - // (matching core-neoforge's own dependency) so it's genuinely on this project's own runtime - // classpath too, needed by mixins that reference dev.architectury.platform.Mod. modApi(libs.architectury.neoforge) implementation(libs.junit.jupiter.api) @@ -90,25 +80,7 @@ dependencies { testRuntimeOnly(libs.junit.jupiter.engine) "common"(project(":archie-gametest-common", "namedElements")) { isTransitive = false } - // Compile-only: archie-core-common's real classes for archie-gametest-common's source to - // compile against. NOT added to the runtime classpath (unlike the "common" config above) - - // its classes are also physically present in coreNeoForgeRemapJar below (merged in via - // archie-core-neoforge's shadowJar), and having both on the runtime classpath means two - // different FML-registered mods ("archie_gametest" folding in these raw classes, "archie" from - // the real jar) would each claim the same packages, which NeoForge's module layer rejects. - compileOnly(project(":archie-core-common", "namedElements")) { isTransitive = false } - // Compile-only for the same reason as above: archie-core-neoforge's own "namedElements" dev jar - // is production-*shaped* but not production-*complete* (it lacks archie-core-common's classes, - // only merged in via archie-core-neoforge's shadowJar, which dev mode skips), so mixin prepare - // fails "not found" at runtime despite compiling fine against it. Depend on its real remapJar - // output for the actual dev-mode run classpath instead (files(), not project(...), so this - // stays lazy/task-output-driven and doesn't hit the "mod* on a project reference reads the jar - // during configuration" issue namedElements avoids elsewhere in this build) - same jar a real - // downstream consumer would use, so its mixin refmap and merged classes are both genuinely - // complete. Kept off the runtime classpath alongside namedElements above, to avoid archie-core- - // neoforge's own classes appearing twice (dev jar + remapJar) at runtime. - compileOnly(project(":archie-core-neoforge", "namedElements")) - modRuntimeOnly(files(coreNeoForgeRemapJar)) + api(project(":archie-core-neoforge", "namedElements")) } modResources { diff --git a/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/gametest/GameTestHooksMixin.java b/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/gametest/GameTestHooksMixin.java index f8a3cbb5c..1f478b33c 100644 --- a/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/gametest/GameTestHooksMixin.java +++ b/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/gametest/GameTestHooksMixin.java @@ -30,25 +30,30 @@ private static void getTemplateNamespaceMixin(Method method, CallbackInfoReturna { ResourceLocation template = ResourceLocation.parse(gameTest.template()); cir.setReturnValue(template.getNamespace()); + cir.cancel(); return; } - if (mod != null) - { - cir.setReturnValue(mod.getModId()); - return; - } - + // NoOpGameTest (the fallback placeholder used when a mod has zero real registered tests + // for the current side) never goes through AGameTestPlatform.register, so it's never in + // AGameTestRegistrationBridge.getTestClassToMod() - it's always Archie's own infrastructure + // regardless of which mod's invocation triggered it, so default to Archie.MOD's id. + cir.setReturnValue(mod != null ? mod.getModId() : Archie.MOD_ID); + cir.cancel(); } + /** + * Always suppresses vanilla's own "namespace." infix (see NeoForge's {@code GameTestHooks# + * turnMethodIntoTestFunction}) - it unconditionally wraps the result with {@code + * getTemplateNamespace(method) + ":"} regardless of this method's return value, so any + * additional infixing here would only ever produce a malformed structure id for Archie's own + * (bare-path) templates. + */ @Inject(method = "prefixGameTestTemplate(Ljava/lang/reflect/Method;)Z", at = @At("HEAD"), cancellable = true) private static void prefixGameTestTemplateMixin(Method method, CallbackInfoReturnable cir) { - GameTest gameTest = method.getAnnotation(GameTest.class); - if (gameTest.template().contains(":")) - { - cir.setReturnValue(false); - } + cir.setReturnValue(false); + cir.cancel(); } @Inject(method = "registerGametests()V", at = @At(value = "INVOKE", target = "Lnet/neoforged/fml/ModLoader;postEvent(Lnet/neoforged/bus/api/Event;)V")) diff --git a/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/gametest/GameTestRegistryMixin.java b/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/gametest/GameTestRegistryMixin.java new file mode 100644 index 000000000..f3ffbbf3d --- /dev/null +++ b/gametest/neoforge/src/main/java/net/kernelpanicsoft/archie/mixin/neoforge/gametest/GameTestRegistryMixin.java @@ -0,0 +1,39 @@ +package net.kernelpanicsoft.archie.mixin.neoforge.gametest; + +import com.llamalad7.mixinextras.injector.ModifyReturnValue; +import net.minecraft.gametest.framework.GameTest; +import net.minecraft.gametest.framework.GameTestRegistry; +import net.minecraft.gametest.framework.TestFunction; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +import java.lang.reflect.Method; + +/** + * `GameTestRegistry#turnMethodIntoTestFunction` unconditionally wraps its result with + * {@code GameTestHooks.getTemplateNamespace(method) + ":"}, regardless of what {@code + * GameTestHooks#prefixGameTestTemplate} returns - {@code GameTestHooksMixin}'s overrides of those + * two methods alone can't prevent an already-namespaced {@code @GameTest(template = ...)} string + * (like Archie's own {@code EMPTY} constant) from coming out double-prefixed. Fix up the result + * directly instead: {@link TestFunction} is a record, so every other field can just be copied + * from vanilla's own (otherwise correct) result. + */ +@Mixin(GameTestRegistry.class) +public abstract class GameTestRegistryMixin +{ + @ModifyReturnValue(method = "turnMethodIntoTestFunction", at = @At("RETURN")) + private static TestFunction archie$fixNamespacedTemplate(TestFunction original, Method method) + { + GameTest gameTest = method.getAnnotation(GameTest.class); + if (gameTest == null || !gameTest.template().contains(":")) + { + return original; + } + + return new TestFunction( + original.batchName(), original.testName(), gameTest.template(), original.rotation(), + original.maxTicks(), original.setupTicks(), original.required(), original.manualOnly(), + original.maxAttempts(), original.requiredSuccesses(), original.skyAccess(), original.function() + ); + } +} diff --git a/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt b/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt index e9f47e124..5f27972de 100644 --- a/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt +++ b/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/AGameTestRegistrationBridge.kt @@ -1,6 +1,7 @@ package net.kernelpanicsoft.archie.gametest import dev.architectury.platform.Mod +import net.kernelpanicsoft.archie.Archie import net.kernelpanicsoft.archie.events.gametest.AGametestEvents import net.kernelpanicsoft.archie.gametest.platform.AGameTestModFilter import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform @@ -32,9 +33,10 @@ object AGameTestRegistrationBridge /** * No-ops unless [AGameTestPlatform.isGameTest]. For every mod selected by - * [AGameTestModFilter] from [AGametestEvents.MODS], subscribes to that mod's - * `RegisterGameTestsEvent`; when it fires, fires [AGametestEvents.REGISTER_GAME_TEST] for the - * mod and registers each resulting test class with NeoForge's event. + * [AGameTestModFilter] from [AGametestEvents.MODS] (or just [Archie.MOD] if that's empty), + * subscribes to that mod's `RegisterGameTestsEvent`; when it fires, fires + * [AGametestEvents.REGISTER_GAME_TEST] for the mod and registers each resulting test class + * with NeoForge's event. * * Falls back to [NoOpGameTest] for a mod whose registration turns up no classes at all for the * current [AGameTestPlatform.side] (e.g. a client-only mod's server invocation) - vanilla's @@ -46,7 +48,7 @@ object AGameTestRegistrationBridge { if (!AGameTestPlatform.isGameTest) return - for (mod in AGameTestModFilter.selectMods(AGametestEvents.MODS)) + for (mod in AGameTestModFilter.selectMods(AGametestEvents.MODS.ifEmpty { listOf(Archie.MOD) })) { ModList.get().getModContainerById(mod.modId).ifPresent { it.eventBus?.addListener { event -> diff --git a/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ArchieGameTestNeoForge.kt b/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ArchieGameTestNeoForge.kt new file mode 100644 index 000000000..12ecf82b4 --- /dev/null +++ b/gametest/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/gametest/ArchieGameTestNeoForge.kt @@ -0,0 +1,29 @@ +package net.kernelpanicsoft.archie.gametest + +import net.kernelpanicsoft.archie.Archie +import net.kernelpanicsoft.archie.events.gametest.AGametestEvents +import net.kernelpanicsoft.archie.gametest.internal.ArchieGameTest +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform +import net.neoforged.fml.common.Mod + +/** + * NeoForge entrypoint for archie-gametest, registered via the `@Mod` annotation. + * + * Activates [ArchieGameTest] directly rather than via `ArchieExtension`/`ServiceLoader` - NeoForge's + * own per-mod JPMS module boundaries make classic `META-INF/services` discovery unreliable across + * mods, so each product now triggers its own hooks from its own real entrypoint instead. Also + * registers [Archie.MOD] with [AGametestEvents] itself, on Archie's behalf - `archie-core` has no + * gametest entrypoint of its own to do this from, since it doesn't know about gametest concepts. + */ +@Mod("archie_gametest") +object ArchieGameTestNeoForge +{ + init + { + if (AGameTestPlatform.isGameTest) + { + AGametestEvents += Archie.MOD + ArchieGameTest.init() + } + } +} diff --git a/gametest/neoforge/src/main/resources/archie_gametest.mixins.json b/gametest/neoforge/src/main/resources/archie_gametest.mixins.json index 949eb01cb..5d6920474 100644 --- a/gametest/neoforge/src/main/resources/archie_gametest.mixins.json +++ b/gametest/neoforge/src/main/resources/archie_gametest.mixins.json @@ -7,7 +7,8 @@ "lifecycle.MinecraftClientMixin" ], "mixins": [ - "gametest.GameTestHooksMixin" + "gametest.GameTestHooksMixin", + "gametest.GameTestRegistryMixin" ], "injectors": { "defaultRequire": 1 diff --git a/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt index 4f6a1c3ba..7893a95cd 100644 --- a/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt +++ b/test/common/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTest.kt @@ -10,7 +10,6 @@ import net.kernelpanicsoft.archie.events.gametest.AGametestEvents import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform import net.kernelpanicsoft.archie.test.gametest.ArchieTestGameTest import net.kernelpanicsoft.archie.test.gametest.DataAttachmentTestFixtures -import net.kernelpanicsoft.archie.test.gametest.CapabilityLookupTestFixtures import net.kernelpanicsoft.archie.test.data.ArchieTestDatagen import net.kernelpanicsoft.archie.util.onClient import net.minecraft.resources.ResourceLocation @@ -44,11 +43,6 @@ object ArchieTest GuiRegistry.init() if (AGameTestPlatform.isGameTest) DataAttachmentTestFixtures.init() - // Must come after TileRegistry.init() (needs TestTile to actually exist), but is otherwise - // still normal mod-init timing - see CapabilityLookupTestFixtures's KDoc for why this can't - // be deferred to inside the @GameTest methods themselves. - if (AGameTestPlatform.isGameTest) - CapabilityLookupTestFixtures.init() } @JvmStatic diff --git a/test/fabric/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestFabric.kt b/test/fabric/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestFabric.kt index 17bcef71d..669c352db 100644 --- a/test/fabric/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestFabric.kt +++ b/test/fabric/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestFabric.kt @@ -2,6 +2,8 @@ package net.kernelpanicsoft.archie.test import net.fabricmc.api.ClientModInitializer import net.fabricmc.api.ModInitializer +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform +import net.kernelpanicsoft.archie.test.gametest.CapabilityLookupTestFixtures object ArchieTestFabric : ModInitializer, ClientModInitializer { @@ -9,6 +11,9 @@ object ArchieTestFabric : ModInitializer, ClientModInitializer { ArchieTest.init() ArchieTest.initCommon() + // See the matching comment in ArchieTest.init() - Fabric's registries resolve immediately + // on registration (no staged/frozen model to race), so this is safe right here. + if (AGameTestPlatform.isGameTest) CapabilityLookupTestFixtures.init() } override fun onInitializeClient() diff --git a/test/neoforge/build.gradle.kts b/test/neoforge/build.gradle.kts index 33ba4b670..e8f6213de 100644 --- a/test/neoforge/build.gradle.kts +++ b/test/neoforge/build.gradle.kts @@ -1,5 +1,4 @@ import net.kernelpanicsoft.archie.plugin.bundleMod -import org.gradle.api.tasks.bundling.AbstractArchiveTask plugins { alias(libs.plugins.shadow) @@ -15,16 +14,6 @@ actualizer { actualizes(project(":archie-test-common")) } -// See the dependencies{} block below - project(...) inside dependencies{} resolves to -// DependencyHandler.project(...) (a ProjectDependency), not the real Project, so it can't be -// chased for .tasks there; look these up here instead, where project(...) is still the real -// Project. Each of these products' own "namedElements" dev jar is production-shaped but not -// production-complete (their shadowJar-merged classes are missing until a real build), so this -// project depends on their real remapJar output for its own runtime classpath instead. -val coreNeoForgeRemapJar = project(":archie-core-neoforge").tasks.named("remapJar", AbstractArchiveTask::class).flatMap { it.archiveFile } -val datagenNeoForgeRemapJar = project(":archie-datagen-neoforge").tasks.named("remapJar", AbstractArchiveTask::class).flatMap { it.archiveFile } -val gametestNeoForgeRemapJar = project(":archie-gametest-neoforge").tasks.named("remapJar", AbstractArchiveTask::class).flatMap { it.archiveFile } - configurations { create("common") create("shadowCommon") @@ -120,17 +109,9 @@ dependencies { "common"(project(":archie-test-common", "namedElements")) { isTransitive = false } "shadowCommon"(project(":archie-test-common", "transformProductionNeoForge")) { isTransitive = false } - // Compile-only: real classes to compile against, kept off the runtime classpath - see the - // matching comment in gametest/neoforge/build.gradle.kts for why (each of these products' own - // dev jar is production-shaped but not production-complete, and duplicate registration under - // two different FML-recognized mods breaks NeoForge's per-mod module layer). - compileOnly(project(":archie-core-common", "namedElements")) { isTransitive = false } - compileOnly(project(":archie-core-neoforge", "namedElements")) - compileOnly(project(":archie-datagen-neoforge", "namedElements")) - compileOnly(project(":archie-gametest-neoforge", "namedElements")) - modRuntimeOnly(files(coreNeoForgeRemapJar)) - modRuntimeOnly(files(datagenNeoForgeRemapJar)) - modRuntimeOnly(files(gametestNeoForgeRemapJar)) + api(project(":archie-core-neoforge", "namedElements")) + api(project(":archie-datagen-neoforge", "namedElements")) + api(project(":archie-gametest-neoforge", "namedElements")) } modResources { diff --git a/test/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestNeoForge.kt b/test/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestNeoForge.kt index 729d3eaf0..cb5ecf913 100644 --- a/test/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestNeoForge.kt +++ b/test/neoforge/src/main/kotlin/net/kernelpanicsoft/archie/test/ArchieTestNeoForge.kt @@ -1,10 +1,13 @@ package net.kernelpanicsoft.archie.test import dev.nyon.klf.MOD_BUS +import net.kernelpanicsoft.archie.gametest.platform.AGameTestPlatform +import net.kernelpanicsoft.archie.test.gametest.CapabilityLookupTestFixtures import net.neoforged.fml.common.Mod import net.neoforged.fml.event.lifecycle.FMLClientSetupEvent import net.neoforged.fml.event.lifecycle.FMLCommonSetupEvent import net.neoforged.fml.event.lifecycle.FMLConstructModEvent +import net.neoforged.neoforge.capabilities.RegisterCapabilitiesEvent @Mod(ArchieTest.MOD_ID) object ArchieTestNeoForge @@ -19,6 +22,12 @@ object ArchieTestNeoForge MOD_BUS.addListener { ArchieTest.initCommon() } + // See the matching comment in ArchieTest.init() - TileRegistry.TestTile isn't valid until + // its registry unfreezes at RegisterEvent, which has already fired by RegisterCapabilitiesEvent + // (the same event Common Storage Lib's own BlockLookup.onRegister listens to internally). + MOD_BUS.addListener { + if (AGameTestPlatform.isGameTest) CapabilityLookupTestFixtures.init() + } // GuiRegistry.initClient() (an ADeferredRegistryHolder override) now registers its own // screen factories at the correct time on its own - see // net.kernelpanicsoft.archie.registries.scheduleEarlyClientRegistration. A manual