From 82ab7d87699d0a7b33d1ab97b586adc804854f9f Mon Sep 17 00:00:00 2001 From: Essential CI Date: Wed, 26 Feb 2025 11:31:38 +0000 Subject: [PATCH] Version 1.3.5.8 --- changelog/release-1.3.5.8.md | 23 ++ .../gg/essential/gui/layoutdsl/layout.kt | 16 +- .../gui/elementa/state/v2/animate.kt | 3 +- gradle.properties | 2 +- .../gg/essential/gui/common/extensions.kt | 9 - .../gg/essential/config/EssentialConfig.kt | 122 +++---- .../gui/common/OldEssentialDropDown.kt | 229 ------------- .../gg/essential/gui/common/Tooltips.kt | 27 +- .../kotlin/gg/essential/gui/wardrobe/Item.kt | 2 +- .../essential/gui/wardrobe/WardrobeState.kt | 36 ++- .../FeaturedPageCollectionConfiguration.kt | 305 +++++++++++------- .../cosmetics/EmoteWheelManager.kt | 36 ++- .../cosmetics/InfraCosmeticsData.kt | 3 +- .../essential/util/essentialGuiExtensions.kt | 24 +- .../cosmetics/EssentialModelRenderer.java | 9 +- .../events/CosmeticEventEmitter.java | 2 +- .../essential/handlers/OnlineIndicator.java | 14 +- .../key/EssentialKeybindingRegistry.java | 5 +- .../impl/client/gui/GuiInventoryExt.java | 15 +- .../Mixin_UpdateWindowTitle_AddSPSTitle.java | 41 +++ ...Mixin_UpdateWindowTitle_DisplayScreen.java | 30 ++ .../Mixin_UpdateWindowTitle_LoadWorld.java | 35 ++ .../Mixin_UpdateWindowTitle_OpenToLan.java | 30 ++ ... Mixin_TrackInventoryPlayerRendering.java} | 9 +- ...ServerChatChannelMessagePacketHandler.java | 46 +-- .../connectionmanager/sps/SPSManager.java | 3 + .../gg/essential/gui/common/UI3DPlayer.kt | 2 +- .../gg/essential/gui/friends/SocialMenu.kt | 19 +- .../gui/friends/message/MessageInput.kt | 30 +- .../gui/friends/message/MessageTitleBar.kt | 4 +- .../message/v2/ReplyableMessageScreen.kt | 3 + .../gui/friends/previews/ChannelPreview.kt | 12 +- .../gui/modals/UpdateNotificationModal.kt | 1 + .../gui/screenshot/ScreenshotOverlay.kt | 126 ++++++-- .../essential/gui/sps/InviteFriendsModal.kt | 11 +- .../essential/gui/wardrobe/EmoteWheelPage.kt | 200 ++++-------- .../gg/essential/gui/wardrobe/Wardrobe.kt | 9 +- .../gui/wardrobe/WardrobeContainer.kt | 10 +- .../wardrobe/categories/CategoryComponent.kt | 2 +- .../wardrobe/categories/featuredCategory.kt | 118 ++++--- .../gui/wardrobe/components/cosmeticItem.kt | 153 +-------- .../cosmeticOrEmoteItemFunctions.kt | 38 ++- .../gui/wardrobe/components/draggingEmote.kt | 110 +++++++ .../gui/wardrobe/components/previewWindow.kt | 66 +++- .../modals/PurchaseConfirmationModal.kt | 31 +- .../gg/essential/sps/WindowTitleManager.kt | 72 +++++ .../resources/assets/essential/commit.txt | 2 +- .../assets/essential/lang/en_us.lang | 7 + src/main/resources/mixins.essential.json | 6 +- .../essential/cosmetics/WearablesManager.kt | 37 ++- .../cosmetics/events/AnimationEvent.kt | 2 + .../cosmetics/events/AnimationEventType.kt | 1 + .../state/EssentialAnimationSystem.kt | 19 +- .../mod/cosmetics/featured/featuredPage.kt | 92 ++++++ .../gg/essential/model/ModelInstance.kt | 25 -- .../gg/essential/ice/stun/StunSocket.kt | 3 +- .../gg/essential/mod/vigilance2/GuiBuilder.kt | 14 + versions/1.16.2-1.12.2.txt | 4 + 58 files changed, 1342 insertions(+), 963 deletions(-) create mode 100644 changelog/release-1.3.5.8.md delete mode 100644 gui/essential/src/main/kotlin/gg/essential/gui/common/OldEssentialDropDown.kt rename subprojects/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/featured/FeaturedPage.kt => src/main/java/gg/essential/mixins/impl/client/gui/GuiInventoryExt.java (64%) create mode 100644 src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_AddSPSTitle.java create mode 100644 src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_DisplayScreen.java create mode 100644 src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_LoadWorld.java create mode 100644 src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_OpenToLan.java rename src/main/java/gg/essential/mixins/transformers/client/gui/inventory/{Mixin_DisableCosmeticsInInventory.java => Mixin_TrackInventoryPlayerRendering.java} (85%) create mode 100644 src/main/kotlin/gg/essential/gui/wardrobe/components/draggingEmote.kt create mode 100644 src/main/kotlin/gg/essential/sps/WindowTitleManager.kt create mode 100644 subprojects/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/featured/featuredPage.kt diff --git a/changelog/release-1.3.5.8.md b/changelog/release-1.3.5.8.md new file mode 100644 index 0000000..a3ea7e1 --- /dev/null +++ b/changelog/release-1.3.5.8.md @@ -0,0 +1,23 @@ +Title: Bug Patch +Summary: Minor bug fixes + +## Improvements +- Added the ability to have more interactions between different cosmetics and emotes +- Improved cosmetic and emote settings and hide additional cosmetic/emote settings if cosmetics/emotes are disabled +- Improved nameplate and tab-list Essential Icon settings +- Improved game window title to say "Multiplayer (Hosted World)" when in a hosted world + +## Wardrobe +- Added the ability to move emotes between emote wheels by dragging it over the "switch emote wheel" arrows +- Improved the layout of the Featured page + +## Social Menu +- Increased the character limit of chat messages from 500 to 2500 +- Added a visual indicator when a chat message approaches, or is over, the character limit +- Added confirmation toast when inviting players to a world +- Removed group invites for now due to various issues with how they behaved + +## Bug Fixes +- Fixed Essential nameplate indicator not being correctly affected by lighting in 1.8.9 & 1.12.2 +- Fixed the "Direct message notifications" and "Group message notifications" settings not working +- Fixed the show/hide cosmetic keybind doing things despite cosmetics being disabled diff --git a/elementa/layoutdsl/src/main/kotlin/gg/essential/gui/layoutdsl/layout.kt b/elementa/layoutdsl/src/main/kotlin/gg/essential/gui/layoutdsl/layout.kt index 7226661..0aea0ea 100644 --- a/elementa/layoutdsl/src/main/kotlin/gg/essential/gui/layoutdsl/layout.kt +++ b/elementa/layoutdsl/src/main/kotlin/gg/essential/gui/layoutdsl/layout.kt @@ -81,14 +81,6 @@ class LayoutScope( return IfDsl({ state() == null }, true) } - fun if_(condition: StateByScope.() -> Boolean, cache: Boolean = false, block: LayoutScope.() -> Unit): IfDsl { - return if_(stateBy(condition), cache, block) - } - - fun ifNotNull(stateBlock: StateByScope.() -> T?, cache: Boolean = false, block: LayoutScope.(T) -> Unit): IfDsl { - return ifNotNull(stateBy(stateBlock), cache, block) - } - class IfDsl(internal val elseState: StateV2, internal var cache: Boolean) infix fun IfDsl.`else`(block: LayoutScope.() -> Unit) { @@ -105,11 +97,6 @@ class LayoutScope( forEach({ trackedListOf(state()) }, cache) { block(it) } } - /** Makes available to the inner scope the value derived from the given [stateBlock]. */ - fun bind(stateBlock: StateByScope.() -> T, cache: Boolean = false, block: LayoutScope.(T) -> Unit) { - bind(stateBy(stateBlock), cache, block) - } - /** * Repeats the inner block for each element in the given list state. * If the list state changes, components from old scopes are removed and new scopes are created and initialized as @@ -314,12 +301,13 @@ inline fun UIComponent.layout(modifier: Modifier = Modifier, block: LayoutScope. * * Note: This does **not** change the size constrains of `this`. These must be set up manually or via [modifier]. */ -fun UIComponent.layoutAsBox(modifier: Modifier = Modifier, block: LayoutScope.() -> Unit) { +fun UIComponent.layoutAsBox(modifier: Modifier = Modifier, block: LayoutScope.() -> Unit): UIComponent { contract { callsInPlace(block, InvocationKind.EXACTLY_ONCE) } addChildModifier(Modifier.alignBoth(Alignment.Center)) layout(modifier, block) + return this } /** diff --git a/elementa/statev2/src/main/kotlin/gg/essential/gui/elementa/state/v2/animate.kt b/elementa/statev2/src/main/kotlin/gg/essential/gui/elementa/state/v2/animate.kt index bcb7415..6d59257 100644 --- a/elementa/statev2/src/main/kotlin/gg/essential/gui/elementa/state/v2/animate.kt +++ b/elementa/statev2/src/main/kotlin/gg/essential/gui/elementa/state/v2/animate.kt @@ -48,8 +48,7 @@ private class AnimationDriver( override fun setup() { previousDriverStateValue = driver.getUntracked() durationFrames = (Window.of(boundComponent).animationFPS * duration).toInt().coerceAtLeast(1) - driverEffect = effect(ReferenceHolder.Weak) { - val input = driver() + driverEffect = driver.onChange(ReferenceHolder.Weak) { input -> animationEventList.add(AnimationEvent(previousDriverStateValue, input, durationFrames)) previousDriverStateValue = input } diff --git a/gradle.properties b/gradle.properties index 832ecc3..439bc45 100644 --- a/gradle.properties +++ b/gradle.properties @@ -7,4 +7,4 @@ org.gradle.configureondemand=true org.gradle.parallel.threads=128 org.gradle.jvmargs=-Xmx16G minecraftVersion=11202 -version=1.3.5.7 +version=1.3.5.8 diff --git a/gui/elementa/src/main/kotlin/gg/essential/gui/common/extensions.kt b/gui/elementa/src/main/kotlin/gg/essential/gui/common/extensions.kt index a8add32..441e4f0 100644 --- a/gui/elementa/src/main/kotlin/gg/essential/gui/common/extensions.kt +++ b/gui/elementa/src/main/kotlin/gg/essential/gui/common/extensions.kt @@ -24,7 +24,6 @@ import gg.essential.elementa.utils.ObservableClearEvent import gg.essential.elementa.utils.ObservableList import gg.essential.elementa.utils.ObservableRemoveEvent import gg.essential.gui.elementa.state.v2.toV1 -import gg.essential.gui.util.hasWindow import kotlin.reflect.KProperty @Deprecated("Using StateV1 is discouraged, use StateV2 instead") @@ -60,14 +59,6 @@ fun T.bindParent( index: Int? = null ) = bindParent(parent, state.toV1(parent), delayed, index) -fun T.bindFloating(state: State) = apply { - state.onSetValueAndNow { - if (hasWindow) { - this.setFloating(it) - } - } -} - fun T.bindEffect(effect: Effect, state: State, delayed: Boolean = true) = apply { state.onSetValueAndNow { val update = { diff --git a/gui/essential/src/main/kotlin/gg/essential/config/EssentialConfig.kt b/gui/essential/src/main/kotlin/gg/essential/config/EssentialConfig.kt index a2aee69..8f1ac99 100644 --- a/gui/essential/src/main/kotlin/gg/essential/config/EssentialConfig.kt +++ b/gui/essential/src/main/kotlin/gg/essential/config/EssentialConfig.kt @@ -237,6 +237,9 @@ object EssentialConfig : Vigilant2(), GuiEssentialPlatform.Config { val showQuickActionBarState = property("General.Experience.Quick Action Bar", true) var showQuickActionBar by showQuickActionBarState + val replaceWindowTitleState = property("General.General.Replace Window Title", true) + var replaceWindowTitle by replaceWindowTitleState + val screenshotBrowserItemsPerRowState = property("Hidden.Hidden.screenshotBrowserItemsPerRow", 3) var screenshotBrowserItemsPerRow by screenshotBrowserItemsPerRowState @@ -384,24 +387,27 @@ object EssentialConfig : Vigilant2(), GuiEssentialPlatform.Config { category("Emotes") { subcategory("General") { switch(!disableEmotesState) { - name = "Show emotes" - description = "Show emote animations on yourself and other players." - } - - selector(allowEmoteSounds) { - name = "Allow emote sounds" - description = "Select who you can hear emote sounds from." - options = AllowEmoteSounds.entries.map { it.label } - } - - switch(thirdPersonEmotesState) { - name = "Play emotes in third person view" - description = "Emotes will be shown in third-person view. You can still toggle between front and back view." - } - - switch(emotePreviewState) { - name = "Emote preview" - description = "When playing emotes, show a model of your character performing the emote in the upper left corner of the screen." + name = "Emotes" + description = "Better express yourself with emotes." + } + dynamic { + if (!disableEmotesState()) { + selector(allowEmoteSounds) { + name = "Allow emote sounds" + description = "Select who you can hear emote sounds from." + options = AllowEmoteSounds.entries.map { it.label } + } + + switch(thirdPersonEmotesState) { + name = "Play emotes in third person view" + description = "Emotes will be shown in third-person view. You can still toggle between front and back view." + } + + switch(emotePreviewState) { + name = "Emote preview" + description = "When playing emotes, show a model of your character performing the emote in the upper left corner of the screen." + } + } } } } @@ -409,37 +415,42 @@ object EssentialConfig : Vigilant2(), GuiEssentialPlatform.Config { category("Cosmetics") { subcategory("General") { switch(!disableCosmeticsState) { - name = "Show cosmetics" - description = "Show cosmetics on yourself and other players." - } - switch(ownCosmeticsHiddenStateWithSource.bimap({ it.first }, { it to true })) { - name = "Hide your cosmetics" - description = "Hides your equipped cosmetics for all players." - } - - val swapFirstTwo: (Int) -> Int = { if (it in 0..1) (it + 1) % 2 else it } - - selector(cosmeticArmorSettingSelfState.bimap(swapFirstTwo, swapFirstTwo)) { - name = "Cosmetics & armor visibility on me" - description = "Cosmetics and armor may conflict with each other on your player. This setting does not effect what other players see." - options = listOf("Only cosmetics", "Only armor", "Cosmetics and armor") - } - - selector(cosmeticArmorSettingOtherState.bimap(swapFirstTwo, swapFirstTwo)) { - name = "Cosmetics & armor visibility on others" - description = "Cosmetics and armor may conflict with each other on other players. This setting does not effect what other players see." - options = listOf("Only cosmetics", "Only armor", "Cosmetics and armor") - } - - switch(disableCosmeticsInInventoryState) { - name = "Hide cosmetics in inventory" - description = "Hides your equipped cosmetics on the player preview inside your inventory." + name = "Cosmetics" + description = "Enhance your Minecraft character with cosmetics." + } + dynamic { + if (!disableCosmeticsState()) { + switch(ownCosmeticsHiddenStateWithSource.bimap({ it.first }, { it to true })) { + name = "Hide your cosmetics" + description = "Hide your equipped cosmetics for everyone." + } + + val swapFirstTwo: (Int) -> Int = { if (it in 0..1) (it + 1) % 2 else it } + + selector(cosmeticArmorSettingSelfState.bimap(swapFirstTwo, swapFirstTwo)) { + name = "Cosmetics & armor visibility on me" + description = "Cosmetics and armor may conflict with each other on your player. This setting does not effect what other players see." + options = listOf("Only cosmetics", "Only armor", "Cosmetics and armor") + } + + selector(cosmeticArmorSettingOtherState.bimap(swapFirstTwo, swapFirstTwo)) { + name = "Cosmetics & armor visibility on others" + description = "Cosmetics and armor may conflict with each other on other players. This setting does not effect what other players see." + options = listOf("Only cosmetics", "Only armor", "Cosmetics and armor") + } + + switch(disableCosmeticsInInventoryState) { + name = "Hide cosmetics in inventory" + description = "Hides your equipped cosmetics on the player preview inside your inventory." + } + + switch(hideCosmeticsWhenServerOverridesSkinState) { + name = "Hide cosmetics on server skins" + description = "Hides cosmetics on players when the joined server modifies the user’s skins." + } + } } - switch(hideCosmeticsWhenServerOverridesSkinState) { - name = "Hide cosmetics on server skins" - description = "Hides cosmetics on players when the joined server modifies the user’s skins." - } } } @@ -512,14 +523,17 @@ object EssentialConfig : Vigilant2(), GuiEssentialPlatform.Config { } } - subcategory("Essential Indicator") { - switch(showEssentialIndicatorOnTabState) { - name = "Essential indicator in tab-list" - description = "Shows the indicator on other Essential players in the tab-list." - } + subcategory("Nameplates") { switch(showEssentialIndicatorOnNametagState) { - name = "Essential indicator on nameplates" - description = "Shows the indicator on other Essential players’ nameplates." + name = "Essential icon on nameplates" + description = "Shows the Essential icon on Essential players’ nameplates." + } + } + + subcategory("Tab-list") { + switch(showEssentialIndicatorOnTabState) { + name = "Essential icon in tab-list" + description = "Shows the Essential icon on Essential players in the tab-list." } } diff --git a/gui/essential/src/main/kotlin/gg/essential/gui/common/OldEssentialDropDown.kt b/gui/essential/src/main/kotlin/gg/essential/gui/common/OldEssentialDropDown.kt deleted file mode 100644 index 0089c89..0000000 --- a/gui/essential/src/main/kotlin/gg/essential/gui/common/OldEssentialDropDown.kt +++ /dev/null @@ -1,229 +0,0 @@ -/* - * Copyright (c) 2024 ModCore Inc. All rights reserved. - * - * This code is part of ModCore Inc.'s Essential Mod repository and is protected - * under copyright registration # TX0009138511. For the full license, see: - * https://github.com/EssentialGG/Essential/blob/main/LICENSE - * - * You may not use, copy, reproduce, modify, sell, license, distribute, - * commercialize, or otherwise exploit, or create derivative works based - * upon, this file or any other in this repository, all of which is reserved by Essential. - */ -package gg.essential.gui.common - -import gg.essential.elementa.components.ScrollComponent -import gg.essential.elementa.components.UIBlock -import gg.essential.elementa.components.UIContainer -import gg.essential.elementa.constraints.* -import gg.essential.elementa.constraints.animation.* -import gg.essential.elementa.dsl.* -import gg.essential.elementa.effects.ScissorEffect -import gg.essential.elementa.state.BasicState -import gg.essential.elementa.state.State -import gg.essential.elementa.state.toConstraint -import gg.essential.gui.EssentialPalette -import gg.essential.gui.common.shadow.EssentialUIText -import gg.essential.gui.common.shadow.ShadowIcon -import gg.essential.gui.util.hoveredState -import gg.essential.universal.USound -import gg.essential.util.* -import gg.essential.vigilance.utils.onLeftClick - -class OldEssentialDropDown( - initialSelection: Int, - private val options: List, - private val maxDisplayOptions: Int = 6, -) : UIBlock() { - - private val writableExpandedState: State = BasicState(false) - private val dropdownAnimating = BasicState(false) - private var animationCounter = 0 - - /** Public States **/ - val selectedIndex: State = BasicState(initialSelection) - val selectedText: State = selectedIndex.map { - options[it] - } - val expandedState = ReadOnlyState(writableExpandedState) - - private val selectedArea by UIContainer().constrain { - width = 100.percent - height = 17.pixels - } childOf this - - private val selectAreaHovered = selectedArea.hoveredState() - - private val currentSelectionText by EssentialUIText(shadowColor = EssentialPalette.TEXT_SHADOW).bindText(selectedText).constrain { - x = 5.pixels - y = CenterConstraint() - color = EssentialPalette.getTextColor(selectAreaHovered).toConstraint() - } childOf selectedArea - - private val iconState = writableExpandedState.map { - if (it) { - EssentialPalette.ARROW_UP_7X4 - } else { - EssentialPalette.ARROW_DOWN_7X4 - } - } - - private val downArrow by ShadowIcon(iconState, BasicState(true)).constrain { - x = 5.pixels(alignOpposite = true) - y = CenterConstraint() - }.rebindPrimaryColor(EssentialPalette.getTextColor(selectAreaHovered)).rebindShadowColor(BasicState(EssentialPalette.COMPONENT_BACKGROUND)) childOf selectedArea - - - private val expandedBlock by UIBlock(EssentialPalette.BUTTON_HIGHLIGHT).constrain { - y = SiblingConstraint() - width = 100.percent - height = 0.pixels // Start collapsed - }.bindFloating(writableExpandedState or dropdownAnimating) childOf this effect ScissorEffect() - - private val scrollerContainer by UIContainer().constrain { - x = CenterConstraint() - y = CenterConstraint() - width = 100.percent - 4.pixels - height = (100.percent - 4.pixels).coerceAtLeast(0.pixels) - } childOf expandedBlock - - private val scroller by ScrollComponent().centered().constrain { - width = 100.percent - height = 100.percent - } childOf scrollerContainer - - private val expandedContentArea by UIBlock(EssentialPalette.COMPONENT_BACKGROUND).constrain { - x = CenterConstraint() - width = 100.percent - height = ChildBasedSizeConstraint() + 6.pixels - } childOf scroller - - private val expandedContent by UIContainer().centered().constrain { - width = 100.percent - height = ChildBasedSizeConstraint() - } childOf expandedContentArea - - private val scrollbarContainer by UIContainer().constrain { - x = 0.pixels(alignOpposite = true) - y = CenterConstraint() - width = 2.pixels - height = 100.percent - }.onLeftClick { - it.stopPropagation() - } childOf scrollerContainer - - private val scrollbar by UIBlock(EssentialPalette.SCROLLBAR).constrain { - width = 100.percent - } childOf scrollbarContainer - - private fun getMaxItemWidth(): Float { - return options.maxOf { - it.width() - } - } - - private val scrollable = options.size > maxDisplayOptions - - /** - * @return The default width of this dropdown based on its contents - */ - fun getDefaultWidth(): Float { - return getMaxItemWidth() + 25 - } - - init { - constrain { - width = (getDefaultWidth()).pixels - height = ChildBasedSizeConstraint() - } - - setColor((selectAreaHovered or expandedState).map { - if (it) { - EssentialPalette.BUTTON_HIGHLIGHT - } else { - EssentialPalette.COMPONENT_BACKGROUND_HIGHLIGHT - } - }.toConstraint()) - - if (scrollable) { - scroller.setVerticalScrollBarComponent(scrollbar, hideWhenUseless = false) - } - - options.withIndex().forEach { (index, value) -> - val optionContainer by UIBlock().constrain { - y = SiblingConstraint() - width = 100.percent - height = 20.pixels - }.onLeftClick { - USound.playButtonPress() - it.stopPropagation() - select(index) - } childOf expandedContent - val hovered = optionContainer.hoveredState() - - optionContainer.setColor(hovered.map { - if (it) { - EssentialPalette.BUTTON - } else { - EssentialPalette.COMPONENT_BACKGROUND - } - }.toConstraint()) - val text by EssentialUIText(value, shadowColor = EssentialPalette.TEXT_SHADOW_LIGHT).constrain { - x = 5.pixels - y = CenterConstraint() - color = EssentialPalette.getTextColor(hovered).toConstraint() - } childOf optionContainer - } - - selectedArea.onLeftClick { event -> - USound.playButtonPress() - event.stopPropagation() - - if (writableExpandedState.get()) { - collapse() - } else { - expand() - } - } - } - - fun select(index: Int) { - if (index in options.indices) { - selectedIndex.set(index) - collapse() - } - } - - fun expand(instantly: Boolean = false) { - writableExpandedState.set(true) - applyExpandedBlockHeight( - instantly, - (options.size.coerceAtMost(maxDisplayOptions) * 20).pixels + 10.pixels - ) - } - - fun collapse(instantly: Boolean = false) { - writableExpandedState.set(false) - applyExpandedBlockHeight(instantly, 0.pixels) - } - - private fun applyExpandedBlockHeight( - instantly: Boolean, - heightConstraint: HeightConstraint, - ) { - if (instantly) { - expandedBlock.setHeight(heightConstraint) - dropdownAnimating.set(false) - } else { - val counterInstance = ++animationCounter - dropdownAnimating.set(true) - expandedBlock.animate { - setHeightAnimation(Animations.OUT_EXP, 0.25f, heightConstraint) - onComplete { - if (counterInstance == animationCounter) { - dropdownAnimating.set(false) - } - } - } - } - } -} diff --git a/gui/essential/src/main/kotlin/gg/essential/gui/common/Tooltips.kt b/gui/essential/src/main/kotlin/gg/essential/gui/common/Tooltips.kt index 6f642cb..f9315a0 100644 --- a/gui/essential/src/main/kotlin/gg/essential/gui/common/Tooltips.kt +++ b/gui/essential/src/main/kotlin/gg/essential/gui/common/Tooltips.kt @@ -263,28 +263,32 @@ class EssentialTooltip( Position.RIGHT -> left - 2 - i Position.ABOVE -> hCenter - (notchSize - i) - 0.5 Position.BELOW -> hCenter - (notchSize - i) - 0.5 - is Position.MOUSE -> continue + Position.MOUSE -> continue + is Position.MOUSE_OFFSET -> continue }, when (position) { Position.LEFT -> vCenter - (notchSize - i) - 0.5 Position.RIGHT -> vCenter - (notchSize - i) - 0.5 Position.ABOVE -> bottom + i Position.BELOW -> top - 2 - i - is Position.MOUSE -> continue + Position.MOUSE -> continue + is Position.MOUSE_OFFSET -> continue }, when (position) { Position.LEFT -> right + 2 + i Position.RIGHT -> left - 1 - i Position.ABOVE -> hCenter + (notchSize - i) + 0.5 Position.BELOW -> hCenter + (notchSize - i) + 0.5 - is Position.MOUSE -> continue + Position.MOUSE -> continue + is Position.MOUSE_OFFSET -> continue }, when (position) { Position.LEFT -> vCenter + (notchSize - i) + 0.5 Position.RIGHT -> vCenter + (notchSize - i) + 0.5 Position.ABOVE -> bottom + i + 2 Position.BELOW -> top - i - 1 - is Position.MOUSE -> continue + Position.MOUSE -> continue + is Position.MOUSE_OFFSET -> continue }, ) UIBlock.drawBlock( @@ -295,28 +299,32 @@ class EssentialTooltip( Position.RIGHT -> left - 1 - i Position.ABOVE -> hCenter - (notchSize - i) - 0.5 Position.BELOW -> hCenter - (notchSize - i) - 0.5 - is Position.MOUSE -> continue + Position.MOUSE -> continue + is Position.MOUSE_OFFSET -> continue }, when (position) { Position.LEFT -> vCenter - (notchSize - i) - 0.5 Position.RIGHT -> vCenter - (notchSize - i) - 0.5 Position.ABOVE -> bottom + i Position.BELOW -> top - 1 - i - is Position.MOUSE -> continue + Position.MOUSE -> continue + is Position.MOUSE_OFFSET -> continue }, when (position) { Position.LEFT -> right + 1 + i Position.RIGHT -> left - i Position.ABOVE -> hCenter + (notchSize - i) + 0.5 Position.BELOW -> hCenter + (notchSize - i) + 0.5 - is Position.MOUSE -> continue + Position.MOUSE -> continue + is Position.MOUSE_OFFSET -> continue }, when (position) { Position.LEFT -> vCenter + (notchSize - i) + 0.5 Position.RIGHT -> vCenter + (notchSize - i) + 0.5 Position.ABOVE -> bottom + i + 1 Position.BELOW -> top - i - is Position.MOUSE -> continue + Position.MOUSE -> continue + is Position.MOUSE_OFFSET -> continue }, ) } @@ -329,7 +337,8 @@ class EssentialTooltip( data object RIGHT : Position data object ABOVE : Position data object BELOW : Position - data class MOUSE(val xOffset: Float = 0f, val yOffset: Float = 0f) : Position + data object MOUSE : Position + data class MOUSE_OFFSET(val xOffset: Float = 0f, val yOffset: Float = 0f) : Position } } diff --git a/gui/essential/src/main/kotlin/gg/essential/gui/wardrobe/Item.kt b/gui/essential/src/main/kotlin/gg/essential/gui/wardrobe/Item.kt index 813e720..f399e04 100644 --- a/gui/essential/src/main/kotlin/gg/essential/gui/wardrobe/Item.kt +++ b/gui/essential/src/main/kotlin/gg/essential/gui/wardrobe/Item.kt @@ -42,7 +42,7 @@ sealed interface Item { fun getCost(wardrobeState: WardrobeState): State = getPricingInfo(wardrobeState).map { it?.realCost } - data class CosmeticOrEmote(val cosmetic: Cosmetic, val settingsOverride: List? = null) : Item { + data class CosmeticOrEmote(val cosmetic: Cosmetic, val settingsOverride: List = emptyList()) : Item { override val id: String get() = cosmetic.id override val itemId: ItemId diff --git a/gui/essential/src/main/kotlin/gg/essential/gui/wardrobe/WardrobeState.kt b/gui/essential/src/main/kotlin/gg/essential/gui/wardrobe/WardrobeState.kt index 76d3e1f..f3215f4 100644 --- a/gui/essential/src/main/kotlin/gg/essential/gui/wardrobe/WardrobeState.kt +++ b/gui/essential/src/main/kotlin/gg/essential/gui/wardrobe/WardrobeState.kt @@ -55,6 +55,7 @@ import gg.essential.network.connectionmanager.skins.SkinsManager import gg.essential.network.cosmetics.Cosmetic import gg.essential.gui.util.pollingStateV2 import gg.essential.mod.cosmetics.featured.FeaturedItem +import gg.essential.mod.cosmetics.featured.FeaturedItemRow import gg.essential.mod.cosmetics.settings.CosmeticSettings import gg.essential.mod.cosmetics.settings.setting import gg.essential.network.connectionmanager.cosmetics.EmoteWheelManager @@ -321,19 +322,16 @@ class WardrobeState( /** Show purchase animation if true. **/ val purchaseAnimationState = mutableStateOf(false) - val draggingEmoteSlot = mutableStateOf(null).apply { + /** The [DraggedEmoteInfo] data for the emote currently being dragged or null if no emote is being dragged. */ + val draggingEmote = mutableStateOf(null).apply { onChange(component) { inEmoteWheel.set(true) } } - /** Slot that a drag&drop is currently hovering on top of. `-1` for "Remove". */ - val draggingOntoEmoteSlot = mutableStateOf(null) - - val emoteWheel = emoteWheelManager.selectedEmoteWheelSlots - - val draggingOntoOccupiedEmoteSlot = - draggingOntoEmoteSlot.zip(emoteWheel).map { (slot, wheel) -> - slot != null && wheel.getOrNull(slot) != null - } + val draggingOntoOccupiedEmoteSlot = memo { + val dragged = draggingEmote() + dragged?.to != null && dragged.to != dragged.from && + emoteWheelManager.getEmoteWheel(dragged.to.emoteWheelId)?.slots?.getOrNull(dragged.to.slotIndex) != null + } val equippedOutfitId = outfitManager.selectedOutfitId @@ -432,7 +430,7 @@ class WardrobeState( return@memo listOf() } - layoutStateObject.second?.value?.rows?.flatten()?.mapNotNull { + layoutStateObject.second?.value?.rows?.filterIsInstance()?.flatMap { it.items }?.mapNotNull { when (it) { is FeaturedItem.Bundle -> it.bundle is FeaturedItem.Cosmetic -> it.cosmetic @@ -687,6 +685,22 @@ class WardrobeState( data class CosmeticWithSortInfo(val cosmetic: Cosmetic, val owned: Boolean, val price: Int?, val collection: CosmeticCategory?) + /** The [emoteWheelId] and [slotIndex] for an emote in an emote wheel. */ + data class EmoteSlotId(val emoteWheelId: String, val slotIndex: Int) + /** + * The [EmoteSlotId] for an emote being dragged. + * A null value for [from] indicates the emote originated from the Wardrobe container. + * A null value for [to] indicates the emote's destination is the Wardrobe container and that it should be removed. + * A same value for [from] and [to] indicate the emote currently has no destination and that it should not be removed. + */ + data class DraggedEmote( + val emoteId: CosmeticId? = null, + val from: EmoteSlotId? = null, + val to: EmoteSlotId? = null, + val clickOffset: Pair = Pair(0f, 0f), + val onInstantLeftClick: () -> Unit = {}, + ) + companion object { private val LOGGER = LoggerFactory.getLogger(WardrobeState::class.java) diff --git a/gui/essential/src/main/kotlin/gg/essential/gui/wardrobe/configuration/FeaturedPageCollectionConfiguration.kt b/gui/essential/src/main/kotlin/gg/essential/gui/wardrobe/configuration/FeaturedPageCollectionConfiguration.kt index 6a5dd16..36be17c 100644 --- a/gui/essential/src/main/kotlin/gg/essential/gui/wardrobe/configuration/FeaturedPageCollectionConfiguration.kt +++ b/gui/essential/src/main/kotlin/gg/essential/gui/wardrobe/configuration/FeaturedPageCollectionConfiguration.kt @@ -18,6 +18,7 @@ import gg.essential.gui.common.EssentialDropDown import gg.essential.gui.common.compactFullEssentialToggle import gg.essential.gui.common.input.StateTextInput import gg.essential.gui.common.input.essentialStateTextInput +import gg.essential.gui.common.input.essentialStringInput import gg.essential.gui.common.modal.CancelableInputModal import gg.essential.gui.common.modal.configure import gg.essential.gui.elementa.state.v2.* @@ -30,9 +31,15 @@ import gg.essential.gui.wardrobe.configuration.ConfigurationUtils.labeledRow import gg.essential.gui.wardrobe.configuration.ConfigurationUtils.navButton import gg.essential.gui.wardrobe.configuration.cosmetic.settings.* import gg.essential.mod.cosmetics.CosmeticBundle +import gg.essential.mod.cosmetics.featured.BaseDivider +import gg.essential.mod.cosmetics.featured.BlankDivider +import gg.essential.mod.cosmetics.featured.DividerType +import gg.essential.mod.cosmetics.featured.FeaturedPageComponent import gg.essential.mod.cosmetics.featured.FeaturedItem +import gg.essential.mod.cosmetics.featured.FeaturedItemRow import gg.essential.mod.cosmetics.featured.FeaturedPage import gg.essential.mod.cosmetics.featured.FeaturedPageCollection +import gg.essential.mod.cosmetics.featured.TextDivider import gg.essential.mod.cosmetics.settings.CosmeticSettingType import gg.essential.network.cosmetics.Cosmetic import gg.essential.universal.USound @@ -113,45 +120,165 @@ class FeaturedPageCollectionConfiguration( val layout: FeaturedPage ) : AbstractConfigurationSubmenu(id, name, pageCollection) { + private val referenceHolder = ReferenceHolderImpl() + override fun LayoutScope.layout(modifier: Modifier) { val rows = layout.rows - fun update(builder: MutableList>.() -> Unit) { - val mutableRows = rows.toMutableList() - builder(mutableRows) - currentlyEditing.update(currentlyEditing.copy(pages = currentlyEditing.pages + (width to layout.copy(rows = mutableRows)))) + for ((componentIndex, component) in rows.withIndex()) { + when (component) { + is FeaturedItemRow -> rowConfiguration(componentIndex, component) + is BaseDivider -> dividerConfiguration(componentIndex, component) + } + } + navButton("Add Row") { + USound.playButtonPress() + update { + add(FeaturedItemRow(emptyList())) + } + } + navButton("Add Divider") { + USound.playButtonPress() + update { + add(BlankDivider) + } + } + val optionList = mutableListOf>(EssentialDropDown.Option("", null)) + optionList += pageCollection.pages.keys.filter { it != width }.map { EssentialDropDown.Option("$it-wide", it) } + + if (optionList.size > 1) { + divider() + labeledListInputRow("Copy from page:", null, stateOf(optionList).toListState()) { + val featuredPage = pageCollection.pages[it] ?: return@labeledListInputRow // Should never happen + pageCollection.update(pageCollection.copy(pages = pageCollection.pages + (width to featuredPage))) + } + } + } + + fun update(builder: MutableList.() -> Unit) { + val mutableRows = layout.rows.toMutableList() + builder(mutableRows) + currentlyEditing.update(currentlyEditing.copy(pages = currentlyEditing.pages + (width to layout.copy(rows = mutableRows)))) + } + + fun updateRow(componentIndex: Int, builder: MutableList.() -> Unit) { + val mutableRows = layout.rows.toMutableList() + val mutableRow = (mutableRows[componentIndex] as FeaturedItemRow).items.toMutableList() + builder(mutableRow) + mutableRows[componentIndex] = FeaturedItemRow(mutableRow.toList()) + currentlyEditing.update(currentlyEditing.copy(pages = currentlyEditing.pages + (width to layout.copy(rows = mutableRows)))) + } + + fun updateComponent(componentIndex: Int, builder: FeaturedPageComponent.() -> FeaturedPageComponent) { + val mutableRows = layout.rows.toMutableList() + val mutableRow = mutableRows[componentIndex] + mutableRows[componentIndex] = builder(mutableRow) + currentlyEditing.update(currentlyEditing.copy(pages = currentlyEditing.pages + (width to layout.copy(rows = mutableRows)))) + } + + private fun LayoutScope.configurationTitle(title: String, componentIndex: Int, component: FeaturedPageComponent) { + val rows = layout.rows + row(Modifier.fillWidth().height(10f).color(EssentialPalette.LIGHT_SCROLLBAR), Arrangement.SpaceBetween) { + text("$title " + (componentIndex + 1)) + row(Modifier.fillHeight()) { + box(Modifier.widthAspect(1f).fillHeight()) { + icon(EssentialPalette.ARROW_UP_7X5) + }.onLeftClick { + if (componentIndex > 0) { + USound.playButtonPress() + update { + removeAt(componentIndex) + add(componentIndex - 1, component) + } + } + } + box(Modifier.widthAspect(1f).fillHeight()) { + icon(EssentialPalette.ARROW_DOWN_7X5) + }.onLeftClick { + if (componentIndex + 1 < rows.size) { + USound.playButtonPress() + update { + removeAt(componentIndex) + add(componentIndex + 1, component) + } + } + } + box(Modifier.widthAspect(1f).fillHeight()) { + icon(EssentialPalette.CANCEL_5X) + }.onLeftClick { + USound.playButtonPress() + update { + removeAt(componentIndex) + } + } + } + } + } + + private fun LayoutScope.dividerConfiguration(componentIndex: Int, baseDivider: BaseDivider) { + configurationTitle("Divider", componentIndex, baseDivider) + + val optionList = listOf( + EssentialDropDown.Option("Blank", DividerType.BLANK), + EssentialDropDown.Option("Text", DividerType.TEXT) + ) + val currentDivider = when (baseDivider) { + is TextDivider -> DividerType.TEXT + is BlankDivider -> DividerType.BLANK + } + + labeledListInputRow("Type:", currentDivider, stateOf(optionList).toListState()) { option -> + updateComponent(componentIndex) { + when (option) { + DividerType.TEXT -> TextDivider("") + DividerType.BLANK -> BlankDivider + } + } } - fun updateRow(rowIndex: Int, builder: MutableList.() -> Unit) { - val mutableRows = rows.map { it.toMutableList() }.toMutableList() - val mutableRow = mutableRows[rowIndex] - builder(mutableRow) - currentlyEditing.update(currentlyEditing.copy(pages = currentlyEditing.pages + (width to layout.copy(rows = mutableRows)))) + if (baseDivider is TextDivider) { + val textState = mutableStateOf(baseDivider.text).apply { + onChange(referenceHolder) { text -> + updateComponent(componentIndex) { TextDivider(text) } + } + } + row(Modifier.fillWidth(), Arrangement.spacedBy(10f, FloatPosition.END)) { + text("Text:") + essentialStringInput(textState) + } } + } - for ((rowIndex, row) in rows.withIndex()) { - row(Modifier.fillWidth().height(10f).color(EssentialPalette.LIGHT_SCROLLBAR), Arrangement.SpaceBetween) { - text("Row " + (rowIndex + 1)) + private fun LayoutScope.rowConfiguration(componentIndex: Int, row: FeaturedItemRow) { + configurationTitle("Row", componentIndex, row) + for ((itemIndex, item) in row.items.withIndex()) { + row(Modifier.fillWidth().height(10f), Arrangement.SpaceBetween) { + val text = "- " + when (item) { + is FeaturedItem.Bundle -> "Bundle: " + item.bundle + is FeaturedItem.Cosmetic -> "Cosmetic: " + item.cosmetic + is FeaturedItem.Empty -> "Empty" + } + text(text) row(Modifier.fillHeight()) { box(Modifier.widthAspect(1f).fillHeight()) { icon(EssentialPalette.ARROW_UP_7X5) }.onLeftClick { - if (rowIndex > 0) { + if (itemIndex > 0) { USound.playButtonPress() - update { - removeAt(rowIndex) - add(rowIndex - 1, row) + updateRow(componentIndex) { + removeAt(itemIndex) + add(itemIndex - 1, item) } } } box(Modifier.widthAspect(1f).fillHeight()) { icon(EssentialPalette.ARROW_DOWN_7X5) }.onLeftClick { - if (rowIndex + 1 < rows.size) { + if (itemIndex + 1 < row.items.size) { USound.playButtonPress() - update { - removeAt(rowIndex) - add(rowIndex + 1, row) + updateRow(componentIndex) { + removeAt(itemIndex) + add(itemIndex + 1, item) } } } @@ -159,116 +286,58 @@ class FeaturedPageCollectionConfiguration( icon(EssentialPalette.CANCEL_5X) }.onLeftClick { USound.playButtonPress() - update { - removeAt(rowIndex) + updateRow(componentIndex) { + removeAt(itemIndex) } } } } - for ((itemIndex, item) in row.withIndex()) { - row(Modifier.fillWidth().height(10f), Arrangement.SpaceBetween) { - val text = "- " + when (item) { - is FeaturedItem.Bundle -> "Bundle: " + item.bundle - is FeaturedItem.Cosmetic -> "Cosmetic: " + item.cosmetic - is FeaturedItem.Empty -> "Empty" - } - text(text) - row(Modifier.fillHeight()) { - box(Modifier.widthAspect(1f).fillHeight()) { - icon(EssentialPalette.ARROW_UP_7X5) - }.onLeftClick { - if (itemIndex > 0) { - USound.playButtonPress() - updateRow(rowIndex) { - removeAt(itemIndex) - add(itemIndex - 1, item) - } - } - } - box(Modifier.widthAspect(1f).fillHeight()) { - icon(EssentialPalette.ARROW_DOWN_7X5) - }.onLeftClick { - if (itemIndex + 1 < row.size) { - USound.playButtonPress() - updateRow(rowIndex) { - removeAt(itemIndex) - add(itemIndex + 1, item) - } - } - } - box(Modifier.widthAspect(1f).fillHeight()) { - icon(EssentialPalette.CANCEL_5X) - }.onLeftClick { - USound.playButtonPress() - updateRow(rowIndex) { - removeAt(itemIndex) - } - } + if (item is FeaturedItem.Cosmetic) { + val cosmeticId = item.cosmetic + val settings = mutableListStateOf(*item.settings.toTypedArray()) + for (settingType in CosmeticSettingType.values()) { + val component = when (settingType) { + CosmeticSettingType.PLAYER_POSITION_ADJUSTMENT -> PlayerPositionAdjustmentSettingConfiguration(cosmeticsDataWithChanges, state.modelLoader, cosmeticId, settings) + CosmeticSettingType.SIDE -> SideSettingConfiguration(cosmeticsDataWithChanges, state.modelLoader, cosmeticId, settings) + CosmeticSettingType.VARIANT -> VariantSettingConfiguration(cosmeticsDataWithChanges, state.modelLoader, cosmeticId, settings) } + component() } - if (item is FeaturedItem.Cosmetic) { - val cosmeticId = item.cosmetic - val settings = mutableListStateOf(*item.settings.toTypedArray()) - for (settingType in CosmeticSettingType.values()) { - val component = when (settingType) { - CosmeticSettingType.PLAYER_POSITION_ADJUSTMENT -> PlayerPositionAdjustmentSettingConfiguration(cosmeticsDataWithChanges, state.modelLoader, cosmeticId, settings) - CosmeticSettingType.SIDE -> SideSettingConfiguration(cosmeticsDataWithChanges, state.modelLoader, cosmeticId, settings) - CosmeticSettingType.VARIANT -> VariantSettingConfiguration(cosmeticsDataWithChanges, state.modelLoader, cosmeticId, settings) - } - component() - } - settings.onSetValue(stateScope) { - updateRow(rowIndex) { - this[itemIndex] = item.copy(settings = it) - } - } - } - } - row(Modifier.fillWidth(), Arrangement.spacedBy(10f, FloatPosition.END)) { - val bundleState = mutableStateOf(null) - text("Add Bundle:") - essentialStateTextInput( - bundleState, - { it?.id ?: "" }, - { if (it.isBlank()) null else (cosmeticsDataWithChanges.getCosmeticBundle(it) ?: throw StateTextInput.ParseException()) } - ) - bundleState.onSetValue(stateScope) { - val bundleId = (it ?: return@onSetValue).id - updateRow(rowIndex) { - add(FeaturedItem.Bundle(bundleId)) - } - } - } - row(Modifier.fillWidth(), Arrangement.spacedBy(10f, FloatPosition.END)) { - val cosmeticState = mutableStateOf(null) - text("Add Cosmetic:") - essentialStateTextInput( - cosmeticState, - { it?.id ?: "" }, - { if (it.isBlank()) null else (cosmeticsDataWithChanges.getCosmetic(it) ?: throw StateTextInput.ParseException()) } - ) - cosmeticState.onSetValue(stateScope) { - val cosmeticId = (it ?: return@onSetValue).id - updateRow(rowIndex) { - add(FeaturedItem.Cosmetic(cosmeticId, listOf())) + settings.onSetValue(stateScope) { + updateRow(componentIndex) { + this[itemIndex] = item.copy(settings = it) } } } } - navButton("Add Row") { - USound.playButtonPress() - update { - add(listOf()) + row(Modifier.fillWidth(), Arrangement.spacedBy(10f, FloatPosition.END)) { + val bundleState = mutableStateOf(null) + text("Add Bundle:") + essentialStateTextInput( + bundleState, + { it?.id ?: "" }, + { if (it.isBlank()) null else (cosmeticsDataWithChanges.getCosmeticBundle(it) ?: throw StateTextInput.ParseException()) } + ) + bundleState.onSetValue(stateScope) { + val bundleId = (it ?: return@onSetValue).id + updateRow(componentIndex) { + add(FeaturedItem.Bundle(bundleId)) + } } } - val optionList = mutableListOf>(EssentialDropDown.Option("", null)) - optionList += pageCollection.pages.keys.filter { it != width }.map { EssentialDropDown.Option("$it-wide", it) } - - if (optionList.size > 1) { - divider() - labeledListInputRow("Copy from page:", null, stateOf(optionList).toListState()) { - val featuredPage = pageCollection.pages[it] ?: return@labeledListInputRow // Should never happen - pageCollection.update(pageCollection.copy(pages = pageCollection.pages + (width to featuredPage))) + row(Modifier.fillWidth(), Arrangement.spacedBy(10f, FloatPosition.END)) { + val cosmeticState = mutableStateOf(null) + text("Add Cosmetic:") + essentialStateTextInput( + cosmeticState, + { it?.id ?: "" }, + { if (it.isBlank()) null else (cosmeticsDataWithChanges.getCosmetic(it) ?: throw StateTextInput.ParseException()) } + ) + cosmeticState.onSetValue(stateScope) { + val cosmeticId = (it ?: return@onSetValue).id + updateRow(componentIndex) { + add(FeaturedItem.Cosmetic(cosmeticId, listOf())) + } } } } diff --git a/gui/essential/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/EmoteWheelManager.kt b/gui/essential/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/EmoteWheelManager.kt index 6c98b02..c602a0d 100644 --- a/gui/essential/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/EmoteWheelManager.kt +++ b/gui/essential/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/EmoteWheelManager.kt @@ -45,6 +45,7 @@ class EmoteWheelManager( emoteWheels().sortedBy { it.createdAt.toEpochMilli() } } private val mutableSelectedEmoteWheelId: MutableState = mutableStateOf(null) + val selectedEmoteWheelId: State = mutableSelectedEmoteWheelId val selectedEmoteWheel = memo { orderedEmoteWheels().firstOrNull { it.id == mutableSelectedEmoteWheelId() } } val selectedEmoteWheelIndex = memo { orderedEmoteWheels().indexOfFirst { it.id == mutableSelectedEmoteWheelId() } } val selectedEmoteWheelSlots = memo { @@ -65,6 +66,10 @@ class EmoteWheelManager( sentEmoteWheelId = null } + fun getEmoteWheel(id: String): EmoteWheelPage? { + return emoteWheels.getUntracked().find { it.id == id } + } + fun selectEmoteWheel(id: String) { val emoteWheel = orderedEmoteWheels.getUntracked().find { it.id == id } ?: return mutableSelectedEmoteWheelId.set(emoteWheel.id) @@ -117,15 +122,27 @@ class EmoteWheelManager( connectionManager.call(ClientCosmeticEmoteWheelSelectPacket(selectedEmoteWheelId)).fireAndForget() } + /** + * Sets one of the saved emotes for an emote wheel. + * + * @param emoteWheelId The emote wheel id of the emote wheel to change + * @param slotIndex The id of slot in the emote wheel to change + * @param emoteId The (CosmeticId) emote id to save + */ + fun setEmote(emoteWheelId: String, slotIndex: Int, emoteId: CosmeticId?) { + val emoteWheel = emoteWheels.getUntracked().find { it.id == emoteWheelId } ?: return + setEmotes(emoteWheelId, emoteWheel.slots.toMutableList().apply { this[slotIndex] = emoteId }) + } /** - * Sets the saved emotes for the emote wheel + * Sets the saved emotes for an emote wheel * + * @param emoteWheelId The emote wheel id of the emote wheel to change * @param emotes The (CosmeticId) list of emotes to save */ - fun setEmotes(emotes: List) { - val selectedEmoteWheel = selectedEmoteWheel.getUntracked() ?: return - val slots = selectedEmoteWheelSlots.getUntracked().toMutableList() + fun setEmotes(emoteWheelId: String, emotes: List) { + val emoteWheel = emoteWheels.getUntracked().find { it.id == emoteWheelId } ?: return + val slots = emoteWheel.slots.toMutableList() val unlockedCosmetics = unlockedCosmetics.getUntracked() for ((i, value) in emotes.withIndex()) { if (value != null && !unlockedCosmetics.contains(value)) { @@ -133,17 +150,10 @@ class EmoteWheelManager( } if (value != slots.set(i, value)) { - connectionManager.call(ClientCosmeticEmoteWheelUpdatePacket(selectedEmoteWheel.id, i, value)).fireAndForget() + connectionManager.call(ClientCosmeticEmoteWheelUpdatePacket(emoteWheel.id, i, value)).fireAndForget() } } - editEmoteWheel(selectedEmoteWheel.copy(slots = slots.toList())) - } - - /** - * Changes one of the emotes saved for the emote wheel. - */ - fun setEmote(slotIndex: Int, emoteId: String?) { - setEmotes(ArrayList(selectedEmoteWheelSlots.getUntracked()).apply { this[slotIndex] = emoteId }) + editEmoteWheel(emoteWheel.copy(slots = slots.toList())) } private fun editEmoteWheel(new: EmoteWheelPage) { diff --git a/gui/essential/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/InfraCosmeticsData.kt b/gui/essential/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/InfraCosmeticsData.kt index 21d2d6c..03e3bea 100644 --- a/gui/essential/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/InfraCosmeticsData.kt +++ b/gui/essential/src/main/kotlin/gg/essential/network/connectionmanager/cosmetics/InfraCosmeticsData.kt @@ -30,6 +30,7 @@ import gg.essential.mod.cosmetics.settings.CosmeticProperty import gg.essential.mod.cosmetics.CosmeticSlot import gg.essential.mod.cosmetics.CosmeticType import gg.essential.mod.cosmetics.featured.FeaturedItem +import gg.essential.mod.cosmetics.featured.FeaturedItemRow import gg.essential.network.CMConnection import gg.essential.network.cosmetics.toMod import gg.essential.util.Client @@ -169,7 +170,7 @@ class InfraCosmeticsData private constructor( if (collection == null) return@whenCompleteAsync - val featuredItems = collection.pages.values.flatMap { it.rows }.flatten() + val featuredItems = collection.pages.values.flatMap { it.rows }.filterIsInstance().flatMap { it.items } requestCosmeticsIfMissing(featuredItems.filterIsInstance().map { it.cosmetic }.toSet()) requestBundlesIfMissing(featuredItems.filterIsInstance().map { it.bundle }.toSet()) diff --git a/gui/essential/src/main/kotlin/gg/essential/util/essentialGuiExtensions.kt b/gui/essential/src/main/kotlin/gg/essential/util/essentialGuiExtensions.kt index 4482d2b..35f341a 100644 --- a/gui/essential/src/main/kotlin/gg/essential/util/essentialGuiExtensions.kt +++ b/gui/essential/src/main/kotlin/gg/essential/util/essentialGuiExtensions.kt @@ -39,6 +39,7 @@ import gg.essential.gui.layoutdsl.* import gg.essential.gui.util.hoveredState import gg.essential.gui.util.isComponentInParentChain import gg.essential.gui.util.stateBy +import gg.essential.universal.UMouse import gg.essential.vigilance.utils.onLeftClick import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.asExecutor @@ -162,23 +163,38 @@ private fun UIComponent.positionTooltip( EssentialTooltip.Position.LEFT -> SiblingConstraint(padding = padding, alignOpposite = true) EssentialTooltip.Position.RIGHT -> SiblingConstraint(padding = padding) EssentialTooltip.Position.ABOVE, EssentialTooltip.Position.BELOW -> CenterConstraint() - is EssentialTooltip.Position.MOUSE -> MousePositionConstraint() + EssentialTooltip.Position.MOUSE -> MousePositionConstraint() + is EssentialTooltip.Position.MOUSE_OFFSET -> MousePositionConstraint() } boundTo this@positionTooltip var yConstraint: YConstraint = when (position) { EssentialTooltip.Position.LEFT, EssentialTooltip.Position.RIGHT -> CenterConstraint() EssentialTooltip.Position.ABOVE -> SiblingConstraint(padding = padding, alignOpposite = true) EssentialTooltip.Position.BELOW -> SiblingConstraint(padding = padding) - is EssentialTooltip.Position.MOUSE -> MousePositionConstraint() + EssentialTooltip.Position.MOUSE -> MousePositionConstraint() + is EssentialTooltip.Position.MOUSE_OFFSET -> MousePositionConstraint() } boundTo this@positionTooltip // Since an additive constraint can't be boundTo - if(position is EssentialTooltip.Position.MOUSE) { + if (position is EssentialTooltip.Position.MOUSE_OFFSET) { xConstraint += position.xOffset.pixels yConstraint += position.yOffset.pixels } - if (windowPadding != null) { + if (position is EssentialTooltip.Position.MOUSE) { + val xPadding = 7f + val yPadding = 16f + xConstraint += basicXConstraint { + if (Window.of(tooltip).getRight() - UMouse.Scaled.x <= tooltip.getWidth() + xPadding + (windowPadding ?: 0f)) { + -tooltip.getWidth() - xPadding + } else { + xPadding + } + } + yConstraint -= yPadding.pixels + } + + if (windowPadding != null && position !is EssentialTooltip.Position.MOUSE) { val minConstraint = lazyPosition { windowPadding.pixels boundTo Window.of(this) } val maxConstraint = lazyPosition { windowPadding.pixels(alignOpposite = true) boundTo Window.of(this) } diff --git a/src/main/java/gg/essential/cosmetics/EssentialModelRenderer.java b/src/main/java/gg/essential/cosmetics/EssentialModelRenderer.java index 6d222e4..cde9dd5 100644 --- a/src/main/java/gg/essential/cosmetics/EssentialModelRenderer.java +++ b/src/main/java/gg/essential/cosmetics/EssentialModelRenderer.java @@ -11,6 +11,8 @@ */ package gg.essential.cosmetics; +import gg.essential.config.EssentialConfig; +import gg.essential.mixins.impl.client.gui.GuiInventoryExt; import gg.essential.model.EnumPart; import gg.essential.model.ModelInstance; import gg.essential.model.backend.PlayerPose; @@ -55,10 +57,6 @@ public class EssentialModelRenderer implements LayerRenderer { //#endif - /** - * Flag to skip cosmetic rendering - */ - public static boolean suppressCosmeticRendering = false; private final RenderPlayer playerRenderer; public EssentialModelRenderer(RenderPlayer playerRenderer) { @@ -69,7 +67,8 @@ public EssentialModelRenderer(RenderPlayer playerRenderer) { } public static boolean shouldRender(AbstractClientPlayer player) { - if (suppressCosmeticRendering) { + if (GuiInventoryExt.isInventoryEntityRendering.getUntracked() + && EssentialConfig.INSTANCE.getDisableCosmeticsInInventory()) { return false; } diff --git a/src/main/java/gg/essential/cosmetics/events/CosmeticEventEmitter.java b/src/main/java/gg/essential/cosmetics/events/CosmeticEventEmitter.java index 1da4436..7f93574 100644 --- a/src/main/java/gg/essential/cosmetics/events/CosmeticEventEmitter.java +++ b/src/main/java/gg/essential/cosmetics/events/CosmeticEventEmitter.java @@ -51,7 +51,7 @@ public void triggerEvent(UUID playerUuid, CosmeticSlot slot, String event) { player.getWearablesManager().getModels(); for (ModelInstance value : essentialCosmeticModels.values()) { if (slot == value.getCosmetic().getType().getSlot()) { - value.getEssentialAnimationSystem().fireTriggerFromAnimation(event); + value.getEssentialAnimationSystem().fireTriggerFromAnimation(event, null); } } } diff --git a/src/main/java/gg/essential/handlers/OnlineIndicator.java b/src/main/java/gg/essential/handlers/OnlineIndicator.java index 2537dde..822bf9d 100644 --- a/src/main/java/gg/essential/handlers/OnlineIndicator.java +++ b/src/main/java/gg/essential/handlers/OnlineIndicator.java @@ -41,6 +41,8 @@ //$$ import net.minecraft.client.renderer.RenderType; //$$ import net.minecraft.util.ResourceLocation; //$$ import net.minecraft.client.renderer.IRenderTypeBuffer; +//#else +import net.minecraft.util.ResourceLocation; //#endif import java.awt.*; @@ -73,6 +75,10 @@ public static boolean currentlyDrawingEntityName() { //#endif } + //#if MC<11600 + private static final ResourceLocation whiteTexture = new ResourceLocation("essential", "textures/white.png"); + //#endif + public static void drawNametagIndicator( UMatrixStack matrixStack, //#if MC>=11600 @@ -128,7 +134,10 @@ public static void drawNametagIndicator( //$$ TextRenderTypeVertexConsumer vertexConsumer = TextRenderTypeVertexConsumer.create(vertexConsumerProvider, alwaysOnTop); //#else UGraphics buffer = UGraphics.getFromTessellator(); - TextRenderTypeVertexConsumer vertexConsumer = TextRenderTypeVertexConsumer.create(buffer); + + // use createWithTexture() for under 1.16 to allow for the light map to apply, we also pass in a blank white texture + // @see createWithTexture() doc for details + TextRenderTypeVertexConsumer vertexConsumer = TextRenderTypeVertexConsumer.createWithTexture(buffer, whiteTexture); //#endif vertexConsumer.pos(matrixStack, x1, y1, z).color(0, 0, 0, backgroundOpacity).tex(0, 0).light(light).endVertex(); @@ -157,7 +166,8 @@ public static void drawNametagIndicator( //#if MC>=11600 //$$ vertexConsumer = TextRenderTypeVertexConsumer.create(vertexConsumerProvider, false); //#else - vertexConsumer = TextRenderTypeVertexConsumer.create(buffer); + // use createWithTexture() for under 1.16 same reason as above + vertexConsumer = TextRenderTypeVertexConsumer.createWithTexture(buffer, whiteTexture); //#endif Diamond.drawDiamond(matrixStack, vertexConsumer, 6, vanillaX - 6, diamondCenter, color.getRGB(), light); diff --git a/src/main/java/gg/essential/key/EssentialKeybindingRegistry.java b/src/main/java/gg/essential/key/EssentialKeybindingRegistry.java index 580689f..3148d00 100644 --- a/src/main/java/gg/essential/key/EssentialKeybindingRegistry.java +++ b/src/main/java/gg/essential/key/EssentialKeybindingRegistry.java @@ -136,7 +136,10 @@ public KeyBinding[] registerKeyBinds(KeyBinding[] allBindings) { int cosmeticToggleKey = UKeyboard.KEY_NONE; cosmetics_visibility_toggle = new EssentialKeybinding("COSMETICS_VISIBILITY_TOGGLE", CATEGORY, cosmeticToggleKey, false).withInitialPress(() -> { - if (OverlayManagerImpl.INSTANCE.getFocusedLayer() == null && !(OverlayManagerImpl.INSTANCE.getHoveredLayer() instanceof EphemeralLayer)) { + if (OverlayManagerImpl.INSTANCE.getFocusedLayer() == null + && !(OverlayManagerImpl.INSTANCE.getHoveredLayer() instanceof EphemeralLayer) + && !EssentialConfig.INSTANCE.getDisableCosmetics() + ) { Essential.getInstance().getConnectionManager().getCosmeticsManager().toggleOwnCosmeticVisibility(true); } }); diff --git a/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/featured/FeaturedPage.kt b/src/main/java/gg/essential/mixins/impl/client/gui/GuiInventoryExt.java similarity index 64% rename from subprojects/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/featured/FeaturedPage.kt rename to src/main/java/gg/essential/mixins/impl/client/gui/GuiInventoryExt.java index 2a56057..3503a33 100644 --- a/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/featured/FeaturedPage.kt +++ b/src/main/java/gg/essential/mixins/impl/client/gui/GuiInventoryExt.java @@ -9,11 +9,14 @@ * commercialize, or otherwise exploit, or create derivative works based * upon, this file or any other in this repository, all of which is reserved by Essential. */ -package gg.essential.mod.cosmetics.featured +package gg.essential.mixins.impl.client.gui; -import kotlinx.serialization.Serializable +import gg.essential.gui.elementa.state.v2.MutableState; -@Serializable -data class FeaturedPage( - val rows: List>, -) +import static gg.essential.gui.elementa.state.v2.StateKt.mutableStateOf; + +public interface GuiInventoryExt { + + MutableState isInventoryEntityRendering = mutableStateOf(false); + +} diff --git a/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_AddSPSTitle.java b/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_AddSPSTitle.java new file mode 100644 index 0000000..8b49fa4 --- /dev/null +++ b/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_AddSPSTitle.java @@ -0,0 +1,41 @@ +/* + * Copyright (c) 2024 ModCore Inc. All rights reserved. + * + * This code is part of ModCore Inc.'s Essential Mod repository and is protected + * under copyright registration # TX0009138511. For the full license, see: + * https://github.com/EssentialGG/Essential/blob/main/LICENSE + * + * You may not use, copy, reproduce, modify, sell, license, distribute, + * commercialize, or otherwise exploit, or create derivative works based + * upon, this file or any other in this repository, all of which is reserved by Essential. + */ +package gg.essential.mixins.transformers.client.gui; + +import com.llamalad7.mixinextras.injector.ModifyExpressionValue; +import gg.essential.config.EssentialConfig; +import gg.essential.util.ServerType; +import net.minecraft.client.Minecraft; +import org.spongepowered.asm.mixin.Mixin; +import org.spongepowered.asm.mixin.injection.At; + +@Mixin(Minecraft.class) +public abstract class Mixin_UpdateWindowTitle_AddSPSTitle { + + //#if MC>=11600 + //$$ @ModifyExpressionValue(method = "getWindowTitle", at = @At(value = "CONSTANT", args = "stringValue=title.multiplayer.other")) + //$$ public String modifyMultiplayerWindowTitleOther(String original) { + //$$ if (EssentialConfig.INSTANCE.getReplaceWindowTitle() && ServerType.Companion.current() instanceof ServerType.SPS) { + //$$ return "title.multiplayer.hosted"; + //$$ } + //$$ return original; + //$$ } + //$$ + //$$ @ModifyExpressionValue(method = "getWindowTitle", at = @At(value = "CONSTANT", args = "stringValue=title.multiplayer.lan")) + //$$ public String modifyMultiplayerWindowTitleLAN(String original) { + //$$ if (EssentialConfig.INSTANCE.getReplaceWindowTitle() && ServerType.Companion.current() instanceof ServerType.SPS) { + //$$ return "title.multiplayer.hosted"; + //$$ } + //$$ return original; + //$$ } + //#endif +} \ No newline at end of file diff --git a/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_DisplayScreen.java b/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_DisplayScreen.java new file mode 100644 index 0000000..4b34a3f --- /dev/null +++ b/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_DisplayScreen.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 ModCore Inc. All rights reserved. + * + * This code is part of ModCore Inc.'s Essential Mod repository and is protected + * under copyright registration # TX0009138511. For the full license, see: + * https://github.com/EssentialGG/Essential/blob/main/LICENSE + * + * You may not use, copy, reproduce, modify, sell, license, distribute, + * commercialize, or otherwise exploit, or create derivative works based + * upon, this file or any other in this repository, all of which is reserved by Essential. + */ +package gg.essential.mixins.transformers.client.gui; + +import gg.essential.sps.WindowTitleManager; +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 Mixin_UpdateWindowTitle_DisplayScreen { + + //#if MC<11600 + @Inject(method = "displayGuiScreen", at = @At(value = "TAIL")) + private void onDisplayGuiScreen(CallbackInfo ci) { + WindowTitleManager.INSTANCE.updateTitle(); + } + //#endif +} diff --git a/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_LoadWorld.java b/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_LoadWorld.java new file mode 100644 index 0000000..00ef332 --- /dev/null +++ b/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_LoadWorld.java @@ -0,0 +1,35 @@ +/* + * Copyright (c) 2024 ModCore Inc. All rights reserved. + * + * This code is part of ModCore Inc.'s Essential Mod repository and is protected + * under copyright registration # TX0009138511. For the full license, see: + * https://github.com/EssentialGG/Essential/blob/main/LICENSE + * + * You may not use, copy, reproduce, modify, sell, license, distribute, + * commercialize, or otherwise exploit, or create derivative works based + * upon, this file or any other in this repository, all of which is reserved by Essential. + */ +package gg.essential.mixins.transformers.client.gui; + +import gg.essential.sps.WindowTitleManager; +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; + +//#if MC>=11600 +//#else +import net.minecraft.client.multiplayer.WorldClient; +//#endif + +@Mixin(Minecraft.class) +public abstract class Mixin_UpdateWindowTitle_LoadWorld { + + //#if MC<11600 + @Inject(method = "loadWorld(Lnet/minecraft/client/multiplayer/WorldClient;Ljava/lang/String;)V", at = @At(value = "RETURN")) + private void onLoadWorld(WorldClient worldClientIn, String loadingMessage, CallbackInfo ci) { + WindowTitleManager.INSTANCE.updateTitle(); + } + //#endif +} \ No newline at end of file diff --git a/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_OpenToLan.java b/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_OpenToLan.java new file mode 100644 index 0000000..36ef5ed --- /dev/null +++ b/src/main/java/gg/essential/mixins/transformers/client/gui/Mixin_UpdateWindowTitle_OpenToLan.java @@ -0,0 +1,30 @@ +/* + * Copyright (c) 2024 ModCore Inc. All rights reserved. + * + * This code is part of ModCore Inc.'s Essential Mod repository and is protected + * under copyright registration # TX0009138511. For the full license, see: + * https://github.com/EssentialGG/Essential/blob/main/LICENSE + * + * You may not use, copy, reproduce, modify, sell, license, distribute, + * commercialize, or otherwise exploit, or create derivative works based + * upon, this file or any other in this repository, all of which is reserved by Essential. + */ +package gg.essential.mixins.transformers.client.gui; + +import gg.essential.sps.WindowTitleManager; +import net.minecraft.client.gui.GuiShareToLan; +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(GuiShareToLan.class) +public class Mixin_UpdateWindowTitle_OpenToLan { + + //#if MC<11600 + @Inject(method = "actionPerformed", at = @At(value = "INVOKE", target = "Lnet/minecraft/server/integrated/IntegratedServer;shareToLAN(Lnet/minecraft/world/GameType;Z)Ljava/lang/String;", shift = At.Shift.AFTER)) + private void onActionPerformed(CallbackInfo ci) { + WindowTitleManager.INSTANCE.updateTitle(); + } + //#endif +} diff --git a/src/main/java/gg/essential/mixins/transformers/client/gui/inventory/Mixin_DisableCosmeticsInInventory.java b/src/main/java/gg/essential/mixins/transformers/client/gui/inventory/Mixin_TrackInventoryPlayerRendering.java similarity index 85% rename from src/main/java/gg/essential/mixins/transformers/client/gui/inventory/Mixin_DisableCosmeticsInInventory.java rename to src/main/java/gg/essential/mixins/transformers/client/gui/inventory/Mixin_TrackInventoryPlayerRendering.java index c49f771..eadb72a 100644 --- a/src/main/java/gg/essential/mixins/transformers/client/gui/inventory/Mixin_DisableCosmeticsInInventory.java +++ b/src/main/java/gg/essential/mixins/transformers/client/gui/inventory/Mixin_TrackInventoryPlayerRendering.java @@ -11,9 +11,8 @@ */ package gg.essential.mixins.transformers.client.gui.inventory; -import gg.essential.config.EssentialConfig; -import gg.essential.cosmetics.EssentialModelRenderer; import gg.essential.gui.common.UI3DPlayer; +import gg.essential.mixins.impl.client.gui.GuiInventoryExt; import net.minecraft.client.gui.inventory.GuiInventory; import org.spongepowered.asm.mixin.Mixin; import org.spongepowered.asm.mixin.injection.At; @@ -21,7 +20,7 @@ import org.spongepowered.asm.mixin.injection.callback.CallbackInfo; @Mixin(GuiInventory.class) -public class Mixin_DisableCosmeticsInInventory { +public class Mixin_TrackInventoryPlayerRendering { private static final String DRAW_ENTITY = //#if MC>=12005 @@ -39,11 +38,11 @@ public class Mixin_DisableCosmeticsInInventory { @Inject(method = DRAW_ENTITY, at = @At("HEAD")) private static void essential$disableCosmeticsInInventoryStart(CallbackInfo info) { // If UI3DPlayer.current is null then the method was not called while rendering a player display - EssentialModelRenderer.suppressCosmeticRendering = UI3DPlayer.current == null && EssentialConfig.INSTANCE.getDisableCosmeticsInInventory(); + GuiInventoryExt.isInventoryEntityRendering.set(UI3DPlayer.current == null); } @Inject(method = DRAW_ENTITY, at = @At("RETURN")) private static void essential$disableCosmeticsInInventoryCleanup(CallbackInfo info) { - EssentialModelRenderer.suppressCosmeticRendering = false; + GuiInventoryExt.isInventoryEntityRendering.set(false); } } diff --git a/src/main/java/gg/essential/network/connectionmanager/handler/chat/ServerChatChannelMessagePacketHandler.java b/src/main/java/gg/essential/network/connectionmanager/handler/chat/ServerChatChannelMessagePacketHandler.java index f52e625..fee5920 100644 --- a/src/main/java/gg/essential/network/connectionmanager/handler/chat/ServerChatChannelMessagePacketHandler.java +++ b/src/main/java/gg/essential/network/connectionmanager/handler/chat/ServerChatChannelMessagePacketHandler.java @@ -121,30 +121,34 @@ static class NotificationHandler implements Consumer { @Override public void accept(String name) { - String notificationTitle = channel.getType() == ChannelType.DIRECT_MESSAGE ? name : String.format(Locale.ROOT, "%s [%s]", name, channel.getName()); + boolean dm = channel.getType() == ChannelType.DIRECT_MESSAGE; - if (EssentialConfig.INSTANCE.getMessageSound() && !EssentialConfig.INSTANCE.getStreamerMode()) { - USound.INSTANCE.playExpSound(); - } + if((dm && EssentialConfig.INSTANCE.getMessageReceivedNotifications()) || (!dm && EssentialConfig.INSTANCE.getGroupMessageReceivedNotifications())) { + String notificationTitle = dm ? name : String.format(Locale.ROOT, "%s [%s]", name, channel.getName()); - Notifications.INSTANCE.push( - notificationTitle, - message.getContents(), - 4f, - () -> { - GuiUtil.openScreen(SocialMenu.class, () -> new SocialMenu(channel.getId())); - return Unit.INSTANCE; - }, - () -> Unit.INSTANCE, - (notificationBuilder) -> { - notificationBuilder.setTrimTitle(true); - notificationBuilder.setTrimMessage(true); - - notificationBuilder.withCustomComponent(Slot.ICON, CachedAvatarImage.create(message.getSender())); - - return Unit.INSTANCE; + if (EssentialConfig.INSTANCE.getMessageSound() && !EssentialConfig.INSTANCE.getStreamerMode()) { + USound.INSTANCE.playExpSound(); } - ); + + Notifications.INSTANCE.push( + notificationTitle, + message.getContents(), + 4f, + () -> { + GuiUtil.openScreen(SocialMenu.class, () -> new SocialMenu(channel.getId())); + return Unit.INSTANCE; + }, + () -> Unit.INSTANCE, + (notificationBuilder) -> { + notificationBuilder.setTrimTitle(true); + notificationBuilder.setTrimMessage(true); + + notificationBuilder.withCustomComponent(Slot.ICON, CachedAvatarImage.create(message.getSender())); + + return Unit.INSTANCE; + } + ); + } } } } diff --git a/src/main/java/gg/essential/network/connectionmanager/sps/SPSManager.java b/src/main/java/gg/essential/network/connectionmanager/sps/SPSManager.java index 690ab0e..4e586bc 100644 --- a/src/main/java/gg/essential/network/connectionmanager/sps/SPSManager.java +++ b/src/main/java/gg/essential/network/connectionmanager/sps/SPSManager.java @@ -39,6 +39,7 @@ import gg.essential.network.connectionmanager.queue.PacketQueue; import gg.essential.network.connectionmanager.queue.SequentialPacketQueue; import gg.essential.sps.ResourcePackSharingHttpServer; +import gg.essential.sps.WindowTitleManager; import gg.essential.universal.UMinecraft; import gg.essential.universal.wrappers.message.UTextComponent; import gg.essential.upnp.UPnPPrivacy; @@ -379,6 +380,8 @@ public void startLocalSession(SPSSessionSource sessionSource) { Essential.EVENT_BUS.post(new SPSStartEvent(address)); EssentialCommandRegistry.INSTANCE.registerSPSHostCommands(); + + WindowTitleManager.INSTANCE.updateTitle(); } public synchronized void updateLocalSession(@NotNull String ip, int port) { diff --git a/src/main/kotlin/gg/essential/gui/common/UI3DPlayer.kt b/src/main/kotlin/gg/essential/gui/common/UI3DPlayer.kt index 44b6b87..19680cb 100644 --- a/src/main/kotlin/gg/essential/gui/common/UI3DPlayer.kt +++ b/src/main/kotlin/gg/essential/gui/common/UI3DPlayer.kt @@ -856,7 +856,7 @@ open class UI3DPlayer( else -> Pair(getThirdPartyCape(), null) } - playerModel.update() + playerModel.essentialAnimationSystem.updateAnimationState() // processes arm swing wearablesManager.update() poseManager.update(wearablesManager) diff --git a/src/main/kotlin/gg/essential/gui/friends/SocialMenu.kt b/src/main/kotlin/gg/essential/gui/friends/SocialMenu.kt index 54720ce..b5535e9 100644 --- a/src/main/kotlin/gg/essential/gui/friends/SocialMenu.kt +++ b/src/main/kotlin/gg/essential/gui/friends/SocialMenu.kt @@ -41,6 +41,7 @@ import gg.essential.gui.modals.select.selectModal import gg.essential.gui.modals.select.users import gg.essential.gui.notification.Notifications import gg.essential.gui.notification.warning +import gg.essential.gui.sps.InviteFriendsModal import gg.essential.gui.util.onItemAdded import gg.essential.gui.util.toStateV2List import gg.essential.universal.UMinecraft @@ -215,7 +216,7 @@ class SocialMenu @JvmOverloads constructor( val options = extraOptions.toMutableList() val joinPlayerOption = ContextOptionMenu.Option( - "Join", + "Join Game", // New default is text, so remove entirely when removing feature flag textColor = EssentialPalette.TEXT, hoveredColor = EssentialPalette.MESSAGE_SENT, @@ -226,15 +227,15 @@ class SocialMenu @JvmOverloads constructor( handleJoinSession(user) } val invitePlayerOption = ContextOptionMenu.Option( - "Invite", + "Invite to Game", // New default is text, so remove entirely when removing feature flag textColor = EssentialPalette.TEXT, hoveredColor = EssentialPalette.MESSAGE_SENT, // New default is black, so remove entirely when removing feature flag shadowColor = EssentialPalette.BLACK, - image = EssentialPalette.INVITE_10X6, + image = EssentialPalette.ENVELOPE_9X7, ) { - handleInvitePlayers(setOf(user)) + handleInvitePlayers(setOf(user), UuidNameLookup.nameState(user).getUntracked()) } val topmostOptions: MutableList = mutableListOf() @@ -340,7 +341,8 @@ class SocialMenu @JvmOverloads constructor( addMarkMessagesReadOption(channel.id, options) - if (ServerType.current()?.supportsInvites == true) { + // Left commented if we re-add in the future + /* if (ServerType.current()?.supportsInvites == true) { options.add( ContextOptionMenu.Option( "Invite Group", @@ -351,12 +353,12 @@ class SocialMenu @JvmOverloads constructor( shadowColor = EssentialPalette.BLACK, image = EssentialPalette.INVITE_10X6, ) { - handleInvitePlayers(channel.members) + handleInvitePlayers(channel.members, channel.name) } ) options.add(ContextOptionMenu.Divider) - } + } */ val mutedState = socialStateManager.messengerStates.getMuted(channel.id) if (channel.type == ChannelType.GROUP_DIRECT_MESSAGE && channel.createdInfo.by == UUIDUtil.getClientUUID()) { @@ -463,10 +465,11 @@ class SocialMenu @JvmOverloads constructor( } } - fun handleInvitePlayers(users: Set) { + fun handleInvitePlayers(users: Set, name: String) { val currentServerData = UMinecraft.getMinecraft().currentServerData if (hasLocalSession()) { spsManager.reinviteUsers(users) + InviteFriendsModal.sendInviteNotification(name) } else if (currentServerData != null) { connectionManager.socialManager.reinviteFriendsOnServer(currentServerData.serverIP, users) } diff --git a/src/main/kotlin/gg/essential/gui/friends/message/MessageInput.kt b/src/main/kotlin/gg/essential/gui/friends/message/MessageInput.kt index 1c60ed8..75b7fd3 100644 --- a/src/main/kotlin/gg/essential/gui/friends/message/MessageInput.kt +++ b/src/main/kotlin/gg/essential/gui/friends/message/MessageInput.kt @@ -21,6 +21,7 @@ import gg.essential.elementa.dsl.coerceAtMost import gg.essential.elementa.dsl.pixels import gg.essential.elementa.utils.roundToRealPixels import gg.essential.gui.EssentialPalette +import gg.essential.gui.common.EssentialTooltip import gg.essential.gui.common.IconButton import gg.essential.gui.common.input.UIMultilineTextInput import gg.essential.gui.elementa.state.v2.MutableState @@ -28,6 +29,7 @@ import gg.essential.gui.elementa.state.v2.State import gg.essential.gui.elementa.state.v2.combinators.map import gg.essential.gui.elementa.state.v2.effect import gg.essential.gui.elementa.state.v2.memo +import gg.essential.gui.elementa.state.v2.mutableStateOf import gg.essential.gui.friends.message.screenshot.ScreenshotAttacher import gg.essential.gui.friends.message.screenshot.ScreenshotAttachmentManager import gg.essential.gui.friends.message.screenshot.ScreenshotPicker @@ -69,6 +71,9 @@ class MessageInput( private val usernameState = memo { replyTo()?.sender?.let(UUIDUtil::nameState)?.invoke() ?: "Nobody" } + private val message = mutableStateOf("") + private val messageLength = memo { message().length } + private val input = UIMultilineTextInput(shadowColor = EssentialPalette.TEXT_SHADOW_LIGHT).apply { effect(this) { placeholder = "Message ${channelName()}" @@ -100,6 +105,8 @@ class MessageInput( } } + + message.set(this@apply.getText()) } } @@ -252,6 +259,7 @@ class MessageInput( input(Modifier.fillWidth()) } } + spacer(width = 67f) box(Modifier.width(2f).maxSiblingHeight()) { scrollBar = box(Modifier.fillWidth().color(EssentialPalette.SCROLLBAR)) } @@ -268,6 +276,10 @@ class MessageInput( ).onActiveClick { screenshotAttachmentManager.isPickingScreenshots.set { !it } } + characterLimit(Modifier + .alignHorizontal(Alignment.End(11f)) + .alignVertical(Alignment.End(10f)) + ) }.onLeftClick { screenshotAttachmentManager.isPickingScreenshots.set(false) } @@ -321,7 +333,7 @@ class MessageInput( private fun handleSendMessage() { var text = input.getText() - val charLimit = 500 + val charLimit = MESSAGE_CHAR_LIMIT if (text.length > charLimit) { Notifications.warning("Too many characters", "You have exceeded the\n$charLimit character limit.") return @@ -352,11 +364,27 @@ class MessageInput( } } + private fun LayoutScope.characterLimit(modifier: Modifier = Modifier) { + if_({ messageLength() >= MESSAGE_CHAR_WARNING }) { + text({ "${MESSAGE_CHAR_LIMIT - messageLength()}" }, Modifier + .color { if (messageLength() > MESSAGE_CHAR_LIMIT) EssentialPalette.TEXT_WARNING else EssentialPalette.TEXT } + .shadow { EssentialPalette.BLACK } + .hoverScope() + .hoverTooltip({ if (messageLength() <= MESSAGE_CHAR_LIMIT) "Remaining characters" else "Message is too long" }, position = EssentialTooltip.Position.ABOVE) + .then(modifier) + ) + } + } + override fun afterInitialization() { grabFocus() } companion object { + + private const val MESSAGE_CHAR_LIMIT = 2500 + private const val MESSAGE_CHAR_WARNING = 2000 + val colorSymbols = listOf("§", "§", "§", "${ChatColor.COLOR_CHAR}") } diff --git a/src/main/kotlin/gg/essential/gui/friends/message/MessageTitleBar.kt b/src/main/kotlin/gg/essential/gui/friends/message/MessageTitleBar.kt index 1f40042..a10a217 100644 --- a/src/main/kotlin/gg/essential/gui/friends/message/MessageTitleBar.kt +++ b/src/main/kotlin/gg/essential/gui/friends/message/MessageTitleBar.kt @@ -123,7 +123,7 @@ class MessageTitleBar( ) } - if (!preview.channel.isAnnouncement()) { + if (!preview.channel.isAnnouncement() && preview.channel.type != ChannelType.GROUP_DIRECT_MESSAGE) { if (ServerType.current()?.supportsInvites == true) { @@ -143,7 +143,7 @@ class MessageTitleBar( if (!invited.get()) { USound.playButtonPress() invited.set(true) - gui.handleInvitePlayers(preview.channel.members) + gui.handleInvitePlayers(preview.channel.members, preview.titleState.getUntracked()) } } button.onMouseLeave { diff --git a/src/main/kotlin/gg/essential/gui/friends/message/v2/ReplyableMessageScreen.kt b/src/main/kotlin/gg/essential/gui/friends/message/v2/ReplyableMessageScreen.kt index ccb5a28..50bc6ba 100644 --- a/src/main/kotlin/gg/essential/gui/friends/message/v2/ReplyableMessageScreen.kt +++ b/src/main/kotlin/gg/essential/gui/friends/message/v2/ReplyableMessageScreen.kt @@ -396,6 +396,9 @@ class ReplyableMessageScreen( is ClientMessage.Part.Text -> ParagraphLineImpl(messageWrapper, part.content) }) } + if (messages.isEmpty()) { + messages.add(ParagraphLineImpl(messageWrapper, "")) + } return messages } diff --git a/src/main/kotlin/gg/essential/gui/friends/previews/ChannelPreview.kt b/src/main/kotlin/gg/essential/gui/friends/previews/ChannelPreview.kt index a9e132e..d818f67 100644 --- a/src/main/kotlin/gg/essential/gui/friends/previews/ChannelPreview.kt +++ b/src/main/kotlin/gg/essential/gui/friends/previews/ChannelPreview.kt @@ -44,6 +44,7 @@ import gg.essential.gui.elementa.state.v2.toV2 import gg.essential.gui.friends.SocialMenu import gg.essential.gui.friends.Tab import gg.essential.gui.friends.message.MessageUtils +import gg.essential.gui.friends.state.PlayerActivity import gg.essential.gui.image.ImageFactory import gg.essential.gui.layoutdsl.* import gg.essential.gui.studio.Tag @@ -74,6 +75,7 @@ class ChannelPreview( private val activity = gui.socialStateManager.statusStates.getActivityState(uuid) private val joinable = activity.map { it.isJoinable() } + private val isOnline = memo { activity() !is PlayerActivity.Offline } val titleState = if (otherUser != null) { UUIDUtil.nameState(otherUser) @@ -127,9 +129,12 @@ class ChannelPreview( } layoutAsBox(Modifier.fillWidth().height(40f).then(BasicYModifier(::SiblingConstraint)).then(color)) { - row(Modifier.fillWidth(padding = 10f).fillHeight()) { - image(Modifier.width(24f).heightAspect(1f)) - spacer(width = 7.5f) + row(Modifier.fillParent()) { + spacer(width = 6f) + box(Modifier.width(32f).heightAspect(1f)) { + image(Modifier.width(24f).heightAspect(1f)) + } + spacer(width = 3.5f) column(Modifier.fillRemainingWidth().fillHeight(), Arrangement.spacedBy(0f, FloatPosition.START), Alignment.Start) { spacer(height = 10f) row(Modifier.fillWidth()) { @@ -167,6 +172,7 @@ class ChannelPreview( spacer(width = 4f) unreadQuantity(Modifier.childBasedWidth(padding = 2f).childBasedHeight(padding = 2f)) } + spacer(width = 10f) } } diff --git a/src/main/kotlin/gg/essential/gui/modals/UpdateNotificationModal.kt b/src/main/kotlin/gg/essential/gui/modals/UpdateNotificationModal.kt index e75cb61..78d233d 100644 --- a/src/main/kotlin/gg/essential/gui/modals/UpdateNotificationModal.kt +++ b/src/main/kotlin/gg/essential/gui/modals/UpdateNotificationModal.kt @@ -124,4 +124,5 @@ class UpdateNotificationModal(modalManager: ModalManager) : VerticalConfirmDenyM } }, Window::enqueueRenderOperation) } + } diff --git a/src/main/kotlin/gg/essential/gui/screenshot/ScreenshotOverlay.kt b/src/main/kotlin/gg/essential/gui/screenshot/ScreenshotOverlay.kt index b67cf5b..a218000 100644 --- a/src/main/kotlin/gg/essential/gui/screenshot/ScreenshotOverlay.kt +++ b/src/main/kotlin/gg/essential/gui/screenshot/ScreenshotOverlay.kt @@ -17,13 +17,35 @@ import gg.essential.config.EssentialConfig import gg.essential.data.OnboardingData import gg.essential.elementa.UIComponent import gg.essential.elementa.components.UIBlock +import gg.essential.elementa.components.UIBlock.Companion.drawBlock import gg.essential.elementa.components.UIContainer import gg.essential.elementa.components.UIImage import gg.essential.elementa.components.UIText import gg.essential.elementa.components.Window -import gg.essential.elementa.constraints.* -import gg.essential.elementa.constraints.animation.* -import gg.essential.elementa.dsl.* +import gg.essential.elementa.constraints.AspectConstraint +import gg.essential.elementa.constraints.CenterConstraint +import gg.essential.elementa.constraints.ChildBasedMaxSizeConstraint +import gg.essential.elementa.constraints.ChildBasedSizeConstraint +import gg.essential.elementa.constraints.FillConstraint +import gg.essential.elementa.constraints.SiblingConstraint +import gg.essential.elementa.constraints.XConstraint +import gg.essential.elementa.constraints.animation.Animations +import gg.essential.elementa.dsl.animate +import gg.essential.elementa.dsl.basicHeightConstraint +import gg.essential.elementa.dsl.basicWidthConstraint +import gg.essential.elementa.dsl.basicXConstraint +import gg.essential.elementa.dsl.basicYConstraint +import gg.essential.elementa.dsl.childOf +import gg.essential.elementa.dsl.constrain +import gg.essential.elementa.dsl.max +import gg.essential.elementa.dsl.minus +import gg.essential.elementa.dsl.percent +import gg.essential.elementa.dsl.percentOfWindow +import gg.essential.elementa.dsl.pixel +import gg.essential.elementa.dsl.pixels +import gg.essential.elementa.dsl.provideDelegate +import gg.essential.elementa.dsl.times +import gg.essential.elementa.dsl.toConstraint import gg.essential.elementa.effects.OutlineEffect import gg.essential.elementa.state.BasicState import gg.essential.elementa.state.State @@ -35,8 +57,14 @@ import gg.essential.gui.common.bindParent import gg.essential.gui.common.onSetValueAndNow import gg.essential.gui.common.or import gg.essential.gui.common.shadow.ShadowIcon +import gg.essential.gui.elementa.state.v2.animateTransitions +import gg.essential.gui.elementa.state.v2.mutableStateOf import gg.essential.gui.image.ImageFactory -import gg.essential.gui.layoutdsl.* +import gg.essential.gui.layoutdsl.Modifier +import gg.essential.gui.layoutdsl.fillHeight +import gg.essential.gui.layoutdsl.fillWidth +import gg.essential.gui.layoutdsl.layoutAsColumn +import gg.essential.gui.layoutdsl.row import gg.essential.gui.modals.NotAuthenticatedModal import gg.essential.gui.modals.TOSModal import gg.essential.gui.notification.Notifications @@ -49,9 +77,15 @@ import gg.essential.gui.screenshot.toast.ScreenshotPreviewAction import gg.essential.gui.screenshot.toast.ScreenshotPreviewActionSlot import gg.essential.gui.util.hoveredState import gg.essential.gui.util.onAnimationFrame +import gg.essential.universal.UMatrixStack import gg.essential.universal.UResolution import gg.essential.universal.USound -import gg.essential.util.* +import gg.essential.util.GuiUtil +import gg.essential.util.Multithreading +import gg.essential.util.bindEssentialTooltip +import gg.essential.util.centered +import gg.essential.util.div +import gg.essential.util.times import gg.essential.vigilance.utils.onLeftClick import java.awt.Color import java.io.File @@ -453,32 +487,63 @@ class ScreenshotUploadToast : UIContainer() { private var targetProgress: ToastProgress = initialProgress private var currentProgress: ToastProgress = targetProgress val timerEnabled = BasicState(false) - private val stateText by UIText("Uploading...").constrain { + private val stateText by UIText("Uploading...").setShadowColor(EssentialPalette.TEXT_SHADOW_LIGHT).constrain { x = SiblingConstraint(6f) y = CenterConstraint() color = EssentialPalette.TEXT_HIGHLIGHT.toConstraint() } childOf this - private val progressContainer by UIContainer().constrain { + private val progressState = mutableStateOf(0f) + private val progressStateAnimated = progressState.animateTransitions(this@ScreenshotUploadToast, 0.5f, Animations.LINEAR) + + private val progress by object : UIComponent() { + + override fun draw(matrixStack: UMatrixStack) { + beforeDraw(matrixStack) + + val x = constraints.getX().toDouble() + val y = constraints.getY().toDouble() + val width = constraints.getWidth().toDouble() + val height = constraints.getHeight().toDouble() + + val percent = progressStateAnimated.getUntracked().toDouble() + + matrixStack.push() + matrixStack.translate(1f, 1f, 0f) + drawInner(matrixStack, x, y, width, height, percent, EssentialPalette.TEXT_SHADOW_LIGHT) + matrixStack.pop() + drawInner(matrixStack, x, y, width, height, percent, EssentialPalette.TEXT_HIGHLIGHT) + + super.draw(matrixStack) + } + + private fun drawInner(matrixStack: UMatrixStack, x: Double, y: Double, width: Double, height: Double, percent: Double, color: Color) { + drawBlock(matrixStack, color, x, y, x + width, y + 1) + drawBlock(matrixStack, color, x, y + height - 1, x + width, y + height) + drawBlock(matrixStack, color, x, y + 1, x + 1, y + height - 1) + drawBlock(matrixStack, color, x + width - 1, y + 1, x + width, y + height - 1) + + drawBlock(matrixStack, color, x + 1, y + 1, x + (width - 2) * percent, y + height - 1) + } + }.constrain { x = SiblingConstraint(4f) y = CenterConstraint() width = FillConstraint(useSiblings = false) - 1.pixel - height = 9.pixels - } childOf this effect OutlineEffect(EssentialPalette.TEXT_HIGHLIGHT, 1f) - - private val progressBlock by UIBlock(EssentialPalette.TEXT_HIGHLIGHT).constrain { - width = 0.pixels - height = 100.percent - } childOf progressContainer + height = 8.pixels + } childOf this init { constrain { width = 100.percent - height = - ChildBasedMaxSizeConstraint() + 2.pixels // so that the outline is not scissored out of existence by the notification + height = ChildBasedMaxSizeConstraint() - 2.pixels } } + override fun afterInitialization() { + super.afterInitialization() + progress.setFloating(true) + } + override fun animationFrame() { super.animationFrame() updateProgress() @@ -497,22 +562,22 @@ class ScreenshotUploadToast : UIContainer() { } val targetPercent = if (targetProgress is ToastProgress.Step) { - targetProgress.completionPercent + targetProgress.completionPercent.coerceAtMost(100) } else { 100 } - progressBlock.animate { - setWidthAnimation(Animations.LINEAR, 0.5f, targetPercent.pixels) - onComplete { - if (targetProgress is ToastProgress.Complete) { - // If we were successful, and it's been under maxCompletionDelayMillis, use some delay for dramatic effect. - val timeElapsedMillis = System.currentTimeMillis() - startUploadMillis - val delayMillis = maxCompletionDelayMillis - timeElapsedMillis - if (targetProgress.success && delayMillis > 0) { - delay(delayMillis) { fireComplete(targetProgress) } - } else { - fireComplete(targetProgress) - } + + progressState.set(targetPercent * 0.01f) + + delay(500) { + if (targetProgress is ToastProgress.Complete) { + // If we were successful, and it's been under maxCompletionDelayMillis, use some delay for dramatic effect. + val timeElapsedMillis = System.currentTimeMillis() - startUploadMillis + val delayMillis = maxCompletionDelayMillis - timeElapsedMillis + if (targetProgress.success && delayMillis > 0) { + delay(delayMillis) { fireComplete(targetProgress) } + } else { + fireComplete(targetProgress) } } } @@ -530,7 +595,8 @@ class ScreenshotUploadToast : UIContainer() { private fun fireComplete(status: ToastProgress.Complete) { val action = { timerEnabled.set(true) - removeChild(progressContainer) + progress.setFloating(false) + removeChild(progress) stateText.setText(status.message) this.insertChildAt( ShadowIcon( diff --git a/src/main/kotlin/gg/essential/gui/sps/InviteFriendsModal.kt b/src/main/kotlin/gg/essential/gui/sps/InviteFriendsModal.kt index 058916f..8f4a413 100644 --- a/src/main/kotlin/gg/essential/gui/sps/InviteFriendsModal.kt +++ b/src/main/kotlin/gg/essential/gui/sps/InviteFriendsModal.kt @@ -170,7 +170,6 @@ object InviteFriendsModal { height = ChildBasedSizeConstraint() } childOf customContent - val oldDropdowns = mutableListOf() val dropdowns = mutableListOf>() val gamemodes = GameType.values() @@ -470,10 +469,12 @@ object InviteFriendsModal { } fun sendInviteNotification(uuid: UUID) { - UUIDUtil.getName(uuid).thenAcceptOnMainThread { username -> - Notifications.push("", "") { - iconAndMarkdownBody(EssentialPalette.ENVELOPE_9X7.create(), "${username.colored(EssentialPalette.TEXT_HIGHLIGHT)} invited") - } + UUIDUtil.getName(uuid).thenAcceptOnMainThread { sendInviteNotification(it) } + } + + fun sendInviteNotification(name: String) { + Notifications.push("", "") { + iconAndMarkdownBody(EssentialPalette.ENVELOPE_9X7.create(), "${name.colored(EssentialPalette.TEXT_HIGHLIGHT)} invited") } } diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/EmoteWheelPage.kt b/src/main/kotlin/gg/essential/gui/wardrobe/EmoteWheelPage.kt index a161833..813475e 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/EmoteWheelPage.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/EmoteWheelPage.kt @@ -12,82 +12,77 @@ package gg.essential.gui.wardrobe import gg.essential.elementa.UIComponent -import gg.essential.elementa.components.UIBlock import gg.essential.elementa.components.UIContainer -import gg.essential.elementa.components.Window -import gg.essential.elementa.constraints.MousePositionConstraint -import gg.essential.elementa.dsl.constrain -import gg.essential.elementa.dsl.minus -import gg.essential.elementa.dsl.pixels -import gg.essential.elementa.dsl.plus -import gg.essential.elementa.dsl.provideDelegate import gg.essential.elementa.state.BasicState import gg.essential.gui.EssentialPalette import gg.essential.gui.common.* import gg.essential.gui.elementa.state.v2.State -import gg.essential.gui.elementa.state.v2.combinators.and import gg.essential.gui.elementa.state.v2.combinators.map -import gg.essential.gui.elementa.state.v2.combinators.not -import gg.essential.gui.elementa.state.v2.combinators.or -import gg.essential.gui.elementa.state.v2.combinators.zip +import gg.essential.gui.elementa.state.v2.flatten +import gg.essential.gui.elementa.state.v2.memo import gg.essential.gui.elementa.state.v2.mutableStateOf -import gg.essential.gui.elementa.state.v2.set -import gg.essential.gui.elementa.state.v2.stateBy import gg.essential.gui.elementa.state.v2.stateOf import gg.essential.gui.elementa.state.v2.toV1 -import gg.essential.gui.elementa.state.v2.toV2 import gg.essential.gui.layoutdsl.* -import gg.essential.gui.util.hoveredState -import gg.essential.gui.util.onAnimationFrame +import gg.essential.gui.util.Tag +import gg.essential.gui.util.addTag +import gg.essential.gui.util.hoveredStateV2 import gg.essential.network.cosmetics.Cosmetic import gg.essential.universal.UMouse import gg.essential.universal.USound -import gg.essential.util.* import gg.essential.vigilance.utils.onLeftClick -import kotlin.math.abs class EmoteWheelPage(private val state: WardrobeState) : UIContainer() { + init { val containerModifier = Modifier.childBasedMaxSize() layout { box(containerModifier) { column(Arrangement.spacedBy(7f)) { - row(Arrangement.spacedBy(7f)) { - emoteSlot(slotModifier, 0) - emoteSlot(slotModifier, 1) - emoteSlot(slotModifier, 2) - } - row(Arrangement.spacedBy(7f)) { - emoteSlot(slotModifier, 3) - box(slotModifier) - emoteSlot(slotModifier, 4) - } - row(Arrangement.spacedBy(7f)) { - emoteSlot(slotModifier, 5) - emoteSlot(slotModifier, 6) - emoteSlot(slotModifier, 7) + bind({ state.emoteWheelManager.selectedEmoteWheelId() }) { emoteWheelId -> + if (emoteWheelId == null) { + return@bind + } + row(Arrangement.spacedBy(7f)) { + emoteSlot(slotModifier, WardrobeState.EmoteSlotId(emoteWheelId, 0)) + emoteSlot(slotModifier, WardrobeState.EmoteSlotId(emoteWheelId, 1)) + emoteSlot(slotModifier, WardrobeState.EmoteSlotId(emoteWheelId, 2)) + } + row(Arrangement.spacedBy(7f)) { + emoteSlot(slotModifier, WardrobeState.EmoteSlotId(emoteWheelId, 3)) + box(slotModifier) + emoteSlot(slotModifier, WardrobeState.EmoteSlotId(emoteWheelId, 4)) + } + row(Arrangement.spacedBy(7f)) { + emoteSlot(slotModifier, WardrobeState.EmoteSlotId(emoteWheelId, 5)) + emoteSlot(slotModifier, WardrobeState.EmoteSlotId(emoteWheelId, 6)) + emoteSlot(slotModifier, WardrobeState.EmoteSlotId(emoteWheelId, 7)) + } } } } } } - private fun LayoutScope.emoteSlot(modifier: Modifier, index: Int) { - val cartHovered = BasicState(false).map { it } - val emote = state.emoteWheel.map { it[index] } - val cosmetic = emote.map { it?.let { id -> state.cosmeticsData.getCosmetic(id) } } - val empty = emote.map { it == null } - val filledButNotOwned = !empty and cosmetic.zip(state.unlockedCosmetics).map { (cosmetic, unlockCosmetics) -> cosmetic?.let { it.id !in unlockCosmetics } ?: false } - val hovered = mutableStateOf(false) - val draggingInProgress = state.draggingEmoteSlot.map { it != null } - val beingDraggedFrom = state.draggingEmoteSlot.map { it == index } and !empty - val beingDraggedOnto = state.draggingOntoEmoteSlot.map { it == index } and (state.draggingEmoteSlot.zip(state.emoteWheel).map { (index, emoteWheel ) -> index != null && (index == -1 || emoteWheel[index] != null) }) - val visibleCosmetic = cosmetic.zip(beingDraggedFrom).map { (cosmetic, beingDraggedFrom) -> cosmetic.takeUnless { beingDraggedFrom } } + private fun LayoutScope.emoteSlot(modifier: Modifier, emoteSlotId: WardrobeState.EmoteSlotId) { + val cosmetic = memo { + val slots = state.emoteWheelManager.orderedEmoteWheels().find { it.id == emoteSlotId.emoteWheelId }?.slots ?: return@memo null + slots[emoteSlotId.slotIndex]?.let { state.cosmeticsData.getCosmetic(it) } + } + val hoveredSource = mutableStateOf(stateOf(false)) + val hovered = hoveredSource.flatten() + val draggingInProgress = state.draggingEmote.map { it != null } + val beingDraggedFrom = memo { cosmetic() != null && state.draggingEmote()?.from == emoteSlotId } + val beingDraggedOnto = memo { + val draggingEmote = state.draggingEmote() + draggingEmote?.to != draggingEmote?.from && draggingEmote?.to == emoteSlotId + } + val visibleCosmetic = State { cosmetic().takeUnless { beingDraggedFrom() } } - val backgroundColor = stateBy { + val backgroundColor = memo { when { beingDraggedOnto() -> EssentialPalette.COMPONENT_HIGHLIGHT - empty() -> EssentialPalette.INPUT_BACKGROUND + cosmetic() == null -> EssentialPalette.INPUT_BACKGROUND hovered() -> EssentialPalette.COMPONENT_HIGHLIGHT else -> EssentialPalette.COMPONENT_BACKGROUND } @@ -104,114 +99,41 @@ class EmoteWheelPage(private val state: WardrobeState) : UIContainer() { return CosmeticPreview(cosmetic)(modifier) } - val container = EmoteSlot(index) - container(modifier.color(backgroundColor).then(outline)) { + val tooltip = Modifier.then(memo { + if (hovered() && !draggingInProgress()) { + val displayName = cosmetic()?.displayName ?: return@memo Modifier + Modifier.tooltip(displayName, position = EssentialTooltip.Position.MOUSE_OFFSET(10f, -15f), notchSize = 0) + } else { + Modifier + } + }) + + val container = box(modifier.color(backgroundColor).then(outline).then(tooltip)) { ifNotNull(visibleCosmetic) { cosmetic -> box(Modifier.fillParent().whenTrue(beingDraggedOnto, fadeOut)) { thumbnail(cosmetic, Modifier.fillParent()) } } - } - - container.hoveredState().onSetValueAndNow { hovered.set(it) } - - val draggable = - object : UIContainer() { - // when we hitTest, we want the thing below the dragging graphic - override fun isPointInside(x: Float, y: Float): Boolean = false - } - draggable.layout(Modifier.width(container).height(container)) { - ifNotNull(cosmetic) { cosmetic -> - thumbnail(cosmetic, Modifier.fillParent()) - } - } - - fun tooltip(parent: UIComponent, text: State) = - EssentialTooltip(parent, position = EssentialTooltip.Position.RIGHT, notchSize = 0) - .constrain { - x = MousePositionConstraint() + 10.pixels - y = MousePositionConstraint() - 15.pixels - } - .bindLine(text.toV1(this@EmoteWheelPage)) - - - val nameTooltip by tooltip(container, cosmetic.map { it?.displayName ?: "" }) - val swapTooltip by tooltip(draggable, stateOf("Swap")) - val removeTooltip by tooltip(draggable, stateOf("Remove")) + }.addTag(EmoteSlotTag(emoteSlotId)) - val swapVisible = beingDraggedFrom and state.draggingOntoOccupiedEmoteSlot - val removeVisible = beingDraggedFrom and state.draggingOntoEmoteSlot.map { it == -1 } - swapTooltip.bindVisibility(swapVisible) - removeTooltip.bindVisibility(removeVisible) - nameTooltip.bindVisibility((hovered and !draggingInProgress and !empty and !cartHovered.toV2()) or (beingDraggedFrom and !swapVisible and !removeVisible)) + hoveredSource.set(container.hoveredStateV2()) - draggable.onAnimationFrame { - val (mouseX, mouseY) = getMousePosition() - val target = Window.of(draggable).hitTest(mouseX, mouseY) - val slotTarget = target as? EmoteSlot ?: target.findParentOfTypeOrNull() - val removeTarget = target.findParentOfTypeOrNull() - state.draggingOntoEmoteSlot.set(when { - slotTarget != null -> slotTarget.index.takeIf { it != index } - removeTarget != null -> -1 - else -> null - }) - } - - var mayBeLeftClick = false - var clickStart = Pair(0f, 0f) container.onLeftClick { - val xOffset = UMouse.Scaled.x.toFloat() - container.getLeft() - val yOffset = UMouse.Scaled.y.toFloat() - container.getTop() - draggable.constrain { - x = MousePositionConstraint() - xOffset.pixels - y = MousePositionConstraint() - yOffset.pixels - } - Window.of(this).addChild(draggable) - - state.draggingEmoteSlot.set(index) - - mayBeLeftClick = true - clickStart = Pair(xOffset, yOffset) - } - - container.onMouseDrag { mouseX, mouseY, _ -> - val distance = abs(clickStart.first - mouseX) + abs(clickStart.second - mouseY) - if (distance > 5) { - mayBeLeftClick = false - } - } - - draggable.onMouseRelease { - val target = state.draggingOntoEmoteSlot.get() - if (target != null) { - if (target == -1) { - state.emoteWheelManager.setEmote(index, null) - } else { - val sourceEmote = state.emoteWheel.getUntracked()[index] - val targetEmote = state.emoteWheel.getUntracked()[target] - state.emoteWheelManager.setEmote(index, targetEmote) - state.emoteWheelManager.setEmote(target, sourceEmote) - } - } else if (mayBeLeftClick) { - state.emoteWheel.getUntracked()[index]?.let { + val clickOffset = Pair(UMouse.Scaled.x.toFloat() - container.getLeft(), UMouse.Scaled.y.toFloat() - container.getTop()) + state.draggingEmote.set(WardrobeState.DraggedEmote(cosmetic.getUntracked()?.id, emoteSlotId, emoteSlotId, clickOffset) { + if (cosmetic.getUntracked() != null) { USound.playButtonPress() } - state.emoteWheelManager.setEmote(index, null) - } - - state.draggingEmoteSlot.set(null) - state.draggingOntoEmoteSlot.set(null) - - Window.enqueueRenderOperation { - hide(instantly = true) - } + state.emoteWheelManager.setEmote(emoteSlotId.emoteWheelId, emoteSlotId.slotIndex, null) + }) } } - class EmoteSlot(val index: Int) : UIBlock() + class EmoteSlotTag(val emoteSlotId: WardrobeState.EmoteSlotId) : Tag companion object { - val slotModifier = Modifier.width(62f).height(62f) + const val SLOT_SIZE = 62f + val slotModifier = Modifier.width(SLOT_SIZE).height(SLOT_SIZE) } } diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/Wardrobe.kt b/src/main/kotlin/gg/essential/gui/wardrobe/Wardrobe.kt index 8f5ffce..ccaf703 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/Wardrobe.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/Wardrobe.kt @@ -272,7 +272,8 @@ class Wardrobe( //#if MC>=11602 //$$ keyCode == UKeyboard.KEY_ESCAPE -> restorePreviousScreen() //#endif - Essential.getInstance().keybindingRegistry.toggleCosmetics.isKeyCode(keyCode) -> { + Essential.getInstance().keybindingRegistry.toggleCosmetics.isKeyCode(keyCode) + && !EssentialConfig.disableCosmetics -> { Essential.getInstance().connectionManager.cosmeticsManager.toggleOwnCosmeticVisibility(true) } else -> { @@ -375,12 +376,6 @@ class Wardrobe( } modal.onPrimaryAction { - val emotes = state.emoteWheel.get().toMutableList() // Copy list to avoid concurrent modification - emotes.forEachIndexed { index, s -> - if (s != null && s !in state.cosmeticsManager.unlockedCosmetics.get()) { - state.emoteWheelManager.setEmote(index, null) - } - } restorePreviousScreen() } diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/WardrobeContainer.kt b/src/main/kotlin/gg/essential/gui/wardrobe/WardrobeContainer.kt index 6bad1be..39c286c 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/WardrobeContainer.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/WardrobeContainer.kt @@ -26,6 +26,7 @@ import gg.essential.gui.wardrobe.categories.featuredCategory import gg.essential.gui.wardrobe.categories.outfitsCategory import gg.essential.gui.wardrobe.categories.skinsCategory import gg.essential.gui.wardrobe.components.banner +import gg.essential.gui.wardrobe.components.draggingEmote import gg.essential.network.connectionmanager.telemetry.TelemetryManager import gg.essential.util.* @@ -42,7 +43,6 @@ class WardrobeContainer( init { val connectionManager = Essential.getInstance().connectionManager val noticesManager = connectionManager.noticesManager - val draggingOnto = wardrobeState.draggingOntoEmoteSlot.map { it == -1 } val fadeEffect = Modifier.effect { FadeEffect(EssentialPalette.GUI_BACKGROUND, 0.5f) } val categoryBanner = noticesManager.noticeBannerManager.getNoticeBanners() @@ -54,7 +54,7 @@ class WardrobeContainer( } layout { - column(Modifier.fillParent().whenTrue(draggingOnto, fadeEffect)) { + column(Modifier.fillParent().whenTrue(wardrobeState.draggingEmote.map { it?.from != null && it.to == null }, fadeEffect)) { // Sticky banners above the scroller ifNotNull(categoryBanner) { banner -> if (banner.sticky) { @@ -86,12 +86,16 @@ class WardrobeContainer( } } - if_(draggingOnto) { + if_(wardrobeState.draggingEmote.map { it?.from != null && it.to == null }) { object : UIContainer() { // Prevent children from being hovered while this component acts as a big trash can override fun isPointInside(x: Float, y: Float): Boolean = true }(Modifier.fillParent()) } + + ifNotNull({ wardrobeState.draggingEmote()?.copy(to = null) }) { draggingEmote -> + draggingEmote(wardrobeState, draggingEmote) + } } // TODO ideally we declare this in above `layout` but the `scroller` reference currently makes this impossible content.layout { bindContent() } diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/categories/CategoryComponent.kt b/src/main/kotlin/gg/essential/gui/wardrobe/categories/CategoryComponent.kt index c5dd71c..998b4cf 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/categories/CategoryComponent.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/categories/CategoryComponent.kt @@ -162,7 +162,7 @@ class CategoryComponent( val group = groups.find { it.category == category } ?: return scroller.scrollToTopOf(this) val cosmetics = group.sortedCosmetics.get() - val equipped = wardrobeState.equippedCosmeticsState.get().values.toSet() + wardrobeState.emoteWheel.get() + val equipped = wardrobeState.equippedCosmeticsState.get().values.toSet() + wardrobeState.emoteWheelManager.selectedEmoteWheelSlots.getUntracked() val target = group.cosmeticsContainer.children.getOrNull(cosmetics.indexOfFirst { cosmetic -> if (highlightedItem != null) { diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/categories/featuredCategory.kt b/src/main/kotlin/gg/essential/gui/wardrobe/categories/featuredCategory.kt index b6182fd..3d8ec75 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/categories/featuredCategory.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/categories/featuredCategory.kt @@ -11,9 +11,12 @@ */ package gg.essential.gui.wardrobe.categories +import gg.essential.cosmetics.FeaturedPageWidth import gg.essential.elementa.UIComponent import gg.essential.elementa.components.ScrollComponent import gg.essential.elementa.components.Window +import gg.essential.gui.EssentialPalette +import gg.essential.gui.about.components.ColoredDivider import gg.essential.gui.elementa.state.v2.effect import gg.essential.gui.layoutdsl.* import gg.essential.gui.util.findChildrenByTag @@ -23,13 +26,58 @@ import gg.essential.gui.wardrobe.WardrobeCategory import gg.essential.gui.wardrobe.WardrobeState import gg.essential.gui.wardrobe.components.* import gg.essential.gui.wardrobe.something.CosmeticGroup +import gg.essential.mod.cosmetics.featured.BlankDivider import gg.essential.mod.cosmetics.featured.FeaturedItem +import gg.essential.mod.cosmetics.featured.FeaturedItemRow +import gg.essential.mod.cosmetics.featured.TextDivider import gg.essential.util.scrollToTopOf fun LayoutScope.featuredCategory(wardrobeState: WardrobeState, scroller: ScrollComponent, modifier: Modifier = Modifier) { val layoutState = wardrobeState.featuredPageLayout val cosmeticItemHeight = cosmeticWidth + cosmeticTextHeight + fun LayoutScope.featuredItemRow(rowIndex: Int, row: FeaturedItemRow, layoutWidth: FeaturedPageWidth, emptySlots: MutableSet>) { + val verticalPosition = rowIndex * cosmeticItemHeight + rowIndex * cosmeticYSpacing + cosmeticXSpacing + var itemIndex = 0 + for (columnIndex in 0 until layoutWidth) { + // If we ran out of items in this row, break + if (itemIndex >= row.items.size) break + + // If the slot should be empty, we skip it + if (emptySlots.contains(Pair(rowIndex, columnIndex))) continue + + val horizontalPosition = columnIndex * cosmeticWidth + columnIndex * cosmeticXSpacing + val featuredItem = row.items[itemIndex++] + val itemWidth = featuredItem.width + val itemHeight = featuredItem.height + + // Add all additional slots this item occupies to the list of empty slots + for (w in 0 until itemWidth) { + for (h in 0 until itemHeight) { + emptySlots.add(Pair(rowIndex + h, columnIndex + w)) + } + } + + if (featuredItem is FeaturedItem.Empty) + continue + + box(Modifier.alignVertical(Alignment.Start(verticalPosition)).alignHorizontal(Alignment.Start(horizontalPosition))) { + bind(featuredItem.toModItem(wardrobeState)) { item -> + if (item == null) { + text("Error loading item.") + } else { + cosmeticItem( + item, + WardrobeCategory.FeaturedRefresh, + wardrobeState, + Modifier.itemSize(itemWidth, itemHeight) + ) + } + } + } + } + } + var content: UIComponent? = null bind(layoutState) { (layoutsEmpty, layoutEntry) -> @@ -44,50 +92,38 @@ fun LayoutScope.featuredCategory(wardrobeState: WardrobeState, scroller: ScrollC return@bind } val (layoutWidth, layout) = layoutEntry - val totalHeight = layout.rows.size * cosmeticItemHeight + (layout.rows.size - 1).coerceAtLeast(0) * cosmeticYSpacing + 2 * cosmeticXSpacing - // Slots that should be left empty because a bigger item spans over them - val emptySlots = mutableSetOf>() - - content = box(Modifier.height(totalHeight).then(modifier)) { - for ((rowIndex, row) in layout.rows.withIndex()) { - val verticalPosition = rowIndex * cosmeticItemHeight + rowIndex * cosmeticYSpacing + cosmeticXSpacing - var itemIndex = 0 - for (columnIndex in 0 until layoutWidth) { - // If we ran out of items in this row, break - if (itemIndex >= row.size) break - - // If the slot should be empty, we skip it - if (emptySlots.contains(Pair(rowIndex, columnIndex))) continue - - val horizontalPosition = columnIndex * cosmeticWidth + columnIndex * cosmeticXSpacing - val featuredItem = row[itemIndex++] - val itemWidth = featuredItem.width - val itemHeight = featuredItem.height - - // Add all additional slots this item occupies to the list of empty slots - for (w in 0 until itemWidth) { - for (h in 0 until itemHeight) { - emptySlots.add(Pair(rowIndex + h, columnIndex + w)) - } - } - if (featuredItem is FeaturedItem.Empty) - continue - - box(Modifier.alignVertical(Alignment.Start(verticalPosition)).alignHorizontal(Alignment.Start(horizontalPosition))) { - bind(featuredItem.toModItem(wardrobeState)) { item -> - if (item == null) { - text("Error loading item.") - } else { - cosmeticItem( - item, - WardrobeCategory.FeaturedRefresh, - wardrobeState, - Modifier.itemSize(itemWidth, itemHeight) - ) - } + content = column(modifier) { + val rowsRun = mutableListOf() + for (component in layout.rows + null) { + if (component is FeaturedItemRow) { + rowsRun.add(component) + continue + } else if (rowsRun.isNotEmpty()) { + val totalHeight = rowsRun.size * cosmeticItemHeight + (rowsRun.size - 1).coerceAtLeast(0) * cosmeticYSpacing + 2 * cosmeticXSpacing + // Slots that should be left empty because a bigger item spans over them + val emptySlots = mutableSetOf>() + box(Modifier.height(totalHeight).fillWidth()) { + for ((rowIndex, row) in rowsRun.withIndex()) { + featuredItemRow(rowIndex, row, layoutWidth, emptySlots) } } + rowsRun.clear() + } + when (component) { + is BlankDivider -> { + spacer(height = 1f) + } + is TextDivider -> { + spacer(height = 7f) + ColoredDivider( + component.text, + dividerColor = EssentialPalette.BUTTON, + shadowColor = EssentialPalette.COMPONENT_BACKGROUND + )() + } + is FeaturedItemRow -> throw IllegalStateException("FeaturedItemRow should already be processed") + null -> {} } } } diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/components/cosmeticItem.kt b/src/main/kotlin/gg/essential/gui/wardrobe/components/cosmeticItem.kt index 85f1433..bae8a13 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/components/cosmeticItem.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/components/cosmeticItem.kt @@ -12,16 +12,8 @@ package gg.essential.gui.wardrobe.components import gg.essential.Essential -import gg.essential.elementa.UIComponent import gg.essential.elementa.components.GradientComponent -import gg.essential.elementa.components.UIContainer -import gg.essential.elementa.components.Window -import gg.essential.elementa.constraints.MousePositionConstraint -import gg.essential.elementa.dsl.basicXConstraint -import gg.essential.elementa.dsl.basicYConstraint -import gg.essential.elementa.dsl.constrain import gg.essential.elementa.dsl.minus -import gg.essential.elementa.dsl.pixels import gg.essential.elementa.dsl.plus import gg.essential.elementa.dsl.provideDelegate import gg.essential.elementa.events.UIClickEvent @@ -29,7 +21,6 @@ import gg.essential.elementa.font.DefaultFonts import gg.essential.elementa.utils.withAlpha import gg.essential.gui.EssentialPalette import gg.essential.gui.common.CosmeticPreview -import gg.essential.gui.common.EssentialTooltip import gg.essential.gui.common.SequenceAnimatedUIImage import gg.essential.gui.common.bundleRenderPreview import gg.essential.gui.common.modal.OpenLinkModal @@ -60,16 +51,11 @@ import gg.essential.mod.cosmetics.settings.CosmeticSetting import gg.essential.model.util.toJavaColor import gg.essential.network.connectionmanager.coins.CoinsManager import gg.essential.network.connectionmanager.cosmetics.AssetLoader -import gg.essential.network.cosmetics.Cosmetic -import gg.essential.universal.UMouse import gg.essential.universal.USound import gg.essential.util.MinecraftUtils import gg.essential.util.Multithreading import gg.essential.util.UuidNameLookup -import gg.essential.util.findParentOfTypeOrNull import gg.essential.gui.util.hoverScope -import gg.essential.gui.util.isInComponentTree -import gg.essential.gui.util.onAnimationFrame import gg.essential.util.onRightClick import gg.essential.gui.util.pollingState import gg.essential.gui.wardrobe.* @@ -82,7 +68,6 @@ import gg.essential.vigilance.utils.onLeftClick import java.awt.Color import java.net.URI import java.util.concurrent.TimeUnit -import kotlin.math.abs data class CosmeticItemTag(val item: Item) : Tag @@ -104,7 +89,7 @@ fun LayoutScope.cosmeticItem(item: Item, category: WardrobeCategory, state: Ward val isEmote = slot == CosmeticSlot.EMOTE when { state.selectedBundle() != null -> false - isEmote && inEmoteWheel -> state.emoteWheel().contains(item.cosmetic.id) + isEmote && inEmoteWheel -> state.emoteWheelManager.selectedEmoteWheelSlots().contains(item.cosmetic.id) selectedEmote != null -> selectedEmote.itemId == item.itemId !isEmote && !inEmoteWheel -> state.equippedCosmeticsState()[slot] == item.cosmetic.id else -> false @@ -210,7 +195,7 @@ fun LayoutScope.cosmeticItem(item: Item, category: WardrobeCategory, state: Ward // This is used to ensure featured page items initially show as they were configured val settings = state.selectedPreviewingEquippedSettings.map { settings -> val variantSetting = settings[item.cosmetic.id]?.setting() - ?: item.settingsOverride?.setting() + ?: item.settingsOverride.setting() listOfNotNull(variantSetting) } CosmeticPreview(item.cosmetic, settings)(Modifier.fillParent()) @@ -277,130 +262,14 @@ fun LayoutScope.cosmeticItem(item: Item, category: WardrobeCategory, state: Ward }.onRightClick { handleItemRightClick(item, category, state, it) }.apply { - // This has been copied from EmoteWheelPage, if something is changed here, you should probably change it there too. - if (item is Item.CosmeticOrEmote && item.cosmetic.type.slot == CosmeticSlot.EMOTE) { - fun LayoutScope.thumbnail(cosmetic: Cosmetic, modifier: Modifier): UIComponent { - return CosmeticPreview(cosmetic)(modifier) - } - - fun tooltip(parent: UIComponent, text: String) = - EssentialTooltip(parent, position = EssentialTooltip.Position.RIGHT, notchSize = 0) - .constrain { - x = MousePositionConstraint() + 10.pixels - y = MousePositionConstraint() - 15.pixels - } - .addLine(text) - - fun isDraggable(): Boolean { - return item.id in state.unlockedCosmetics.getUntracked() - } - - var maybeDragging = false - var clickStart = Pair(0f, 0f) - val dragging = mutableStateOf(false) - - val draggable by lazy { - val container = object : UIContainer() { - // when we hitTest, we want the thing below the dragging graphic - override fun isPointInside(x: Float, y: Float): Boolean = false - - // `getMousePosition` is protected in UIComponent, so we have to expose it here - fun mousePosition() = getMousePosition() - } - - container.layout(EmoteWheelPage.slotModifier) { - thumbnail(item.cosmetic, Modifier.fillParent()) - } - - container.onAnimationFrame { - val (mouseX, mouseY) = container.mousePosition() - val target = Window.of(container).hitTest(mouseX, mouseY) - val slotTarget = target as? EmoteWheelPage.EmoteSlot - ?: target.findParentOfTypeOrNull() - val removeTarget = target.findParentOfTypeOrNull() - state.draggingOntoEmoteSlot.set(when { - slotTarget != null -> slotTarget.index - removeTarget != null -> -1 - else -> null - }) - } - - val nameTooltip by tooltip(this, item.cosmetic.displayName) - val replace by tooltip(container, "Replace") - replace.bindVisibility(state.draggingOntoOccupiedEmoteSlot and dragging) - nameTooltip.bindVisibility(!state.draggingOntoOccupiedEmoteSlot and dragging) - - container.onMouseRelease { - val target = state.draggingOntoEmoteSlot.get() - if (target != null && target != -1) { - if (item.cosmetic.isCosmeticFree && !owned.get()) { - claimFreeItemNow(item, state) - } - - state.emoteWheelManager.setEmote(target, item.cosmetic.id) - USound.playButtonPress() - } - - state.draggingEmoteSlot.set(null) - state.draggingOntoEmoteSlot.set(null) - - Window.enqueueRenderOperation { - hide(instantly = true) - } - maybeDragging = false - dragging.set(false) - } - - container - } - - onLeftClick { - val xOffset = UMouse.Scaled.x.toFloat() - getLeft() - val yOffset = UMouse.Scaled.y.toFloat() - getTop() - - clickStart = Pair(xOffset, yOffset) - maybeDragging = true - - it.stopPropagation() - } - - onMouseRelease { - // The click was not a drag, call the click handler - if (!dragging.get() && isHovered() && maybeDragging) { - handleCosmeticOrEmoteLeftClick(item, category, state) - } - maybeDragging = false - } - - onMouseDrag { mouseX, mouseY, _ -> - if (!maybeDragging) { - return@onMouseDrag - } - if (!isDraggable()) { - return@onMouseDrag - } - val distance = abs(clickStart.first - mouseX) + abs(clickStart.second - mouseY) - if (distance > 5 && !dragging.get()) { - - draggable.constrain { - x = MousePositionConstraint() - basicXConstraint { - draggable.getWidth() / 2 - } - y = MousePositionConstraint() - basicYConstraint { - draggable.getHeight() / 2 - } - } - Window.enqueueRenderOperation { - if (!draggable.isInComponentTree()) { - Window.of(this).addChild(draggable) - } - state.draggingEmoteSlot.set(-1) - } - dragging.set(true) - } - } - } else { - onLeftClick { + onLeftClick { + if (item is Item.CosmeticOrEmote && item.cosmetic.type.slot == CosmeticSlot.EMOTE) { + state.draggingEmote.set(WardrobeState.DraggedEmote( + item.id, + clickOffset = Pair(EmoteWheelPage.SLOT_SIZE / 2, EmoteWheelPage.SLOT_SIZE / 2), + onInstantLeftClick = { handleCosmeticOrEmoteLeftClick(item, category, state) } + )) + } else { handleItemLeftClick(item, category, state, it) } } @@ -825,7 +694,7 @@ private fun LayoutScope.colorBar(item: Item, category: WardrobeCategory, wardrob // If the user doesn't have a variant selected yet, use the variant from the overrides if they have one, otherwise default // This is used to ensure featured page items show as configured initially, before the user manually sets a color val selected = wardrobeState.getVariant(item).map { variant -> - variant ?: (item.settingsOverride?.setting() ?: item.cosmetic.defaultVariantSetting)?.data?.variant + variant ?: (item.settingsOverride.setting() ?: item.cosmetic.defaultVariantSetting)?.data?.variant } column(Modifier.alignHorizontal(Alignment.End(2f))) { diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/components/cosmeticOrEmoteItemFunctions.kt b/src/main/kotlin/gg/essential/gui/wardrobe/components/cosmeticOrEmoteItemFunctions.kt index a3367f9..3166c1b 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/components/cosmeticOrEmoteItemFunctions.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/components/cosmeticOrEmoteItemFunctions.kt @@ -90,21 +90,22 @@ fun handleCosmeticOrEmoteLeftClick(item: Item.CosmeticOrEmote, category: Wardrob } if (wardrobeState.inEmoteWheel.get()) { - val emoteWheel = wardrobeState.emoteWheel - val existingIndex = emoteWheel.getUntracked().indexOf(cosmetic.id) + val selectedEmoteWheelId = wardrobeState.emoteWheelManager.selectedEmoteWheelId.getUntracked() ?: return + val selectedEmoteWheelSlots = wardrobeState.emoteWheelManager.selectedEmoteWheelSlots + val existingIndex = selectedEmoteWheelSlots.getUntracked().indexOf(cosmetic.id) if (existingIndex != -1) { if (startedInEmoteWheel && !bundleWasSelected && !emoteWasSelected) { // Only remove the emote if the emote wheel preview was open when the emote was clicked and a bundle was not selected - wardrobeState.emoteWheelManager.setEmote(existingIndex, null) + wardrobeState.emoteWheelManager.setEmote(selectedEmoteWheelId, existingIndex, null) // Remove duplicates as well - while (emoteWheel.getUntracked().indexOf(cosmetic.id) != -1) { - wardrobeState.emoteWheelManager.setEmote(emoteWheel.getUntracked().indexOf(cosmetic.id), null) + while (selectedEmoteWheelSlots.getUntracked().indexOf(cosmetic.id) != -1) { + wardrobeState.emoteWheelManager.setEmote(selectedEmoteWheelId, selectedEmoteWheelSlots.getUntracked().indexOf(cosmetic.id), null) } } } else { - val emptyIndex = emoteWheel.getUntracked().indexOfFirst { it == null } + val emptyIndex = selectedEmoteWheelSlots.getUntracked().indexOfFirst { it == null } if (emptyIndex != -1) { - wardrobeState.emoteWheelManager.setEmote(emptyIndex, cosmetic.id) + wardrobeState.emoteWheelManager.setEmote(selectedEmoteWheelId, emptyIndex, cosmetic.id) } else { Notifications.warning("Emote wheel is full.", "") } @@ -138,21 +139,18 @@ fun handleCosmeticOrEmoteLeftClick(item: Item.CosmeticOrEmote, category: Wardrob private fun updateSettingsToOverriddenIfNotSet(item: Item.CosmeticOrEmote, wardrobeState: WardrobeState) { // If we select an item with overridden settings, we override the player's settings too, if they don't have them already set // Used by the featured page, since it initially show the item with overridden settings, so those should apply those when first selected - val settingsOverride = item.settingsOverride - if (settingsOverride != null) { - for (setting in settingsOverride) { - when { - setting is CosmeticSetting.Variant -> { - if (wardrobeState.getVariant(item).get() == null) wardrobeState.setVariant(item, setting.data.variant) - } + for (setting in item.settingsOverride) { + when { + setting is CosmeticSetting.Variant -> { + if (wardrobeState.getVariant(item).get() == null) wardrobeState.setVariant(item, setting.data.variant) + } - setting is CosmeticSetting.PlayerPositionAdjustment -> { - if (wardrobeState.getSelectedPosition(item).get() == null) wardrobeState.setSelectedPosition(item, Triple(setting.data.x, setting.data.y, setting.data.z)) - } + setting is CosmeticSetting.PlayerPositionAdjustment -> { + if (wardrobeState.getSelectedPosition(item).get() == null) wardrobeState.setSelectedPosition(item, Triple(setting.data.x, setting.data.y, setting.data.z)) + } - setting is CosmeticSetting.Side -> { - if (wardrobeState.getSelectedSide(item).get() == null) wardrobeState.setSelectedSide(item, setting.data.side) - } + setting is CosmeticSetting.Side -> { + if (wardrobeState.getSelectedSide(item).get() == null) wardrobeState.setSelectedSide(item, setting.data.side) } } } diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/components/draggingEmote.kt b/src/main/kotlin/gg/essential/gui/wardrobe/components/draggingEmote.kt new file mode 100644 index 0000000..f54662d --- /dev/null +++ b/src/main/kotlin/gg/essential/gui/wardrobe/components/draggingEmote.kt @@ -0,0 +1,110 @@ +/* + * Copyright (c) 2024 ModCore Inc. All rights reserved. + * + * This code is part of ModCore Inc.'s Essential Mod repository and is protected + * under copyright registration # TX0009138511. For the full license, see: + * https://github.com/EssentialGG/Essential/blob/main/LICENSE + * + * You may not use, copy, reproduce, modify, sell, license, distribute, + * commercialize, or otherwise exploit, or create derivative works based + * upon, this file or any other in this repository, all of which is reserved by Essential. + */ +package gg.essential.gui.wardrobe.components + +import gg.essential.elementa.components.Window +import gg.essential.elementa.constraints.MousePositionConstraint +import gg.essential.elementa.dsl.minus +import gg.essential.elementa.dsl.pixels +import gg.essential.gui.common.CosmeticPreview +import gg.essential.gui.common.EssentialTooltip +import gg.essential.gui.elementa.state.v2.effect +import gg.essential.gui.elementa.state.v2.memo +import gg.essential.gui.elementa.state.v2.mutableStateOf +import gg.essential.gui.layoutdsl.Alignment +import gg.essential.gui.layoutdsl.BasicXModifier +import gg.essential.gui.layoutdsl.BasicYModifier +import gg.essential.gui.layoutdsl.LayoutScope +import gg.essential.gui.layoutdsl.Modifier +import gg.essential.gui.layoutdsl.alignBoth +import gg.essential.gui.layoutdsl.floatingBox +import gg.essential.gui.layoutdsl.height +import gg.essential.gui.layoutdsl.then +import gg.essential.gui.layoutdsl.tooltip +import gg.essential.gui.layoutdsl.width +import gg.essential.gui.util.getTag +import gg.essential.gui.util.layoutSafePollingState +import gg.essential.gui.util.selfAndParents +import gg.essential.gui.wardrobe.EmoteWheelPage +import gg.essential.gui.wardrobe.WardrobeContainer +import gg.essential.gui.wardrobe.WardrobeState +import gg.essential.universal.UMouse +import gg.essential.util.findParentOfTypeOrNull +import kotlin.math.abs + +fun LayoutScope.draggingEmote(state: WardrobeState, draggedEmote: WardrobeState.DraggedEmote) { + val clickStart = Pair(UMouse.Scaled.x.toFloat(), UMouse.Scaled.y.toFloat()) + val isFromWardrobe = draggedEmote.from == null + val cosmeticId = draggedEmote.emoteId + val cosmetic = cosmeticId?.let { state.cosmeticsData.getCosmetic(it) } + val clickOffset = draggedEmote.clickOffset + val mayBeClick = mutableStateOf(true) + + val positioning = Modifier.then(BasicXModifier { MousePositionConstraint() - clickOffset.first.pixels }) + .then(BasicYModifier { MousePositionConstraint() - clickOffset.second.pixels }) + + val container = floatingBox(Modifier.width(0f).height(0f).then(positioning)) { // Zero size so it cannot be hovered + if (cosmetic != null) { + val tooltip = Modifier.then(memo { + val tooltipText = when { + isFromWardrobe && state.draggingOntoOccupiedEmoteSlot() -> "Replace" + state.draggingOntoOccupiedEmoteSlot() -> "Swap" + !isFromWardrobe && state.draggingEmote()?.to == null -> "Remove" + else -> cosmetic.displayName + } + Modifier.tooltip(tooltipText, position = EssentialTooltip.Position.MOUSE_OFFSET(10f, -15f), notchSize = 0) + }) + + if_({ !isFromWardrobe || !mayBeClick() }) { + CosmeticPreview(cosmetic)(EmoteWheelPage.slotModifier.alignBoth(Alignment.Start).then(tooltip)) + } + } + }.onMouseDrag { _, _, _ -> + val distance = abs(UMouse.Scaled.x.toFloat() - clickStart.first) + abs(UMouse.Scaled.y.toFloat() - clickStart.second) + if (distance > 5) { + mayBeClick.set(false) + } + }.onMouseRelease { + Window.enqueueRenderOperation { + val dragged = state.draggingEmote.getUntracked() ?: return@enqueueRenderOperation + if (!mayBeClick.getUntracked()) { + val manager = state.emoteWheelManager + val targetEmote = dragged.to?.let { manager.getEmoteWheel(it.emoteWheelId)?.slots?.get(it.slotIndex) } + dragged.from?.let { manager.setEmote(it.emoteWheelId, it.slotIndex, targetEmote) } + dragged.to?.let { manager.setEmote(it.emoteWheelId, it.slotIndex, cosmeticId) } + } else { + dragged.onInstantLeftClick() + } + state.draggingEmote.set(null) + } + } + + val targetState = container.layoutSafePollingState { + val mouseX = UMouse.Scaled.x.toFloat() + val mouseY = UMouse.Scaled.y.toFloat() + Window.of(container).hitTest(mouseX, mouseY) + } + + effect(container) { + val target = targetState() + val slotTarget = target.selfAndParents().firstNotNullOfOrNull { it.getTag()?.emoteSlotId } + val removeTarget = target.findParentOfTypeOrNull() + state.draggingEmote.set { draggingEmote -> + val to = when { + slotTarget != null -> WardrobeState.EmoteSlotId(slotTarget.emoteWheelId, slotTarget.slotIndex) + removeTarget != null -> null + else -> draggingEmote?.from + } + draggingEmote?.copy(to = to) ?: draggingEmote + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/components/previewWindow.kt b/src/main/kotlin/gg/essential/gui/wardrobe/components/previewWindow.kt index d6e5568..0eed4b1 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/components/previewWindow.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/components/previewWindow.kt @@ -63,6 +63,8 @@ import gg.essential.gui.notification.Notifications import gg.essential.gui.util.hoverScope import gg.essential.gui.util.layoutSafePollingState import gg.essential.gui.util.makeHoverScope +import gg.essential.gui.util.onAnimationFrame +import gg.essential.gui.util.selfAndParents import gg.essential.gui.wardrobe.EmoteWheelPage import gg.essential.gui.wardrobe.Item import gg.essential.gui.wardrobe.WardrobeCategory @@ -87,12 +89,19 @@ import gg.essential.network.connectionmanager.cosmetics.AssetLoader import gg.essential.network.connectionmanager.features.Feature import gg.essential.network.cosmetics.Cosmetic import gg.essential.universal.UKeyboard +import gg.essential.universal.UMouse import gg.essential.universal.USound +import gg.essential.util.Client import gg.essential.util.GuiUtil import gg.essential.util.findChildOfTypeOrNull import gg.essential.util.onLeftClick import gg.essential.util.scrollGradient import gg.essential.util.toState +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.cancel +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch import java.util.UUID import kotlin.math.abs import kotlin.math.round @@ -133,6 +142,11 @@ fun LayoutScope.previewWindowTitleBar(state: WardrobeState, modifier: Modifier) model.soundData != null } + var leftButton: UIComponent? = null + var rightButton: UIComponent? = null + val leftFlash = mutableStateOf(false) + val rightFlash = mutableStateOf(false) + val flashModifier = Modifier.outline(EssentialPalette.TEXT, 1f, true) fun LayoutScope.titleBarButton(modifier: Modifier, block: LayoutScope.() -> Unit = {}): UIComponent { return box(modifier.width(17f).heightAspect(1f).shadow(EssentialPalette.BLACK), block) @@ -145,7 +159,7 @@ fun LayoutScope.previewWindowTitleBar(state: WardrobeState, modifier: Modifier) } } - box(modifier) { + val container = box(modifier) { val titleBarBox = box(Modifier.fillParent()) val selectorModifier = BasicXModifier { (CenterConstraint() boundTo titleBarBox).coerceIn(0.pixels, 0.pixels(true)) }.then(BasicWidthModifier { 100.percent.coerceAtMost(115.pixels) }) row(Modifier.fillWidth(padding = 10f), Arrangement.spacedBy(3f, FloatPosition.START)) { @@ -169,7 +183,8 @@ fun LayoutScope.previewWindowTitleBar(state: WardrobeState, modifier: Modifier) row(selectorModifier, Arrangement.spacedBy(3f, FloatPosition.CENTER)) { if_(regularContent) { if_(state.inEmoteWheel or (!emoteSelected and !bundleSelected)) { - titleBarButton(Modifier.color(EssentialPalette.GRAY_BUTTON).hoverColor(EssentialPalette.GRAY_BUTTON_HOVER).hoverScope()) { + leftButton = titleBarButton(Modifier.color(EssentialPalette.GRAY_BUTTON) + .hoverColor(EssentialPalette.GRAY_BUTTON_HOVER).whenTrue(leftFlash, flashModifier).hoverScope()) { icon(EssentialPalette.ARROW_LEFT_4X7, Modifier.color(EssentialPalette.TEXT).hoverColor(EssentialPalette.TEXT_HIGHLIGHT)) }.onLeftClick { handleClick(it) { changeEmoteWheelOrOutfit(state, -1) } } } @@ -199,7 +214,8 @@ fun LayoutScope.previewWindowTitleBar(state: WardrobeState, modifier: Modifier) } } if_(state.inEmoteWheel or (!emoteSelected and !bundleSelected)) { - titleBarButton(Modifier.color(EssentialPalette.GRAY_BUTTON).hoverColor(EssentialPalette.GRAY_BUTTON_HOVER).hoverScope()) { + rightButton = titleBarButton(Modifier.color(EssentialPalette.GRAY_BUTTON) + .hoverColor(EssentialPalette.GRAY_BUTTON_HOVER).whenTrue(rightFlash, flashModifier).hoverScope()) { icon(EssentialPalette.ARROW_RIGHT_4X7, Modifier.color(EssentialPalette.TEXT).hoverColor(EssentialPalette.TEXT_HIGHLIGHT)) }.onLeftClick { handleClick(it) { changeEmoteWheelOrOutfit(state, 1) } } } @@ -231,6 +247,50 @@ fun LayoutScope.previewWindowTitleBar(state: WardrobeState, modifier: Modifier) } } } + + // Animation and behavior for dragging an emote over an emote wheel switch button + var targetButton: UIComponent? = null + var animationScope: CoroutineScope? = null + + fun resetDragAnimation() { + leftFlash.set(false) + rightFlash.set(false) + animationScope?.cancel() + animationScope = null + } + + container.onAnimationFrame { + if (leftButton == null || rightButton == null || state.draggingEmote.getUntracked() == null) { + if (animationScope != null) { + resetDragAnimation() + } + return@onAnimationFrame + } + + // Get which button, if any, the emote is being dragged on top of + val (mouseX, mouseY) = UMouse.Scaled.x.toFloat() to UMouse.Scaled.y.toFloat() + val target = Window.of(container).hitTest(mouseX, mouseY).selfAndParents().firstOrNull { it == leftButton || it == rightButton } + if (target != targetButton) { + // Target changed, reset animation + targetButton = target + resetDragAnimation() + } + + if (targetButton != null && animationScope == null) { + val isLeftButton = target == leftButton + + animationScope = CoroutineScope(Dispatchers.Client) + animationScope?.launch { + delay(750) + repeat(3) { _ -> + (if (isLeftButton) leftFlash else rightFlash).set { !it } + delay(125) + } + changeEmoteWheelOrOutfit(state, if (isLeftButton) -1 else 1) + resetDragAnimation() + } + } + } } fun LayoutScope.previewWindow(state: WardrobeState, modifier: Modifier, bottomDivider: UIComponent) { diff --git a/src/main/kotlin/gg/essential/gui/wardrobe/modals/PurchaseConfirmationModal.kt b/src/main/kotlin/gg/essential/gui/wardrobe/modals/PurchaseConfirmationModal.kt index 89a9fea..ea7c2b4 100644 --- a/src/main/kotlin/gg/essential/gui/wardrobe/modals/PurchaseConfirmationModal.kt +++ b/src/main/kotlin/gg/essential/gui/wardrobe/modals/PurchaseConfirmationModal.kt @@ -91,24 +91,20 @@ class PurchaseConfirmationModal( box(Modifier.fillWidth().height(1f).color(EssentialPalette.BUTTON)) - totalAndDiscount() - } - } - } - } + if_({ discountAmount() != 0 }) { + listEntry( + "Discount", + discountAmount, + nameModifier = Modifier.color(EssentialPalette.GREEN), + costModifier = Modifier.color(EssentialPalette.TEXT_HIGHLIGHT), + negative = true + ) + box(Modifier.fillWidth().height(1f).color(EssentialPalette.BUTTON)) + } - private fun LayoutScope.totalAndDiscount() { - column(Modifier.fillWidth(), Arrangement.spacedBy(5f)) { - if_({ discountAmount() != 0 }) { - listEntry( - "Discount", - discountAmount, - nameModifier = Modifier.color(EssentialPalette.GREEN), - costModifier = Modifier.color(EssentialPalette.TEXT_HIGHLIGHT), - ) + listEntry("Total", totalAmount, Modifier.color(EssentialPalette.TEXT_HIGHLIGHT)) + } } - - listEntry("Total", totalAmount, Modifier.color(EssentialPalette.TEXT_HIGHLIGHT)) } } @@ -126,6 +122,7 @@ class PurchaseConfirmationModal( cost: State, nameModifier: Modifier = Modifier, costModifier: Modifier = nameModifier, + negative: Boolean = false, ) { val defaultTextModifier = Modifier.color(EssentialPalette.TEXT_MID_GRAY).shadow(Color.BLACK) @@ -140,7 +137,7 @@ class PurchaseConfirmationModal( bind(cost) { costAmount -> wrappedText( - "${CoinsManager.COIN_FORMAT.format(costAmount)}{coin-icon}", + "${if(negative) "-" else ""}${CoinsManager.COIN_FORMAT.format(costAmount)}{coin-icon}", textModifier = defaultTextModifier.then(costModifier), ) { "coin-icon" { diff --git a/src/main/kotlin/gg/essential/sps/WindowTitleManager.kt b/src/main/kotlin/gg/essential/sps/WindowTitleManager.kt new file mode 100644 index 0000000..0ca5d72 --- /dev/null +++ b/src/main/kotlin/gg/essential/sps/WindowTitleManager.kt @@ -0,0 +1,72 @@ +/* + * Copyright (c) 2024 ModCore Inc. All rights reserved. + * + * This code is part of ModCore Inc.'s Essential Mod repository and is protected + * under copyright registration # TX0009138511. For the full license, see: + * https://github.com/EssentialGG/Essential/blob/main/LICENSE + * + * You may not use, copy, reproduce, modify, sell, license, distribute, + * commercialize, or otherwise exploit, or create derivative works based + * upon, this file or any other in this repository, all of which is reserved by Essential. + */ +package gg.essential.sps + +import gg.essential.config.EssentialConfig +import gg.essential.data.VersionData.getMinecraftVersion +import gg.essential.gui.elementa.state.v2.ReferenceHolderImpl +import gg.essential.gui.elementa.state.v2.effect +import gg.essential.universal.UI18n +import gg.essential.universal.UMinecraft +import gg.essential.util.ServerType + +//#if MC>=11600 +//#else +import org.lwjgl.opengl.Display +//#endif + +object WindowTitleManager { + + val referenceHolder = ReferenceHolderImpl() + init { + + effect(referenceHolder) { + // Run effect when config value changes + EssentialConfig.replaceWindowTitleState() + + updateTitle() + } + } + + fun updateTitle() { + //#if MC>=11600 + //$$ UMinecraft.getMinecraft().setDefaultMinecraftTitle() + //#else + if (EssentialConfig.replaceWindowTitle) { + Display.setTitle(this.createTitle()) + } + //#endif + } + + private fun createTitle(): String { + val mc = UMinecraft.getMinecraft() + + val builder = StringBuilder("Minecraft* ").append(getMinecraftVersion()) + val netHandler = UMinecraft.getNetHandler() + + if (netHandler != null && netHandler.networkManager.isChannelOpen) { + builder.append(" - ") + + val currentServer = mc.currentServerData + + builder.append(UI18n.i18n(when (ServerType.current()) { + is ServerType.Singleplayer -> "title.singleplayer" + is ServerType.SPS -> "title.multiplayer.hosted" + is ServerType.Multiplayer -> if(currentServer != null && currentServer.isOnLAN) + "title.multiplayer.lan" else "title.multiplayer.other" + is ServerType.Realms -> "title.multiplayer.realms" + else -> "title.multiplayer.other" + })) + } + return builder.toString() + } +} \ No newline at end of file diff --git a/src/main/resources/assets/essential/commit.txt b/src/main/resources/assets/essential/commit.txt index aa5a928..6410ed4 100644 --- a/src/main/resources/assets/essential/commit.txt +++ b/src/main/resources/assets/essential/commit.txt @@ -1 +1 @@ -572d73d0fd \ No newline at end of file +9e33c385e4 \ No newline at end of file diff --git a/src/main/resources/assets/essential/lang/en_us.lang b/src/main/resources/assets/essential/lang/en_us.lang index c8bf9ad..080feb1 100644 --- a/src/main/resources/assets/essential/lang/en_us.lang +++ b/src/main/resources/assets/essential/lang/en_us.lang @@ -352,3 +352,10 @@ gamerule.category.chat=Chat gamerule.category.miscellaneous=Miscellaneous gamerule.category.other=Other gamerule.category.pinned=Pinned + +## Window titles +title.singleplayer=Singleplayer +title.multiplayer.realms=Multiplayer (Realms) +title.multiplayer.lan=Multiplayer (LAN) +title.multiplayer.other=Multiplayer (3rd-party Server) +title.multiplayer.hosted=Multiplayer (Hosted World) diff --git a/src/main/resources/mixins.essential.json b/src/main/resources/mixins.essential.json index af7e040..298d2bb 100644 --- a/src/main/resources/mixins.essential.json +++ b/src/main/resources/mixins.essential.json @@ -40,6 +40,10 @@ "client.gui.Mixin_SelectionListDividers_ServerSelectionList", "client.gui.Mixin_UI3DPlayer_Camera", "client.gui.Mixin_UnfocusTextFieldWhileOverlayHasFocus", + "client.gui.Mixin_UpdateWindowTitle_AddSPSTitle", + "client.gui.Mixin_UpdateWindowTitle_DisplayScreen", + "client.gui.Mixin_UpdateWindowTitle_LoadWorld", + "client.gui.Mixin_UpdateWindowTitle_OpenToLan", "client.gui.MixinGuiChat", "client.gui.MixinGuiInventory_UI3DPlayerOffset", "client.gui.MixinGuiMainMenu", @@ -54,7 +58,7 @@ "client.gui.Mixin_SkipLanScanningEntryForCustomTabs", "client.gui.ServerListEntryNormalAccessor", "client.gui.ServerSelectionListAccessor", - "client.gui.inventory.Mixin_DisableCosmeticsInInventory", + "client.gui.inventory.Mixin_TrackInventoryPlayerRendering", "client.model.Mixin_ExtraTransform_CopyBetweenModelParts", "client.model.Mixin_PlayerEntityRenderStateExt", "client.model.Mixin_PlayerEntityRenderStateExt_UpdateRenderState", diff --git a/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/WearablesManager.kt b/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/WearablesManager.kt index aa61713..b13f2ff 100644 --- a/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/WearablesManager.kt +++ b/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/WearablesManager.kt @@ -79,11 +79,42 @@ class WearablesManager( updateState(state.copyWithout(slot)) } - /** @see ModelInstance.update */ + private var lastUpdateTime = Float.NEGATIVE_INFINITY + + /** + * Updates the state of the models for the current frame prior to rendering. + * + * Note that new animation events are emitted into [ModelAnimationState.pendingEvents] and the caller needs to + * collect them from there and forward them to the actual particle/sound/etc system at the appropriate time. + * + * Also note that this does **not** yet update the locators bound to these model instances. For that one must call + * [updateLocators] after rendering. + * This is because the position and rotation of locators depends on the final rendered player pose, which is only + * available after rendering. + * Because particle events may depend on the position of locators, they should however ideally be updated before + * particles are updated, rendered, and/or spawned. + */ fun update() { - for ((_, model) in models) { - model.update() + if (models.isEmpty()) return + + val now = entity.lifeTime + if (lastUpdateTime == now) return // was already updated this frame + lastUpdateTime = now + + val modelInstances = models.values + + // update animations + modelInstances.forEach { + it.essentialAnimationSystem.maybeFireTextureAnimationStartEvent() + it.essentialAnimationSystem.updateAnimationState() } + + // trigger any animations that are supposed to be triggered in other models + // run after all models have already updated without this interference + modelInstances.forEach { it.essentialAnimationSystem.triggerPendingAnimationsForOtherModels(modelInstances) } + + // update effects after all animations have been updated + modelInstances.forEach { it.animationState.updateEffects() } } /** @see ModelInstance.updateLocators */ diff --git a/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/events/AnimationEvent.kt b/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/events/AnimationEvent.kt index e966642..8f03252 100644 --- a/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/events/AnimationEvent.kt +++ b/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/events/AnimationEvent.kt @@ -22,6 +22,8 @@ data class AnimationEvent( val name: String, @SerialName("on_complete") val onComplete: AnimationEvent? = null, + @SerialName("trigger_in_other_cosmetic") + val triggerInOtherCosmetic: Set = setOf(), // strings only as they are references to another already setup animation event in a different cosmetic val probability: Float = 1f, val skips: Int = 0, val loops: Int = 0, diff --git a/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/events/AnimationEventType.kt b/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/events/AnimationEventType.kt index b234360..568b3db 100644 --- a/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/events/AnimationEventType.kt +++ b/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/events/AnimationEventType.kt @@ -31,4 +31,5 @@ enum class AnimationEventType { IDLE, TEXTURE_ANIMATION_START, EMOTE, + BY_OTHER, // triggered by other cosmetic animation event } diff --git a/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/state/EssentialAnimationSystem.kt b/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/state/EssentialAnimationSystem.kt index e073912..6b600a6 100644 --- a/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/state/EssentialAnimationSystem.kt +++ b/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/cosmetics/state/EssentialAnimationSystem.kt @@ -16,6 +16,7 @@ import gg.essential.cosmetics.events.AnimationEventType import gg.essential.cosmetics.events.AnimationTarget import gg.essential.model.BedrockModel import gg.essential.model.ModelAnimationState +import gg.essential.model.ModelInstance import gg.essential.model.molang.MolangQueryEntity import kotlin.random.Random @@ -30,6 +31,17 @@ class EssentialAnimationSystem( private val ongoingAnimations = mutableSetOf() private val animationStates = AnimationEffectStates() + private val pendingAnimationsForOtherModels = mutableSetOf() + + fun triggerPendingAnimationsForOtherModels(models : Collection) { + if (pendingAnimationsForOtherModels.isEmpty()) return + + pendingAnimationsForOtherModels.forEach { pending -> + models.forEach{ it.essentialAnimationSystem.fireTriggerFromAnimation(pending, AnimationEventType.BY_OTHER) } + } + pendingAnimationsForOtherModels.clear() + } + private class AnimationEffectStates { var skips = HashMap() } @@ -66,6 +78,9 @@ class EssentialAnimationSystem( if (animationState.active.isEmpty() && highestPriority != null) { val animation = bedrockModel.getAnimationByName(highestPriority.name) if (animation != null) { + if (highestPriority.triggerInOtherCosmetic.isNotEmpty()) { + pendingAnimationsForOtherModels.addAll(highestPriority.triggerInOtherCosmetic) + } animationState.startAnimation(animation) } } @@ -112,13 +127,13 @@ class EssentialAnimationSystem( } } - fun fireTriggerFromAnimation(animationName: String) { + fun fireTriggerFromAnimation(animationName: String, requiredType : AnimationEventType? = null) { if (animationName == "texture_start") { textureAnimationSync.syncTextureStart() return } for (animationEvent in bedrockModel.animationEvents) { - if (animationEvent.name == animationName) { + if (animationEvent.name == animationName && (requiredType == null || animationEvent.type == requiredType)) { ongoingAnimations.add(animationEvent) updateAnimationState() break diff --git a/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/featured/featuredPage.kt b/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/featured/featuredPage.kt new file mode 100644 index 0000000..70d67a0 --- /dev/null +++ b/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/mod/cosmetics/featured/featuredPage.kt @@ -0,0 +1,92 @@ +/* + * Copyright (c) 2024 ModCore Inc. All rights reserved. + * + * This code is part of ModCore Inc.'s Essential Mod repository and is protected + * under copyright registration # TX0009138511. For the full license, see: + * https://github.com/EssentialGG/Essential/blob/main/LICENSE + * + * You may not use, copy, reproduce, modify, sell, license, distribute, + * commercialize, or otherwise exploit, or create derivative works based + * upon, this file or any other in this repository, all of which is reserved by Essential. + */ +package gg.essential.mod.cosmetics.featured + +import kotlinx.serialization.KSerializer +import kotlinx.serialization.Serializable +import kotlinx.serialization.descriptors.SerialDescriptor +import kotlinx.serialization.encoding.Decoder +import kotlinx.serialization.encoding.Encoder + +@Serializable(with = FeaturedPageSerializer::class) +data class FeaturedPage( + val rows: List, +) + +@Serializable +private data class FeaturedPageRaw( + val rows: List>, + val dividers: Map>? = null, +) + +sealed interface FeaturedPageComponent + +data class FeaturedItemRow(val items: List) : FeaturedPageComponent + +sealed class BaseDivider : FeaturedPageComponent + +data object BlankDivider : BaseDivider() + +data class TextDivider(val text: String) : BaseDivider() + +enum class DividerType { + BLANK, + TEXT +} + +object FeaturedPageSerializer : KSerializer { + private val inner = FeaturedPageRaw.serializer() + override val descriptor: SerialDescriptor = inner.descriptor + + override fun deserialize(decoder: Decoder): FeaturedPage { + val featuredPageRaw = inner.deserialize(decoder) + + val rowList = featuredPageRaw.rows.map { row -> FeaturedItemRow(row) } + val dividerList = featuredPageRaw.dividers ?: emptyMap() + + fun List.convertToDividerList() : List { + return map { if (it == null) BlankDivider else TextDivider(it) } + } + + val mergedList = rowList.foldIndexed(mutableListOf()) { index, acc, featuredItemRow -> + acc += dividerList[index]?.convertToDividerList() ?: emptyList() + acc += featuredItemRow + acc + }.apply { + dividerList.keys.filter { it >= rowList.size } + .sorted().forEach { addAll(dividerList[it]?.convertToDividerList() ?: emptyList()) } + } + return FeaturedPage(mergedList) + } + + override fun serialize(encoder: Encoder, value: FeaturedPage) { + val dividers = mutableMapOf>() + var originalIndex = 0 + for (componentRow in value.rows) { + when (componentRow) { + is FeaturedItemRow -> { + originalIndex++ + } + + is BaseDivider -> { + dividers.computeIfAbsent(originalIndex) { mutableListOf() }.add( + if (componentRow is TextDivider) componentRow.text else null + ) + } + } + } + + val featuredPageRaw = FeaturedPageRaw(value.rows.filterIsInstance().map { it.items }, dividers) + inner.serialize(encoder, featuredPageRaw) + } + +} \ No newline at end of file diff --git a/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/model/ModelInstance.kt b/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/model/ModelInstance.kt index b2f707d..16c30b5 100644 --- a/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/model/ModelInstance.kt +++ b/subprojects/cosmetics/src/commonMain/kotlin/gg/essential/model/ModelInstance.kt @@ -60,31 +60,6 @@ class ModelInstance( return model.computePose(basePose, animationState, entity) } - private var lastUpdateTime = Float.NEGATIVE_INFINITY - - /** - * Updates the state of this model for the current frame prior to rendering. - * - * Note that new animation events are emitted into [ModelAnimationState.pendingEvents] and the caller needs to - * collect them from there and forward them to the actual particle/sound/etc system at the appropriate time. - * - * Also note that this does **not** yet update the locators bound to this model instance. For that one must call - * [updateLocators] after rendering. - * This is because the position and rotation of locators depends on the final rendered player pose, which is only - * available after rendering. - * Because particle events may depend on the position of locators, they should however ideally be updated before - * particles are updated, rendered, and/or spawned. - */ - fun update() { - val now = entity.lifeTime - if (lastUpdateTime == now) return // was already updated this frame - lastUpdateTime = now - - essentialAnimationSystem.maybeFireTextureAnimationStartEvent() - essentialAnimationSystem.updateAnimationState() - animationState.updateEffects() - } - /** * Updates all locators bound to this model instance. * diff --git a/subprojects/ice/src/main/kotlin/gg/essential/ice/stun/StunSocket.kt b/subprojects/ice/src/main/kotlin/gg/essential/ice/stun/StunSocket.kt index 5061c1c..8bf711f 100644 --- a/subprojects/ice/src/main/kotlin/gg/essential/ice/stun/StunSocket.kt +++ b/subprojects/ice/src/main/kotlin/gg/essential/ice/stun/StunSocket.kt @@ -96,6 +96,7 @@ class StunSocket( // instead use UNDISPATCHED and yield as soon as we're inside our try-finally. hostSocket.use { socket -> yield() + val sha256 = MessageDigest.getInstance("SHA-256") val knownUnreachable = mutableSetOf() for ((packet, deferred) in hostSendChannel) { if (packet.address in knownUnreachable) { @@ -133,6 +134,7 @@ class StunSocket( val packetsToBeSorted = Channel(10) hostSocketScope.launch(Dispatchers.IO) { + val sha256 = MessageDigest.getInstance("SHA-256") val buf = DatagramPacket(ByteArray(1500), 1500) while (coroutineContext.isActive) { try { @@ -583,7 +585,6 @@ class StunSocket( companion object { private val LOG_UDP_PACKET_CONTENT = System.getProperty("essential.sps.log_udp_packet_content").toBoolean() - private val sha256 = MessageDigest.getInstance("SHA-256") private fun ByteArray.maybeSliceArray(offset: Int, length: Int) = if (offset == 0 && length == size) this else sliceArray(offset until offset + length) diff --git a/subprojects/vigilance2/src/main/kotlin/gg/essential/mod/vigilance2/GuiBuilder.kt b/subprojects/vigilance2/src/main/kotlin/gg/essential/mod/vigilance2/GuiBuilder.kt index 5d7fec4..87c651d 100644 --- a/subprojects/vigilance2/src/main/kotlin/gg/essential/mod/vigilance2/GuiBuilder.kt +++ b/subprojects/vigilance2/src/main/kotlin/gg/essential/mod/vigilance2/GuiBuilder.kt @@ -16,6 +16,7 @@ import gg.essential.gui.elementa.state.v2.MutableState import gg.essential.gui.elementa.state.v2.Observer import gg.essential.gui.elementa.state.v2.State import gg.essential.gui.elementa.state.v2.combinators.bimap +import gg.essential.gui.elementa.state.v2.memo import gg.essential.gui.elementa.state.v2.mutableStateOf import gg.essential.gui.elementa.state.v2.stateOf import gg.essential.gui.elementa.state.v2.toListState @@ -80,6 +81,12 @@ class GuiBuilder internal constructor( } } + fun dynamic(block: ObservingCategoryBuilder.() -> Unit) { + content.add(CategoryContent.Dynamic(memo { + ObservingCategoryBuilder(this, guiBuilder, category, subcategory).apply(block) + })) + } + fun subcategory(name: String, block: CategoryBuilder.() -> Unit) { content.addAll(CategoryBuilder(guiBuilder, category, name).apply(block).content) } @@ -135,6 +142,13 @@ class GuiBuilder internal constructor( } } + class ObservingCategoryBuilder( + private val observer: Observer, + guiBuilder: GuiBuilder, + category: String, + subcategory: String, + ) : CategoryBuilder(guiBuilder, category, subcategory), Observer by observer + interface CommonPropertyBuilder { var name: String // default "" var description: String // default "" diff --git a/versions/1.16.2-1.12.2.txt b/versions/1.16.2-1.12.2.txt index 3f99652..4b3b74e 100644 --- a/versions/1.16.2-1.12.2.txt +++ b/versions/1.16.2-1.12.2.txt @@ -2,6 +2,7 @@ com.mojang.blaze3d.systems.RenderSystem enableAlphaTest() net.minecraft.client.r com.mojang.blaze3d.systems.RenderSystem disableAlphaTest() net.minecraft.client.renderer.GlStateManager disableAlpha() com.mojang.blaze3d.systems.RenderSystem net.minecraft.client.renderer.GlStateManager com.mojang.datafixers.DataFixer net.minecraft.util.datafix.DataFixer +net.minecraft.client.Minecraft getResourcePackList() getResourcePackRepository() net.minecraft.client.entity.player.RemoteClientPlayerEntity net.minecraft.client.entity.EntityOtherPlayerMP net.minecraft.client.gui.NewChatGui net.minecraft.client.gui.GuiNewChat net.minecraft.client.gui.NewChatGui func_238493_a_() setChatLine() @@ -63,6 +64,9 @@ net.minecraft.resources.IResourcePack net.minecraft.client.resources.IResourcePa net.minecraft.resources.IResourcePack getName() getPackName() net.minecraft.resources.ResourcePack net.minecraft.client.resources.AbstractResourcePack net.minecraft.resources.ResourcePack file resourcePackFile +net.minecraft.resources.ResourcePackList net.minecraft.client.resources.ResourcePackRepository +net.minecraft.resources.ResourcePackList reloadPacksFromFinders() updateRepositoryEntriesAll() +net.minecraft.resources.ResourcePackList getAllPacks() getRepositoryEntriesAll() net.minecraft.resources.SimpleReloadableResourceManager net.minecraft.client.resources.SimpleReloadableResourceManager net.minecraft.util.concurrent.ThreadTaskExecutor net.minecraft.util.IThreadListener net.minecraft.world.GameRules get() net.minecraft.world.GameRules getString()